diff --git a/.changeset/jetbrains-bundled-cli.md b/.changeset/jetbrains-bundled-cli.md new file mode 100644 index 00000000000..d0239ff1765 --- /dev/null +++ b/.changeset/jetbrains-bundled-cli.md @@ -0,0 +1,5 @@ +--- +"@kilocode/kilo-jetbrains": minor +--- + +Publish a signed GitHub-hosted JetBrains plugin build with the CLI bundled for offline installation. diff --git a/.github/docs-sync/collect.mjs b/.github/docs-sync/collect.mjs new file mode 100644 index 00000000000..51be1a3d8e1 --- /dev/null +++ b/.github/docs-sync/collect.mjs @@ -0,0 +1,137 @@ +// kilocode_change - new file + +/** + * Collects PRs merged to the source repos since the watermark, applies a + * deterministic pre-filter, and writes docs-sync-out/digest.json for the LLM + * triage pass. + * + * Pre-filter drops (triage never sees these): + * - PRs labeled auto-docs (this bot's own rolling PRs) + * - chore/test/ci/build/docs/style/refactor/revert conventional titles + * - PRs touching only docs/non-product paths + * + * Bot-authored PRs are kept: release/dependency bots ship user-facing + * changes too, and the label + docs-only guards above prevent loops. + */ + +import fs from "node:fs" +import { api, appendOutput, appendSummary, listPrFiles, searchIssues } from "./lib.mjs" + +const SOURCE_REPOS = ["Kilo-Org/cloud", "Kilo-Org/kilocode"] +const OUT_DIR = "docs-sync-out" +const BODY_LIMIT = 2000 +const SLIM_BODY_LIMIT = 300 +const PATCH_LIMIT = 8000 +const FILE_LIMIT = 30 +const DROP_TITLE = /^(chore|test|ci|build|docs|style|refactor|revert)(\(.+\))?!?:/i +const DOCS_ONLY_PATH = /^(packages\/kilo-docs\/|\.github\/docs-sync\/|docs-sync-out\/|docs\/|[^/]+\.md$)/ + +function argSince() { + const i = process.argv.indexOf("--since") + const v = i >= 0 ? process.argv[i + 1] : null + if (!v || Number.isNaN(new Date(v).getTime())) { + throw new Error("usage: collect.mjs --since ") + } + return new Date(v) +} + +async function mergedPrs(fullRepo, since) { + const query = `repo:${fullRepo} is:pr is:merged merged:>=${since.toISOString()}` + return searchIssues(query) +} + +const since = argSince() +console.log(`collecting PRs merged since ${since.toISOString()}`) + +const digest = [] +const dropped = { label: 0, title: 0, docs_only: 0, fetch_error: 0 } + +for (const fullRepo of SOURCE_REPOS) { + const prs = await mergedPrs(fullRepo, since) + console.log(`${fullRepo}: ${prs.length} merged PRs in window`) + + for (const item of prs) { + const author = item.user?.login ?? "" + if ((item.labels ?? []).some((l) => l.name === "auto-docs")) { + dropped.label++ + continue + } + if (DROP_TITLE.test(item.title ?? "")) { + dropped.title++ + continue + } + + const number = item.number + let pr + let files + try { + pr = await api(`/repos/${fullRepo}/pulls/${number}`) + files = await listPrFiles(fullRepo, number) + } catch (err) { + // Isolate per-PR failures: one dead PR must not abort the whole run. + console.warn(`::warning::skipping ${fullRepo}#${number}: ${err.message}`) + dropped.fetch_error++ + continue + } + // listPrFiles caps at 300 files; a truncated list can't support the + // docs-only classification, so keep such PRs and record the true total. + const truncated = files.length >= 300 + if (!truncated && files.length > 0 && files.every((f) => DOCS_ONLY_PATH.test(f.filename))) { + dropped.docs_only++ + continue + } + + let patch = "" + for (const f of files) { + if (!f.patch) continue + const chunk = `--- ${f.filename}\n${f.patch}\n` + if (patch.length + chunk.length > PATCH_LIMIT) { + patch += "\n... (diff truncated) ...\n" + break + } + patch += chunk + } + + digest.push({ + repo: fullRepo, + number, + title: pr.title, + url: pr.html_url, + author, + merged_at: pr.merged_at, + labels: (pr.labels ?? []).map((l) => l.name), + body: (pr.body ?? "").slice(0, BODY_LIMIT), + files: files.slice(0, FILE_LIMIT).map((f) => `${f.status} ${f.filename} (+${f.additions}/-${f.deletions})`), + files_total: pr.changed_files ?? files.length, + patch_excerpt: patch, + }) + } +} + +digest.sort((a, b) => new Date(a.merged_at) - new Date(b.merged_at)) + +fs.mkdirSync(OUT_DIR, { recursive: true }) +// Full digest (bodies + patch excerpts) is filtered down to docs-worthy PRs +// for the edit pass; the slim digest keeps the triage pass context small. +fs.writeFileSync(`${OUT_DIR}/digest-full.json`, JSON.stringify(digest, null, 2)) +const slim = digest.map(({ patch_excerpt, body, ...rest }) => ({ + ...rest, + body: body.slice(0, SLIM_BODY_LIMIT), +})) +fs.writeFileSync(`${OUT_DIR}/digest.json`, JSON.stringify(slim, null, 2)) + +console.log(`kept ${digest.length} PRs, dropped:`, dropped) +appendOutput("count", digest.length) +appendOutput("digest", `${OUT_DIR}/digest.json`) + +appendSummary( + [ + "### docs-sync collect", + "", + `- window: since \`${since.toISOString()}\``, + `- kept: **${digest.length}** PRs`, + `- dropped: ${dropped.label} auto-docs, ${dropped.title} title filter, ${dropped.docs_only} docs-only, ${dropped.fetch_error} fetch errors`, + "", + ...digest.map((d) => `- [${d.repo}#${d.number}](${d.url}) ${d.title}`), + ].join("\n"), +) diff --git a/.github/docs-sync/edit-prompt.md b/.github/docs-sync/edit-prompt.md new file mode 100644 index 00000000000..f0099e588f7 --- /dev/null +++ b/.github/docs-sync/edit-prompt.md @@ -0,0 +1,24 @@ +You are the Kilo Code documentation bot. You update the public product documentation in `packages/kilo-docs` (a Markdoc/Next.js site served at kilo.ai/docs) so it reflects recently merged PRs. You are handling one batch of PRs; the batch files and your output file are named at the end of these instructions. + +Before writing anything: + +1. Read `packages/kilo-docs/AGENTS.md` and `packages/kilo-docs/STYLE_GUIDE.md` and follow them exactly: Markdoc custom tags, the `/docs` prefix in image paths, navigation files under `lib/nav/`, redirect rules, and the generated-screenshot policy. +2. Read the attached batch files: the full-details file (PR title, body, file list, `patch_excerpt` diffs) and the triage file (docs-worthiness verdicts, target sections, priorities). + +For each PR in the batch, in priority order: + +- Find the most relevant existing docs page(s) and make minimal, precise updates in the style of the surrounding content. +- Create a new page only when no existing page fits; then add it to the matching nav file in `packages/kilo-docs/lib/nav/`. +- Document only behavior that is actually present in the merged diff. If the PR body or diff shows the feature is behind a flag or otherwise not user-visible yet, skip it and record why. +- If a PR turns out not to need documentation, skip it and record why. Trust evidence over the triage verdict. + +Hard rules: + +- Only create or modify files under `packages/kilo-docs/`. Never touch code, tests, config, images, or anything outside that directory. +- Never remove or rename pages. Never document unreleased behavior. Never copy internal PR discussion into the docs; write user-facing documentation. +- Do not run git commands and do not commit anything; automation handles git. +- Keep the change small and precise. Do not rewrite sections that are already accurate. + +When finished, write the summary JSON file named in the batch specifics below: a JSON array with exactly one entry per batch PR, consumed by automation (this file is never committed). Use `action` values like `updated `, `created `, or `skipped`. Example: + +[{"pr": 123, "url": "https://github.com/Kilo-Org/kilocode/pull/123", "action": "updated pages/code-with-ai/platforms/cli.md", "reason": "documented --variant flag"}, {"pr": 124, "url": "https://github.com/Kilo-Org/kilocode/pull/124", "action": "skipped", "reason": "feature behind unreleased flag"}] diff --git a/.github/docs-sync/edit.mjs b/.github/docs-sync/edit.mjs new file mode 100644 index 00000000000..52943de31ab --- /dev/null +++ b/.github/docs-sync/edit.mjs @@ -0,0 +1,122 @@ +// kilocode_change - new file + +/** + * Runs the LLM edit pass over docs-sync-out/worthy.json in batches. + * + * Batching bounds each `kilo run` context (a replay window can yield dozens + * of docs-worthy PRs with large diffs). Each batch gets its own CLI session + * and writes its own summary file; results are merged into + * docs-sync-out/edit-summary.json. A batch that fails is skipped with a + * warning — its PRs show up in the rolling PR body as skipped, so nothing + * fails silently. + * + * Env: EDIT_MODEL (provider/model), KILO_API_KEY (set by workflow; read natively by the kilo provider). + */ + +import { execFileSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const BATCH_SIZE = 5 +const ATTEMPTS = 2 +const OUT_DIR = "docs-sync-out" +export const SUMMARY_FILE = ".docs-sync-summary.json" + +const HERE = path.dirname(fileURLToPath(import.meta.url)) +const basePrompt = fs.readFileSync(path.join(HERE, "edit-prompt.md"), "utf8") +const model = process.env.EDIT_MODEL +if (!model) throw new Error("EDIT_MODEL is required") + +const worthy = JSON.parse(fs.readFileSync(`${OUT_DIR}/worthy.json`, "utf8")) +const triage = JSON.parse(fs.readFileSync(`${OUT_DIR}/triage.json`, "utf8")) +const priority = new Map(triage.map((e) => [e.url, e])) +const ordered = [...worthy].sort((a, b) => { + const rank = { high: 0, medium: 1, low: 2 } + return (rank[priority.get(a.url)?.priority] ?? 1) - (rank[priority.get(b.url)?.priority] ?? 1) +}) + +function editBatch(batch, index) { + const batchFile = `${OUT_DIR}/edit-batch-${index}.json` + const triageFile = `${OUT_DIR}/edit-batch-triage-${index}.json` + const summaryFile = `${OUT_DIR}/edit-summary-${index}.json` + fs.writeFileSync(batchFile, JSON.stringify(batch, null, 2)) + fs.writeFileSync( + triageFile, + JSON.stringify( + batch.map((d) => priority.get(d.url)).filter(Boolean), + null, + 2, + ), + ) + + const prompt = `${basePrompt} + +Batch specifics for this run: the PRs to handle are in the attached ${batchFile} (full details) and ${triageFile} (triage verdicts). Handle ONLY the PRs in these batch files. When finished, write your per-PR results in the summary JSON format described above to the file \`${summaryFile}\` (path relative to the repository root).` + + for (let attempt = 1; attempt <= ATTEMPTS; attempt++) { + try { + // Message positional first: --file is multi-value and would otherwise + // consume a trailing message as a file path ("File not found"). + execFileSync( + "kilo", + ["run", prompt, "-m", model, "--variant", "high", "--dir", process.cwd(), "-f", batchFile, "-f", triageFile], + // stdout streams live to the Actions log; stderr is piped so failure + // warnings can include the tail of the actual CLI error. + { encoding: "utf8", maxBuffer: 32 * 1024 * 1024, timeout: 25 * 60 * 1000, stdio: ["ignore", "inherit", "pipe"] }, + ) + if (fs.existsSync(summaryFile)) return true + // Tolerate the agent dropping the docs-sync-out/ prefix. + const alt = path.basename(summaryFile) + if (fs.existsSync(alt)) { + fs.renameSync(alt, summaryFile) + return true + } + console.warn(`batch ${index} attempt ${attempt}: summary file ${summaryFile} not produced`) + } catch (err) { + const stderr = String(err.stderr ?? "").trim().split("\n").slice(-5).join("\n") + console.warn(`batch ${index} attempt ${attempt}: kilo run failed: ${stderr || err.message}`) + } + } + console.warn(`::warning::edit batch ${index} failed after ${ATTEMPTS} attempts; ${batch.length} PRs skipped`) + return false +} + +const batches = [] +for (let i = 0; i < ordered.length; i += BATCH_SIZE) { + batches.push(ordered.slice(i, i + BATCH_SIZE)) +} +console.log(`editing docs for ${ordered.length} PRs in ${batches.length} batches of up to ${BATCH_SIZE}`) + +for (let i = 0; i < batches.length; i++) { + editBatch(batches[i], i) +} + +// Merge batch summaries. Coverage: every worthy PR gets an entry so the PR +// body accounts for it; failed batches show up as skipped. +const merged = [] +const seen = new Set() +for (let i = 0; i < batches.length; i++) { + const file = `${OUT_DIR}/edit-summary-${i}.json` + let entries = [] + try { + entries = JSON.parse(fs.readFileSync(file, "utf8")) + } catch { + continue + } + for (const e of entries) { + const url = String(e?.url ?? "") + if (!url.startsWith("http") || seen.has(url)) continue + seen.add(url) + merged.push({ pr: Number(e.pr) || 0, url, action: String(e.action ?? "skipped"), reason: String(e.reason ?? "") }) + } +} +for (const d of ordered) { + if (seen.has(d.url)) continue + merged.push({ pr: d.number, url: d.url, action: "skipped", reason: "edit pass failed or timed out for this PR" }) +} + +// upsert-pr.mjs consumes the merged summary from the repo root; the file is +// removed there before committing so it never lands in the docs PR. +fs.writeFileSync(SUMMARY_FILE, JSON.stringify(merged, null, 2)) +console.log(`edit pass complete: ${merged.filter((e) => e.action !== "skipped").length} changed, ${merged.filter((e) => e.action === "skipped").length} skipped`) diff --git a/.github/docs-sync/extract-json.mjs b/.github/docs-sync/extract-json.mjs new file mode 100644 index 00000000000..c8cdf04ff12 --- /dev/null +++ b/.github/docs-sync/extract-json.mjs @@ -0,0 +1,76 @@ +// kilocode_change - new file + +/** + * Extracts and validates the triage JSON array from raw LLM stdout. + * Usage: extract-json.mjs + * Exit 0 on success, 1 on any failure. Also exports parseTriageEntries for + * the chunked triage runner. + */ + +import fs from "node:fs" +import { pathToFileURL } from "node:url" + +/** Returns validated triage entries, or null when extraction fails. */ +export function parseTriageEntries(raw) { + // `kilo run` prints the assistant message twice (streaming render + final + // summary), so stdout can hold the same array back-to-back. Try each "[" + // from the right and return the first slice that parses — i.e. the last + // (most recent) valid array in the output. + const end = raw.lastIndexOf("]") + if (end < 0) return null + + const starts = [] + for (let i = 0; i <= end; i++) { + if (raw[i] === "[") starts.push(i) + } + + for (let s = starts.length - 1; s >= 0; s--) { + let parsed + try { + parsed = JSON.parse(raw.slice(starts[s], end + 1)) + } catch { + continue + } + if (!Array.isArray(parsed)) continue + const entries = validate(parsed) + if (entries) return entries + } + return null +} + +function validate(parsed) { + const entries = [] + for (const e of parsed) { + const pr = Number(e?.pr) + const url = String(e?.url ?? "") + if (!Number.isInteger(pr) || !url.startsWith("http")) continue + entries.push({ + pr, + url, + docs_worthy: e.docs_worthy === true, + reason: String(e.reason ?? ""), + target_sections: Array.isArray(e.target_sections) ? e.target_sections.map(String) : [], + priority: ["high", "medium", "low"].includes(e.priority) ? e.priority : "medium", + }) + } + return entries.length > 0 ? entries : null +} + +function main() { + const [, , inputPath, outputPath] = process.argv + if (!inputPath || !outputPath) { + console.error("usage: extract-json.mjs ") + process.exit(1) + } + const entries = parseTriageEntries(fs.readFileSync(inputPath, "utf8")) + if (!entries) { + console.error("no valid triage JSON array found in input") + process.exit(1) + } + fs.writeFileSync(outputPath, JSON.stringify(entries, null, 2)) + console.log(`extracted ${entries.length} triage entries (${entries.filter((e) => e.docs_worthy).length} docs-worthy)`) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main() +} diff --git a/.github/docs-sync/filter-worthy.mjs b/.github/docs-sync/filter-worthy.mjs new file mode 100644 index 00000000000..ea99f3d2589 --- /dev/null +++ b/.github/docs-sync/filter-worthy.mjs @@ -0,0 +1,24 @@ +// kilocode_change - new file + +/** + * Filters the full digest down to PRs the triage pass marked docs-worthy. + * Usage: filter-worthy.mjs + * The edit pass consumes the output so its context stays small. + */ + +import fs from "node:fs" + +const [, , digestPath, triagePath, outputPath] = process.argv +if (!digestPath || !triagePath || !outputPath) { + console.error("usage: filter-worthy.mjs ") + process.exit(1) +} + +const digest = JSON.parse(fs.readFileSync(digestPath, "utf8")) +const triage = JSON.parse(fs.readFileSync(triagePath, "utf8")) + +const worthy = new Set(triage.filter((e) => e.docs_worthy).map((e) => e.url)) +const out = digest.filter((d) => worthy.has(d.url)) + +fs.writeFileSync(outputPath, JSON.stringify(out, null, 2)) +console.log(`${out.length} of ${digest.length} digest entries are docs-worthy`) diff --git a/.github/docs-sync/lib.mjs b/.github/docs-sync/lib.mjs new file mode 100644 index 00000000000..56dfb7b9226 --- /dev/null +++ b/.github/docs-sync/lib.mjs @@ -0,0 +1,113 @@ +// kilocode_change - new file + +/** + * Shared helpers for the docs-sync bot scripts. Dependency-free (Node 20+ + * global fetch) so the workflow does not rely on runner images shipping the + * gh CLI. + */ + +import fs from "node:fs" + +const API = "https://api.github.com" +const MAX_RETRIES = 3 + +export function token() { + const t = process.env.GH_TOKEN || process.env.GITHUB_TOKEN + if (!t) throw new Error("GH_TOKEN (or GITHUB_TOKEN) is required") + return t +} + +export function repo() { + const r = process.env.GITHUB_REPOSITORY + if (!r) throw new Error("GITHUB_REPOSITORY is required") + return r +} + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) + +export async function api(path, { method = "GET", body } = {}) { + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + let res + try { + res = await fetch(`${API}${path}`, { + method, + headers: { + authorization: `Bearer ${token()}`, + accept: "application/vnd.github+json", + "x-github-api-version": "2022-11-28", + "user-agent": "kilo-docs-sync-bot", + }, + body: body === undefined ? undefined : JSON.stringify(body), + }) + } catch (err) { + if (attempt < MAX_RETRIES) { + console.warn(`network error (${err.message}), retrying in ${5 * attempt}s`) + await sleep(5000 * attempt) + continue + } + throw err + } + + if (res.status === 403) { + const text = await res.text() + if (text.includes("rate limit") && attempt < MAX_RETRIES) { + const retryAfter = Number(res.headers.get("retry-after")) || 30 + console.warn(`rate limited, retrying in ${retryAfter}s`) + await sleep(retryAfter * 1000) + continue + } + const err = new Error(`${method} ${path} -> 403: ${text}`) + err.status = 403 + throw err + } + + if (res.status >= 500 && attempt < MAX_RETRIES) { + console.warn(`${method} ${path} -> ${res.status}, retrying in ${5 * attempt}s`) + await sleep(5000 * attempt) + continue + } + + if (!res.ok) { + const text = await res.text() + const err = new Error(`${method} ${path} -> ${res.status}: ${text}`) + err.status = res.status + throw err + } + + if (res.status === 204) return null + return res.json() + } + throw new Error(`${method} ${path}: exhausted retries`) +} + +/** Paginated search/issues. Caps at `maxPages` * 100 results. */ +export async function searchIssues(query, { maxPages = 5 } = {}) { + const items = [] + for (let page = 1; page <= maxPages; page++) { + const data = await api(`/search/issues?q=${encodeURIComponent(query)}&per_page=100&page=${page}`) + items.push(...(data.items ?? [])) + if ((data.items ?? []).length < 100) break + } + return items +} + +export async function listPrFiles(fullRepo, number, { maxPages = 3 } = {}) { + const files = [] + for (let page = 1; page <= maxPages; page++) { + const batch = await api(`/repos/${fullRepo}/pulls/${number}/files?per_page=100&page=${page}`) + files.push(...batch) + if (batch.length < 100) break + } + return files +} + +export function appendOutput(name, value) { + const out = process.env.GITHUB_OUTPUT + if (out) fs.appendFileSync(out, `${name}=${value}\n`) + console.log(`output ${name}=${value}`) +} + +export function appendSummary(markdown) { + const summary = process.env.GITHUB_STEP_SUMMARY + if (summary) fs.appendFileSync(summary, markdown + "\n") +} diff --git a/.github/docs-sync/prepare-branch.mjs b/.github/docs-sync/prepare-branch.mjs new file mode 100644 index 00000000000..2b57932c442 --- /dev/null +++ b/.github/docs-sync/prepare-branch.mjs @@ -0,0 +1,61 @@ +// kilocode_change - new file + +/** + * Prepares the rolling docs-sync branch before the edit pass: + * - an open auto-docs PR exists -> check out its head branch and merge + * origin/main (preserves any human commits on the branch) + * - otherwise -> fresh branch from origin/main (bot force-pushes later) + * + * Outputs: branch, mode (update|fresh), pr_number (empty when fresh). + */ + +import { execFileSync } from "node:child_process" +import { api, appendOutput, repo, searchIssues } from "./lib.mjs" + +export const DEFAULT_BRANCH = "docs/auto-sync" + +const git = (args) => execFileSync("git", args, { stdio: ["ignore", "pipe", "inherit"] }).toString().trim() + +const prs = await searchIssues(`repo:${repo()} is:pr is:open label:auto-docs sort:created-desc`, { maxPages: 1 }) + +let mode = "fresh" +let prNumber = "" +let branch = DEFAULT_BRANCH + +if (prs.length > 0) { + const pr = await api(`/repos/${repo()}/pulls/${prs[0].number}`) + branch = pr.head?.ref ?? DEFAULT_BRANCH + prNumber = String(pr.number) + git(["fetch", "origin", "main", branch]) + git(["checkout", branch]) + try { + git(["merge", "origin/main", "--no-edit"]) + mode = "update" + } catch { + console.warn(`merge of origin/main into ${branch} conflicted.`) + console.warn("Leaving the conflicted branch untouched so human commits are preserved; continuing on a fresh dated branch.") + git(["merge", "--abort"]) + branch = `${DEFAULT_BRANCH}-${new Date().toISOString().slice(0, 10)}` + try { + git(["fetch", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]) + } catch { + console.log(`dated branch ${branch} does not exist on origin yet; will create it on push`) + } + git(["checkout", "-B", branch, "origin/main"]) + mode = "conflict" + } +} else { + // Keep the remote-tracking ref current so the later --force-with-lease + // push (stale branch left over from a merged/closed PR) is safe. + try { + git(["fetch", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`]) + } catch { + console.log(`branch ${branch} does not exist on origin yet; will create it on push`) + } + git(["checkout", "-B", branch, "origin/main"]) +} + +appendOutput("branch", branch) +appendOutput("mode", mode) +appendOutput("pr_number", prNumber) +console.log(`branch ${branch} ready (mode=${mode}, pr=${prNumber || "none"})`) diff --git a/.github/docs-sync/triage-prompt.md b/.github/docs-sync/triage-prompt.md new file mode 100644 index 00000000000..10e54406e89 --- /dev/null +++ b/.github/docs-sync/triage-prompt.md @@ -0,0 +1,19 @@ +You are the triage pass of an automated documentation pipeline for Kilo Code. Kilo Code is an open-source agentic engineering platform: VS Code extension, JetBrains plugin, CLI, and the kilo.ai cloud platform (teams, KiloClaw, gateway, code reviews). + +The attached `digest.json` file contains PRs recently merged to Kilo-Org/cloud and Kilo-Org/kilocode. Your only job is to decide which of them require changes to the public product documentation at kilo.ai/docs. + +A PR is docs-worthy ONLY if a user of Kilo Code would need to learn something new or change how they use the product after this PR ships. Examples: new commands, flags, settings, UI workflows, providers, pricing/limits changes, breaking behavior changes, or fixes that change documented behavior. + +A PR is NOT docs-worthy when it is: an internal refactor, infrastructure or CI work, a feature-flag scaffold that is not yet user-visible, test or dependency work, a bug fix that merely restores already-documented behavior, or a change only visible to contributors or self-hosters. + +Rules: + +- Include every input PR exactly once, identified by its `number` and `url`. Never invent PRs. +- When unsure, set `docs_worthy` to false and explain the doubt in `reason`. +- `target_sections` is only filled for docs-worthy PRs. Use rough docs areas, e.g. `getting-started`, `code-with-ai/platforms/cli`, `code-with-ai/platforms/vscode`, `code-with-ai/agents`, `ai-providers`, `teams`, `enterprise`, `automate`. +- `reason` is one short sentence, written for the human who reviews the final docs PR. +- `priority` reflects user impact: high = most users affected, medium = notable subset, low = edge case. + +Respond with a STRICT JSON array and nothing else: no prose, no markdown fences, no comments. Schema: + +[{"pr": 123, "url": "https://github.com/Kilo-Org/kilocode/pull/123", "docs_worthy": true, "reason": "Adds --variant flag to kilo run", "target_sections": ["code-with-ai/platforms/cli"], "priority": "high"}] diff --git a/.github/docs-sync/triage.mjs b/.github/docs-sync/triage.mjs new file mode 100644 index 00000000000..034a7ddcbda --- /dev/null +++ b/.github/docs-sync/triage.mjs @@ -0,0 +1,111 @@ +// kilocode_change - new file + +/** + * Runs the LLM triage pass over docs-sync-out/digest.json in chunks. + * + * A daily window holds ~30-50 PRs; a replay can hold several hundred. A + * single triage call over that volume truncates its JSON output, so the + * digest is split into chunks of CHUNK_SIZE and each chunk is triaged with + * its own `kilo run` call. A chunk that fails twice is degraded to + * "unclassified" entries (docs_worthy=false) instead of failing the run — + * the PR body then shows those PRs as skipped, visible to reviewers. + * + * Env: TRIAGE_MODEL (provider/model), KILO_API_KEY (gateway auth, set by the workflow; + * the kilo provider reads it natively). Reads the prompt from triage-prompt.md next to this script. + */ + +import { execFileSync } from "node:child_process" +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { parseTriageEntries } from "./extract-json.mjs" + +const CHUNK_SIZE = 25 +const ATTEMPTS = 2 +const OUT_DIR = "docs-sync-out" + +const HERE = path.dirname(fileURLToPath(import.meta.url)) +const prompt = fs.readFileSync(path.join(HERE, "triage-prompt.md"), "utf8") +const model = process.env.TRIAGE_MODEL +if (!model) throw new Error("TRIAGE_MODEL is required") + +const digest = JSON.parse(fs.readFileSync(`${OUT_DIR}/digest.json`, "utf8")) + +function triageChunk(chunk, index) { + const chunkFile = `${OUT_DIR}/triage-chunk-${index}.json` + fs.writeFileSync(chunkFile, JSON.stringify(chunk, null, 2)) + + for (let attempt = 1; attempt <= ATTEMPTS; attempt++) { + let raw + try { + // Message positional first: --file is multi-value and would otherwise + // consume a trailing message as a file path ("File not found"). + raw = execFileSync( + "kilo", + ["run", prompt, "-m", model, "--dir", process.cwd(), "-f", chunkFile], + { encoding: "utf8", maxBuffer: 32 * 1024 * 1024, timeout: 10 * 60 * 1000, stdio: ["ignore", "pipe", "pipe"] }, + ) + } catch (err) { + const stderr = String(err.stderr ?? "").trim().split("\n").slice(-5).join("\n") + console.warn(`chunk ${index} attempt ${attempt}: kilo run failed: ${stderr || err.message}`) + continue + } + fs.writeFileSync(`${OUT_DIR}/triage-raw-${index}.txt`, raw) + const entries = parseTriageEntries(raw) + if (entries) { + // An entry for a PR outside this chunk must not win the shared dedupe + // against the chunk that actually owns it — drop foreign entries. + const allowed = new Set(chunk.map((d) => d.url)) + const owned = entries.filter((e) => allowed.has(e.url)) + if (owned.length !== entries.length) { + console.warn(`chunk ${index}: dropped ${entries.length - owned.length} entries for PRs outside the chunk`) + } + if (owned.length > 0) return owned + } + console.warn(`chunk ${index} attempt ${attempt}: no valid JSON in output`) + } + + console.warn(`::warning::chunk ${index} failed triage after ${ATTEMPTS} attempts; marking ${chunk.length} PRs unclassified`) + return chunk.map((d) => ({ + pr: d.number, + url: d.url, + docs_worthy: false, + reason: "triage failed to classify this PR", + target_sections: [], + priority: "medium", + })) +} + +const chunks = [] +for (let i = 0; i < digest.length; i += CHUNK_SIZE) { + chunks.push(digest.slice(i, i + CHUNK_SIZE)) +} +console.log(`triaging ${digest.length} PRs in ${chunks.length} chunks of up to ${CHUNK_SIZE}`) + +const merged = [] +const seen = new Set() +for (let i = 0; i < chunks.length; i++) { + for (const e of triageChunk(chunks[i], i)) { + if (seen.has(e.url)) continue + seen.add(e.url) + merged.push(e) + } +} + +// Coverage: every digest PR gets a triage entry so the PR body's skipped +// table is complete. Unclassified defaults to not-docs-worthy (conservative). +for (const d of digest) { + if (seen.has(d.url)) continue + merged.push({ + pr: d.number, + url: d.url, + docs_worthy: false, + reason: "not classified by triage", + target_sections: [], + priority: "medium", + }) +} + +fs.writeFileSync(`${OUT_DIR}/triage.json`, JSON.stringify(merged, null, 2)) +const worthy = merged.filter((e) => e.docs_worthy).length +console.log(`triage complete: ${merged.length} entries, ${worthy} docs-worthy`) diff --git a/.github/docs-sync/upsert-pr.mjs b/.github/docs-sync/upsert-pr.mjs new file mode 100644 index 00000000000..e39551f307c --- /dev/null +++ b/.github/docs-sync/upsert-pr.mjs @@ -0,0 +1,241 @@ +// kilocode_change - new file + +/** + * Commits the agent's packages/kilo-docs changes, pushes the rolling branch, + * and creates or updates the rolling auto-docs PR. + * + * No-op when the agent produced no docs changes. PRs become drafts when the + * diff exceeds the file cap or verification failed. The PR body carries + * marker-delimited sections so later runs can append rows, plus a + * machine-readable processed-through watermark. + */ + +import { execFileSync } from "node:child_process" +import fs from "node:fs" +import { pathToFileURL } from "node:url" + +const BRANCH = process.env.BRANCH || "docs/auto-sync" +const FILE_CAP = 15 +const ROW_CAP = 150 +const SUMMARY_FILE = ".docs-sync-summary.json" +const DOCS_PATH = "packages/kilo-docs" + +const git = (args) => execFileSync("git", args, { stdio: ["ignore", "pipe", "inherit"] }).toString().trim() + +// Agent-generated strings land in the PR body next to machine-read markers. +// Strip HTML-comment sequences so a crafted/adversarial value cannot forge +// section boundaries or the processed-through watermark. +function clean(value) { + return String(value ?? "").replaceAll("", "") +} + +function shortRef(url) { + return clean(url).replace("https://github.com/", "").replace("/pull/", "#") +} + +function changeRow(e) { + return `| ${clean(e.action).replaceAll("|", "\\|")} | [${shortRef(e.url)}](${clean(e.url)}) |` +} + +function skippedRow(e) { + const reason = clean(e.reason).replaceAll("|", "\\|").replaceAll("\n", " ") + return `| [${shortRef(e.url)}](${clean(e.url)}) | ${reason} |` +} + +export function extractSectionRows(body, name) { + const m = String(body ?? "").match( + new RegExp(`([\\s\\S]*?)`), + ) + if (!m) return [] + return m[1] + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.startsWith("|") && !l.startsWith("| ---") && !/^\|\s*Docs change/.test(l) && !/^\|\s*PR\s*\|/.test(l)) +} + +function section(name, header, rows) { + const body = rows.length > 0 ? [header, "| --- | --- |", ...rows].join("\n") : "_None._" + return `\n${body}\n` +} + +export function renderBody({ date, since, through, changesRows, skippedRows, verified, draftReasons, note }) { + return `## Automated docs sync — ${date} + +This PR keeps kilo.ai/docs in sync with features merged to [Kilo-Org/cloud](https://github.com/Kilo-Org/cloud) and [Kilo-Org/kilocode](https://github.com/Kilo-Org/kilocode). Every change below links to the merged PR it documents. + +- Window: \`${since}\` → \`${through}\` +- Verification (docs build + tests): **${verified ? "passing" : "FAILING — needs a human look"}** +${note ? `- ${note}\n` : ""}${draftReasons.length > 0 ? `- Draft because: ${draftReasons.join("; ")}\n` : ""} +### Changes + +${section("changes", "| Docs change | Source |", changesRows)} + +### Considered, no docs change needed + +${section("skipped", "| PR | Reason |", skippedRows)} + +--- + +(bot) Generated by the docs-sync workflow. Humans review and merge; while this PR stays open, the next daily run appends new changes here. Branch: \`${BRANCH}\`. + +` +} + +function mergeRows(oldRows, newRows) { + const seen = new Set() + const out = [] + for (const row of [...oldRows, ...newRows]) { + if (seen.has(row)) continue + seen.add(row) + out.push(row) + } + return out.slice(-ROW_CAP) +} + +function readJson(path, fallback) { + try { + return JSON.parse(fs.readFileSync(path, "utf8")) + } catch { + return fallback + } +} + +async function main() { + const { api, appendOutput, appendSummary, repo } = await import("./lib.mjs") + + const through = process.env.PROCESSED_THROUGH ?? new Date().toISOString() + const since = process.env.SINCE ?? "unknown" + const mode = ["update", "conflict"].includes(process.env.PREP_MODE) ? process.env.PREP_MODE : "fresh" + const existingPr = process.env.PR_NUMBER || "" + const verified = process.env.VERIFIED === "true" + const date = through.slice(0, 10) + + // The agent's run summary is consumed here and never committed. + const agentSummary = readJson(SUMMARY_FILE, []) + fs.rmSync(SUMMARY_FILE, { force: true }) + const triage = readJson("docs-sync-out/triage.json", []) + + if (git(["status", "--porcelain", "--", DOCS_PATH]) === "") { + console.log("no packages/kilo-docs changes produced; nothing to commit") + appendSummary("### docs-sync: no docs changes\n\nThe agent found nothing worth documenting in this window.") + return + } + + git(["config", "user.name", "github-actions[bot]"]) + git(["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"]) + git(["add", DOCS_PATH]) + git(["commit", "-m", `docs: sync with merged PRs (${date})`]) + + // The draft cap bounds the cumulative PR diff, not just this run's commit. + const changedFiles = git(["diff", "--name-only", "origin/main...HEAD", "--", DOCS_PATH]) + .split("\n") + .filter(Boolean) + const draftReasons = [] + if (changedFiles.length > FILE_CAP) draftReasons.push(`diff exceeds ${FILE_CAP} files (${changedFiles.length})`) + if (!verified) draftReasons.push("docs build/tests not passing") + // Content gate: legitimate bot edits are docs pages and nav files. Anything + // else in the docs package (build config, components, tests) executes + // during the verify build, so force human review before merge. + const nonContent = changedFiles.filter( + (f) => !f.startsWith("packages/kilo-docs/pages/") && !f.startsWith("packages/kilo-docs/lib/nav/"), + ) + if (nonContent.length > 0) { + // File paths are agent-chosen; sanitize before they land in the PR body. + const listed = nonContent + .slice(0, 5) + .map((f) => clean(f).replaceAll("|", "\\|")) + .join(", ") + draftReasons.push(`touches non-content files outside pages/ and lib/nav/: ${listed}`) + } + const draft = draftReasons.length > 0 + + git(mode === "update" ? ["push", "origin", `HEAD:${BRANCH}`] : ["push", "--force-with-lease", "origin", `HEAD:${BRANCH}`]) + + const changesNew = agentSummary.filter((e) => e.action !== "skipped").map(changeRow) + const skippedNew = [ + ...triage.filter((e) => e.docs_worthy === false), + ...agentSummary.filter((e) => e.action === "skipped"), + ].map(skippedRow) + + let oldChanges = [] + let oldSkipped = [] + if (mode === "update" && existingPr) { + const pr = await api(`/repos/${repo()}/pulls/${existingPr}`) + oldChanges = extractSectionRows(pr.body, "changes") + oldSkipped = extractSectionRows(pr.body, "skipped") + } + + const body = renderBody({ + date, + since, + through, + changesRows: mergeRows(oldChanges, changesNew), + skippedRows: mergeRows(oldSkipped, skippedNew), + verified, + draftReasons, + note: + mode === "conflict" && existingPr + ? `Continues from #${existingPr}, whose branch conflicted with \`main\` (its commits are preserved there).` + : "", + }) + + try { + await api(`/repos/${repo()}/labels`, { + method: "POST", + body: { name: "auto-docs", color: "1d76db", description: "Automated docs-sync PRs" }, + }) + } catch (err) { + if (err.status !== 422) throw err // 422 = label already exists + } + + let prNumber + let prUrl + if (mode === "update" && existingPr) { + const pr = await api(`/repos/${repo()}/pulls/${existingPr}`, { + method: "PATCH", + body: { title: `docs: auto-sync with merged PRs (through ${date})`, body }, + }) + prNumber = pr.number + prUrl = pr.html_url + await api(`/repos/${repo()}/issues/${prNumber}/comments`, { + method: "POST", + body: { + body: `(bot) Appended changes processed through \`${through}\`. Verification: **${verified ? "passing" : "failing"}**.${draft ? ` Draft because: ${draftReasons.join("; ")}.` : ""}`, + }, + }) + } else { + const pr = await api(`/repos/${repo()}/pulls`, { + method: "POST", + body: { + title: `docs: auto-sync with merged PRs (through ${date})`, + head: BRANCH, + base: "main", + body, + draft, + }, + }) + prNumber = pr.number + prUrl = pr.html_url + await api(`/repos/${repo()}/issues/${prNumber}/labels`, { method: "POST", body: { labels: ["auto-docs"] } }) + if (mode === "conflict" && existingPr) { + await api(`/repos/${repo()}/issues/${existingPr}/comments`, { + method: "POST", + body: { + body: `(bot) This branch conflicted with \`main\`, so the sync continues in ${prUrl}. Commits on this branch are preserved — please close this PR after the new one is reviewed.`, + }, + }) + } + } + + appendOutput("pr_url", prUrl) + appendSummary(`### docs-sync PR\n\n- ${prUrl}\n- changed files: ${changedFiles.length}\n- draft: ${draft}\n`) + console.log(`PR ${prNumber}: ${prUrl} (draft=${draft}, files=${changedFiles.length})`) +} + +const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href +if (isMain) { + main().catch((err) => { + console.error(err) + process.exit(1) + }) +} diff --git a/.github/docs-sync/watermark.mjs b/.github/docs-sync/watermark.mjs new file mode 100644 index 00000000000..cc4c9690168 --- /dev/null +++ b/.github/docs-sync/watermark.mjs @@ -0,0 +1,75 @@ +// kilocode_change - new file + +/** + * Resolves the docs-sync watermark: the timestamp of the newest source PR the + * bot has already processed. Derived from the bot's own PRs (marker in the PR + * body), so there is no external state to keep consistent. + * + * Priority: workflow_dispatch input `since` > latest open bot PR marker > + * last merged bot PR marker > 72h ago. Hard cap: never look back more than + * 14 days. + */ + +import { appendOutput, appendSummary, repo, searchIssues } from "./lib.mjs" + +const FALLBACK_HOURS = 72 +const CAP_DAYS = 14 +const MARKER = // + +function extractMarker(body) { + const m = (body ?? "").match(MARKER) + if (!m) return null + const d = new Date(m[1]) + return Number.isNaN(d.getTime()) ? null : d +} + +async function findWatermark() { + const r = repo() + for (const state of ["open", "merged"]) { + const query = `repo:${r} is:pr label:auto-docs sort:created-desc ${state === "open" ? "is:open" : "is:merged"}` + const prs = await searchIssues(query, { maxPages: 1 }) + for (const pr of prs) { + // Only trust markers on PRs authored by the bot itself: bodies are + // editable and the label can be applied by anyone with triage access. + if (pr.user?.login !== "github-actions[bot]") continue + const marker = extractMarker(pr.body) + if (marker) { + console.log(`watermark from ${state} PR #${pr.number}: ${marker.toISOString()}`) + return marker + } + } + } + return null +} + +const now = new Date() +let since + +const input = (process.env.INPUT_SINCE ?? "").trim() +if (input) { + since = new Date(input) + if (Number.isNaN(since.getTime())) { + throw new Error(`Invalid INPUT_SINCE: ${input}`) + } + console.log(`watermark from dispatch input: ${since.toISOString()}`) +} else { + since = + (await findWatermark()) ?? new Date(now.getTime() - FALLBACK_HOURS * 3600 * 1000) +} + +// A forged, edited, or malformed marker in the future would silently match +// nothing in the merged:>= search; clamp it loudly. +if (since > now) { + console.warn(`watermark ${since.toISOString()} is in the future, clamping to now`) + since = now +} + +const cap = new Date(now.getTime() - CAP_DAYS * 24 * 3600 * 1000) +if (since < cap) { + console.log(`watermark ${since.toISOString()} older than ${CAP_DAYS}d cap, clamping`) + since = cap +} + +appendOutput("since", since.toISOString()) +appendOutput("now", now.toISOString()) +appendSummary(`### docs-sync watermark\n\n- since: \`${since.toISOString()}\`\n- now: \`${now.toISOString()}\`\n`) diff --git a/.github/workflows/docs-sync.yml b/.github/workflows/docs-sync.yml new file mode 100644 index 00000000000..9eacf14e614 --- /dev/null +++ b/.github/workflows/docs-sync.yml @@ -0,0 +1,166 @@ +# kilocode_change - new file +name: docs-sync + +# Daily bot: collects PRs merged to Kilo-Org/cloud and Kilo-Org/kilocode, +# triages them for docs relevance, runs Kilo CLI headless to update +# packages/kilo-docs, and maintains one rolling PR for human review. +# +# Security posture: scheduled/manual only, checks out main, never executes +# code from PR branches. State is derived from the bot's own PRs (watermark +# marker in the PR body), so missed or failed runs self-heal on the next run. + +on: + schedule: + - cron: "0 7 * * *" # 07:00 UTC daily + workflow_dispatch: + inputs: + since: + description: "Override watermark (ISO date, e.g. 2026-07-20). Default: last processed-through marker, 72h fallback, 14d cap." + required: false + type: string + dry_run: + description: "Collect + triage only, no edits, no PR" + type: boolean + default: false + +permissions: + contents: write # push the rolling branch, create the auto-docs label + pull-requests: write # create/update the rolling PR + issues: write # comment on the rolling PR + +concurrency: + group: docs-sync + cancel-in-progress: false + +env: + TRIAGE_MODEL: ${{ vars.DOCS_SYNC_TRIAGE_MODEL || 'kilo/moonshotai/kimi-k3' }} + EDIT_MODEL: ${{ vars.DOCS_SYNC_EDIT_MODEL || 'kilo/moonshotai/kimi-k3' }} + +jobs: + sync: + if: github.repository == 'Kilo-Org/kilocode' + runs-on: blacksmith-4vcpu-ubuntu-2404 + timeout-minutes: 120 + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 # prepare-branch merges main into the rolling branch + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: "24" + package-manager-cache: false + + - name: Install Kilo CLI + run: | + npm install -g @kilocode/cli + kilo --version + + - name: Resolve watermark + id: wm + env: + GH_TOKEN: ${{ github.token }} + INPUT_SINCE: ${{ inputs.since }} + run: node .github/docs-sync/watermark.mjs + + - name: Collect merged PRs + id: collect + env: + GH_TOKEN: ${{ github.token }} + run: node .github/docs-sync/collect.mjs --since "${{ steps.wm.outputs.since }}" + + - name: Triage merged PRs (LLM, chunked) + id: triage + if: steps.collect.outputs.count != '0' + env: + KILO_API_KEY: ${{ secrets.KILO_API_KEY }} + run: node .github/docs-sync/triage.mjs + + - name: Filter docs-worthy PRs + id: worthy + if: steps.collect.outputs.count != '0' + run: | + node .github/docs-sync/filter-worthy.mjs \ + docs-sync-out/digest-full.json docs-sync-out/triage.json docs-sync-out/worthy.json + count=$(node -p "require('./docs-sync-out/worthy.json').length") + echo "count=$count" >> "$GITHUB_OUTPUT" + if [ "$count" = "0" ]; then + echo "No docs-worthy PRs in this window; skipping edit/verify/PR." + fi + + - name: Setup Bun + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + uses: ./.github/actions/setup-bun + + - name: Prepare rolling branch + id: prep + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + env: + GH_TOKEN: ${{ github.token }} + run: node .github/docs-sync/prepare-branch.mjs + + - name: Update docs (Kilo CLI, batched) + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + env: + KILO_API_KEY: ${{ secrets.KILO_API_KEY }} + run: node .github/docs-sync/edit.mjs + + - name: Verify docs build and tests + id: verify + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + continue-on-error: true + env: + NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.POSTHOG_API_KEY }} + run: | + set -o pipefail + { bun run --filter @kilocode/kilo-docs build && bun run --filter @kilocode/kilo-docs test; } 2>&1 | tee docs-sync-out/verify.log + + - name: Fix verify failures (one pass) + id: fix + if: steps.verify.outcome == 'failure' + continue-on-error: true + env: + KILO_API_KEY: ${{ secrets.KILO_API_KEY }} + NEXT_PUBLIC_POSTHOG_KEY: ${{ secrets.POSTHOG_API_KEY }} + run: | + set -o pipefail + kilo run "The docs build or tests failed. Read the attached docs-sync-out/verify.log and fix the packages/kilo-docs changes so they pass. Do not revert doc edits; fix them. Do not modify anything outside packages/kilo-docs." \ + -m "$EDIT_MODEL" --dir "$GITHUB_WORKSPACE" -f docs-sync-out/verify.log \ + | tee -a docs-sync-out/edit-log.txt + { bun run --filter @kilocode/kilo-docs build && bun run --filter @kilocode/kilo-docs test; } 2>&1 | tee docs-sync-out/verify2.log + + - name: Re-verify status + id: verified + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + env: + VERIFY_OUTCOME: ${{ steps.verify.outcome }} + FIX_OUTCOME: ${{ steps.fix.outcome }} + run: | + if [ "$VERIFY_OUTCOME" = "success" ] || [ "$FIX_OUTCOME" = "success" ]; then + echo "ok=true" >> "$GITHUB_OUTPUT" + else + echo "ok=false" >> "$GITHUB_OUTPUT" + fi + + - name: Upsert rolling PR + if: (steps.worthy.outputs.count || '0') != '0' && inputs.dry_run != true + env: + GH_TOKEN: ${{ github.token }} + PROCESSED_THROUGH: ${{ steps.wm.outputs.now }} + SINCE: ${{ steps.wm.outputs.since }} + BRANCH: ${{ steps.prep.outputs.branch }} + PREP_MODE: ${{ steps.prep.outputs.mode }} + PR_NUMBER: ${{ steps.prep.outputs.pr_number }} + VERIFIED: ${{ steps.verified.outputs.ok }} + run: node .github/docs-sync/upsert-pr.mjs + + - name: Upload run artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: docs-sync-out + path: docs-sync-out/ + retention-days: 14 + if-no-files-found: ignore diff --git a/.github/workflows/publish-jetbrains-bundled.yml b/.github/workflows/publish-jetbrains-bundled.yml new file mode 100644 index 00000000000..56b331680ef --- /dev/null +++ b/.github/workflows/publish-jetbrains-bundled.yml @@ -0,0 +1,321 @@ +# kilocode_change - new file +name: publish-jetbrains-bundled + +on: + workflow_dispatch: + inputs: + pr: + description: Merged JetBrains release PR number to bundle + required: true + type: string + merge_commit: + description: Merge commit SHA from the reviewed release PR + required: true + type: string + +concurrency: + group: publish-jetbrains-bundled-pr-${{ inputs.pr }} + cancel-in-progress: false + +permissions: + contents: read + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + validate: + if: github.repository == 'Kilo-Org/kilocode' + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + pull-requests: read + outputs: + version: ${{ steps.release.outputs.version }} + kind: ${{ steps.release.outputs.kind }} + tag: ${{ steps.release.outputs.tag }} + channel: ${{ steps.release.outputs.marketplace_channel }} + steps: + - name: Checkout trusted validation scripts + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: main + + - name: Setup Bun for validation + uses: ./.github/actions/setup-bun + + - name: Checkout merged release PR for validation + uses: actions/checkout@v6 + with: + fetch-depth: 0 + path: release + persist-credentials: false + ref: ${{ inputs.merge_commit }} + + - name: Validate release PR and tag + id: release + working-directory: release + run: bun ../script/jetbrains-release-validate.ts --pr "$PR_NUMBER" + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + PR_NUMBER: ${{ inputs.pr }} + + bundle: + needs: validate + if: github.repository == 'Kilo-Org/kilocode' + runs-on: blacksmith-8vcpu-ubuntu-2404 + permissions: + actions: read + contents: write + outputs: + version: ${{ needs.validate.outputs.version }} + kind: ${{ needs.validate.outputs.kind }} + steps: + - name: Checkout merged release PR metadata + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ inputs.merge_commit }} + + - name: Save reviewed release metadata + run: | + cp packages/kilo-jetbrains/CHANGELOG.md "$RUNNER_TEMP/jetbrains-CHANGELOG.md" + cp packages/kilo-jetbrains/gradle.properties "$RUNNER_TEMP/jetbrains-gradle.properties" + + - name: Checkout release tag + uses: actions/checkout@v6 + with: + fetch-depth: 0 + ref: ${{ needs.validate.outputs.tag }} + + - name: Restore reviewed release metadata + run: | + cp "$RUNNER_TEMP/jetbrains-CHANGELOG.md" packages/kilo-jetbrains/CHANGELOG.md + cp "$RUNNER_TEMP/jetbrains-gradle.properties" packages/kilo-jetbrains/gradle.properties + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Install dependencies + run: bun install + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y patchelf zip unzip + curl --fail --location \ + https://ziglang.org/download/0.14.0/zig-linux-x86_64-0.14.0.tar.xz \ + --output "$RUNNER_TEMP/zig.tar.xz" + echo "473ec26806133cf4d1918caf1a410f8403a13d979726a9045b421b685031a982 $RUNNER_TEMP/zig.tar.xz" | sha256sum --check --status + tar -xJf "$RUNNER_TEMP/zig.tar.xz" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/zig-linux-x86_64-0.14.0" >> "$GITHUB_PATH" + + - name: Validate signing secrets + run: | + missing=0 + for name in JETBRAINS_CERTIFICATE_CHAIN JETBRAINS_PRIVATE_KEY JETBRAINS_PRIVATE_KEY_PASSWORD; do + if [[ -z "${!name}" ]]; then + echo "Missing required secret: $name" >&2 + missing=1 + fi + done + exit "$missing" + env: + JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }} + JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }} + JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }} + + - name: Build signed bundled plugin + working-directory: packages/kilo-jetbrains + run: | + ./gradlew clean buildPlugin signPlugin verifyPluginSignature verifyPlugin \ + -Pproduction=true \ + -Pkilo.version="$VERSION" \ + -Pkilo.channel="$CHANNEL" \ + -Pkilo.cli.bundled=true + env: + GH_TOKEN: ${{ github.token }} + GITHUB_TOKEN: ${{ github.token }} + VERSION: ${{ needs.validate.outputs.version }} + CHANNEL: ${{ needs.validate.outputs.channel }} + JETBRAINS_CERTIFICATE_CHAIN: ${{ secrets.JETBRAINS_CERTIFICATE_CHAIN }} + JETBRAINS_PRIVATE_KEY: ${{ secrets.JETBRAINS_PRIVATE_KEY }} + JETBRAINS_PRIVATE_KEY_PASSWORD: ${{ secrets.JETBRAINS_PRIVATE_KEY_PASSWORD }} + + - name: Resolve bundled archive + id: archive + run: | + mapfile -t signed < <(compgen -G "packages/kilo-jetbrains/build/distributions/*-signed.zip") + if [[ "${#signed[@]}" -ne 1 ]]; then + echo "Expected exactly one signed bundled JetBrains plugin ZIP, found ${#signed[@]}." >&2 + printf '%s\n' "${signed[@]}" >&2 + exit 1 + fi + asset="kilo-code-${VERSION}-bundled.zip" + dest="packages/kilo-jetbrains/build/release/$asset" + mkdir -p "$(dirname "$dest")" + cp "${signed[0]}" "$dest" + echo "asset=$asset" >> "$GITHUB_OUTPUT" + echo "path=$dest" >> "$GITHUB_OUTPUT" + env: + VERSION: ${{ needs.validate.outputs.version }} + + - name: Upload bundled ZIP to GitHub Release + run: gh release upload "$TAG" "$ARCHIVE" --clobber --repo "$GITHUB_REPOSITORY" + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.validate.outputs.tag }} + ARCHIVE: ${{ steps.archive.outputs.path }} + + - name: Resolve bundled asset URL + id: asset + run: | + url="$(gh release view "$TAG" --json assets --jq '.assets[] | select(.name == env.ASSET) | .url' --repo "$GITHUB_REPOSITORY")" + if [[ -z "$url" ]]; then + echo "Could not resolve GitHub Release URL for $ASSET" >&2 + exit 1 + fi + echo "url=$url" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.validate.outputs.tag }} + ASSET: ${{ steps.archive.outputs.asset }} + + - name: Generate stable plugin repository XML + if: needs.validate.outputs.kind == 'stable' + run: | + mkdir -p pages/jetbrains + python3 <<'PY' + import html + import io + import os + import zipfile + import xml.etree.ElementTree as ET + + archive = os.environ["ARCHIVE"] + asset = os.environ["ASSET_URL"] + version = os.environ["VERSION"] + + def plugin_xml(path): + with zipfile.ZipFile(path) as zip: + for name in zip.namelist(): + if name.endswith("META-INF/plugin.xml"): + return zip.read(name) + for name in zip.namelist(): + if not name.endswith(".jar"): + continue + with zipfile.ZipFile(io.BytesIO(zip.read(name))) as jar: + for item in jar.namelist(): + if item.endswith("META-INF/plugin.xml"): + return jar.read(item) + raise SystemExit("bundled plugin ZIP did not contain META-INF/plugin.xml") + + root = ET.fromstring(plugin_xml(archive)) + + def text(name, default=""): + item = root.find(name) + return item.text.strip() if item is not None and item.text else default + + def cdata(value): + return "", "]]]]>") + "]]>" + + plugin = text("id", "ai.kilocode.jetbrains") + name = text("name", "Kilo Code") + vendor = text("vendor", "Kilo Code") + desc = text("description") + notes = text("change-notes") + idea = root.find("idea-version") + attrs = "" + if idea is not None: + since = idea.attrib.get("since-build") + until = idea.attrib.get("until-build") + if since: + attrs += f' since-build="{html.escape(since)}"' + if until: + attrs += f' until-build="{html.escape(until)}"' + + xml = [ + '', + '', + f' ', + f' {html.escape(name)}', + f' {html.escape(vendor)}', + f' ', + ] + if desc: + xml.append(f' {cdata(desc)}') + if notes: + xml.append(f' {cdata(notes)}') + xml.extend([' ', '', '']) + with open("pages/jetbrains/updatePlugins.xml", "w", encoding="utf-8") as file: + file.write("\n".join(xml)) + PY + env: + ARCHIVE: ${{ steps.archive.outputs.path }} + ASSET_URL: ${{ steps.asset.outputs.url }} + VERSION: ${{ needs.validate.outputs.version }} + + - name: Upload stable Pages source + if: needs.validate.outputs.kind == 'stable' + uses: actions/upload-artifact@v4 + with: + name: jetbrains-pages-${{ needs.validate.outputs.version }} + path: pages + if-no-files-found: error + + - name: Upload workflow artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: kilo-jetbrains-bundled-${{ needs.validate.outputs.version }} + path: | + packages/kilo-jetbrains/build/release/*.zip + pages/jetbrains/updatePlugins.xml + if-no-files-found: ignore + + pages: + needs: bundle + if: needs.bundle.outputs.kind == 'stable' + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + actions: read + id-token: write + pages: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Download stable Pages source + uses: actions/download-artifact@v4 + with: + name: jetbrains-pages-${{ needs.bundle.outputs.version }} + path: pages + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: pages + + - name: Deploy Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish-jetbrains.yml b/.github/workflows/publish-jetbrains.yml index ba43fb4124a..cb976193acc 100644 --- a/.github/workflows/publish-jetbrains.yml +++ b/.github/workflows/publish-jetbrains.yml @@ -23,6 +23,7 @@ concurrency: cancel-in-progress: false permissions: + actions: write contents: write pull-requests: read @@ -199,6 +200,19 @@ jobs: ARCHIVE: ${{ steps.archive.outputs.path }} NOTES: packages/kilo-jetbrains/build/release-notes.md + - name: Dispatch bundled GitHub release build + continue-on-error: true + run: | + gh workflow run publish-jetbrains-bundled.yml \ + --repo "$GITHUB_REPOSITORY" \ + --ref main \ + -f pr="$PR_NUMBER" \ + -f merge_commit="$MERGE_COMMIT" + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr }} + MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha || inputs.merge_commit }} + - name: Upload workflow artifact if: always() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1575f61ab58..2e02e5b705e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -224,6 +224,8 @@ jobs: - name: Run HttpApi exerciser gates run: bun turbo test:httpapi --filter='@kilocode/cli' + env: + KILO_EXPERIMENTAL_DISABLE_FILEWATCHER: "true" # kilocode_change - same rationale as the CLI unit step: a watcher per scenario instance is too heavy for the exerciser # kilocode_change end # kilocode_change start diff --git a/.kilo/skills/kilocode-merge-minimizer/SKILL.md b/.kilo/skills/kilocode-merge-minimizer/SKILL.md index 9c4798de73b..1250e6b1524 100644 --- a/.kilo/skills/kilocode-merge-minimizer/SKILL.md +++ b/.kilo/skills/kilocode-merge-minimizer/SKILL.md @@ -97,10 +97,10 @@ registerKiloFeature(app) After editing shared files or marker comments, run: ```bash -bun run script/check-opencode-annotations.ts +bun run script/check-opencode-annotations.ts --worktree ``` -If the PR uses a non-default comparison base, pass the correct base ref: +If checking committed PR changes against a non-default comparison base, pass the correct base ref without `--worktree`: ```bash bun run script/check-opencode-annotations.ts --base diff --git a/.opencode-version b/.opencode-version index 67afc371e2a..f0257c2afb2 100644 --- a/.opencode-version +++ b/.opencode-version @@ -1 +1 @@ -v1.17.4 +v1.17.5 diff --git a/AGENTS.md b/AGENTS.md index 5382092094e..54a75059e76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Kilo CLI is an open source AI coding agent that generates code from natural lang - **Knip** (unused exports): `bun run knip` from `packages/kilo-vscode/`. CI runs this — all exported types/functions must be imported somewhere. Remove or unexport unused exports before pushing. - **Source links**: After adding or changing URLs in `packages/kilo-vscode/`, `packages/kilo-vscode/webview-ui/`, or `packages/opencode/src/`, run `bun run script/extract-source-links.ts` from the repo root and commit the updated `packages/kilo-docs/source-links.md`. CI runs this check — the build fails if the file is stale. - **kilocode_change check**: `bun run check-kilocode-change` from `packages/kilo-vscode/`. CI runs this — `kilocode_change` is a marker for upstream merge conflicts and must not appear in `packages/kilo-vscode/` or `packages/kilo-ui/` (these are entirely Kilo Code additions). Remove the markers before pushing. -- **opencode annotation check**: `bun run script/check-opencode-annotations.ts` from repo root. CI runs this on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name. +- **opencode annotation check**: `bun run script/check-opencode-annotations.ts --worktree` from repo root when verifying local agent changes. CI runs `bun run script/check-opencode-annotations.ts` on PRs touching `packages/opencode/` — every Kilo-specific change in shared opencode files must be annotated with `kilocode_change` markers. Exempt paths (no markers needed): `packages/opencode/src/kilocode/`, `packages/opencode/test/kilocode/`, and any path containing `kilocode` in the name. - **Effect facade ratchet**: Do not add runtime-backed Promise facades to shared `packages/opencode/src` Effect services; use service dependencies, `AppRuntime`, or Kilo-owned boundaries. Run `bun run script/check-opencode-promise-facades.ts` when touching service adapters. - **workflow allowlist**: `bun run script/check-workflows.ts` from repo root. CI runs this as part of the annotations workflow — any `.yml` / `.yaml` file added to or removed from `.github/workflows/` must be reflected in the hardcoded list in `script/check-workflows.ts`. Prevents upstream-merged workflows from silently starting to run in our CI. - **Backend/SDK programmatic testing**: see [TESTING.md](./TESTING.md) for spawning the local main-branch backend (`bun dev serve`) and driving it via `curl` — use this instead of `kilo serve` (prod binary) when testing backend fixes. @@ -36,7 +36,7 @@ Before saying an implementation is ready, run the smallest relevant checks that | VS Code extension | From `packages/kilo-vscode/`: `bun run typecheck`, `bun run lint`, `bun run test:unit` or `bun run test` | | Extension build/package | From `packages/kilo-vscode/`: `bun run compile` or `bun run package` when touching build, packaging, SDK, or webview integration paths | | JetBrains plugin | From `packages/kilo-jetbrains/`: `./gradlew typecheck`, `./gradlew test`. Requires Java 21; do not run `java -version` as a routine preflight. Check Java only after a Java-version or missing-Java failure. | -| CI-only guards | Run affected guards documented above, such as `bun run knip`, `bun run check-kilocode-change`, `bun run script/check-opencode-annotations.ts`, or source link extraction | +| CI/local guards | Run affected guards documented above, such as `bun run knip`, `bun run check-kilocode-change`, `bun run script/check-opencode-annotations.ts --worktree`, or source link extraction | Never run root `bun test`; the root script prints `do not run tests from root` and exits with code 1. Use package-level tests instead. diff --git a/bun.lock b/bun.lock index ac91ac4f1a1..4dea19d92c9 100644 --- a/bun.lock +++ b/bun.lock @@ -32,7 +32,7 @@ }, "packages/core": { "name": "@opencode-ai/core", - "version": "7.4.13", + "version": "7.4.16", "bin": { "opencode": "./bin/opencode", }, @@ -56,7 +56,7 @@ "@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/togetherai": "2.0.41", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.92", + "@ai-sdk/xai": "3.0.102", "@aws-sdk/credential-providers": "3.1057.0", "@effect/opentelemetry": "catalog:", "@effect/platform-node": "catalog:", @@ -127,7 +127,7 @@ }, "packages/effect-drizzle-sqlite": { "name": "@opencode-ai/effect-drizzle-sqlite", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "drizzle-orm": "catalog:", "effect": "catalog:", @@ -141,7 +141,7 @@ }, "packages/effect-sqlite-node": { "name": "@opencode-ai/effect-sqlite-node", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "effect": "catalog:", }, @@ -153,7 +153,7 @@ }, "packages/http-recorder": { "name": "@opencode-ai/http-recorder", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@effect/platform-node": "4.0.0-beta.74", "@effect/platform-node-shared": "4.0.0-beta.74", @@ -174,7 +174,7 @@ }, "packages/kilo-console": { "name": "@kilocode/kilo-console", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@kilocode/kilo-indexing": "workspace:*", "@kilocode/kilo-web-ui": "workspace:*", @@ -197,7 +197,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -227,7 +227,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@ai-sdk/alibaba": "1.0.17", "@ai-sdk/anthropic": "3.0.71", @@ -263,7 +263,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.4.13", + "version": "7.4.16", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -273,7 +273,7 @@ }, "packages/kilo-indexing": { "name": "@kilocode/kilo-indexing", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@aws-sdk/client-bedrock-runtime": "3.1005.0", "@aws-sdk/credential-provider-ini": "3.972.31", @@ -305,11 +305,11 @@ }, "packages/kilo-jetbrains": { "name": "@kilocode/kilo-jetbrains", - "version": "7.4.11", + "version": "7.4.15", }, "packages/kilo-memory": { "name": "@kilocode/kilo-memory", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "effect": "catalog:", "zod": "catalog:", @@ -323,7 +323,7 @@ }, "packages/kilo-sandbox": { "name": "@kilocode/sandbox", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@anthropic-ai/sandbox-runtime": "catalog:", "effect": "catalog:", @@ -338,7 +338,7 @@ }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "posthog-node": "4.4.0", @@ -352,7 +352,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "0.13.11", @@ -390,7 +390,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-gateway": "workspace:*", @@ -459,7 +459,7 @@ }, "packages/kilo-web-ui": { "name": "@kilocode/kilo-web-ui", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@kilocode/kilo-ui": "workspace:*", "@kobalte/core": "catalog:", @@ -476,7 +476,7 @@ }, "packages/llm": { "name": "@opencode-ai/llm", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", @@ -494,7 +494,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.4.13", + "version": "7.4.16", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -522,7 +522,7 @@ "@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/togetherai": "2.0.41", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.92", + "@ai-sdk/xai": "3.0.102", "@aws-sdk/credential-providers": "3.1057.0", "@clack/prompts": "1.0.0-alpha.1", "@effect/opentelemetry": "catalog:", @@ -661,7 +661,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@kilocode/sdk": "workspace:*", "effect": "catalog:", @@ -689,7 +689,7 @@ }, "packages/plugin-atomic-chat": { "name": "@kilocode/plugin-atomic-chat", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@kilocode/plugin": "workspace:*", }, @@ -703,7 +703,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "semver": "^7.6.3", }, @@ -714,7 +714,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "cross-spawn": "catalog:", }, @@ -729,7 +729,7 @@ }, "packages/server": { "name": "@opencode-ai/server", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@opencode-ai/core": "workspace:*", "drizzle-orm": "catalog:", @@ -743,7 +743,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.4.13", + "version": "7.4.16", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -766,7 +766,7 @@ }, "packages/tui": { "name": "@opencode-ai/tui", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@kilocode/plugin": "workspace:*", "@kilocode/sdk": "workspace:*", @@ -793,7 +793,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.4.13", + "version": "7.4.16", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", @@ -852,8 +852,9 @@ ], "patchedDependencies": { "mammoth@1.12.0": "patches/mammoth@1.12.0.patch", - "@ai-sdk/xai@3.0.92": "patches/@ai-sdk%2Fxai@3.0.92.patch", "virtua@0.49.1": "patches/virtua@0.49.1.patch", + "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", + "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", "pacote@21.5.1": "patches/pacote@21.5.1.patch", @@ -1006,7 +1007,7 @@ "@ai-sdk/vercel": ["@ai-sdk/vercel@2.0.39", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.37", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8eu3ljJpkCTP4ppcyYB+NcBrkcBoSOFthCSgk5VnjaxnDaOJFaxnPwfddM7wx3RwMk2CiK1O61Px/LlqNc7QkQ=="], - "@ai-sdk/xai": ["@ai-sdk/xai@3.0.92", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yqRMdEVfSkZCXs5mqeKfYOuQElo93igwz/4zWbYkhZWAW7tDh+5uqRef6MtQVYF/bTC1EsdbeO9DtsOtAZMOLA=="], + "@ai-sdk/xai": ["@ai-sdk/xai@3.0.102", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.56", "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NeQyOR7OCqDMgaLS4uNX/ep/HrwUzzFYLzXQSRoqLy2jsnqxAJhsgltRwAwf+ADjyPBIAKEOestWnIQA+LrLrQ=="], "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], @@ -4822,9 +4823,11 @@ "@ai-sdk/vercel/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.21", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MtFUYI1/8mgDvRmaBDjbLJPFFrMG777AvSgyIFQtZHIMzm88R/12vYBBpnk7pfiWLFE1DSZzY4WDYzGbKAcmiw=="], - "@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + "@ai-sdk/xai/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.56", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@ai-sdk/provider-utils": "4.0.35" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cQrN6OUn/jvsY3OdsU6Wn+ss7vp1iwIcakZKSlSRMnYqShBfyT7Qht+eqmgxs7w9ttrw6FAG6o11AiBs+iEsTA=="], - "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-ZPtVYt5QIJzOta1kdUiDuCx4HhFkvNPv/rvmZ2b1iXwybYjJsCnNYR4PAw4kW7rgVfDARvHXcU64efWuqNp6bw=="], + + "@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.13", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjYld/2KGPLt78kpqbya+fD4LYS7BqVQJyUjE3qAHrYB0FR2Q90BaWEVIBZaguTWXf/A8L6uG1zO1v9TxVlGWg=="], "@antfu/install-pkg/package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], @@ -5332,6 +5335,8 @@ "ai-gateway-provider/@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@4.0.112", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.71", "@ai-sdk/google": "3.0.64", "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cSfHCkM+9ZrFtQWIN1WlV93JPD+isGSdFxKj7u1L9m2aLVZajlXdcE41GL9hMt7ld7bZYE4NnZ+4VLxBAHE+Eg=="], + "ai-gateway-provider/@ai-sdk/xai": ["@ai-sdk/xai@3.0.92", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.48", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yqRMdEVfSkZCXs5mqeKfYOuQElo93igwz/4zWbYkhZWAW7tDh+5uqRef6MtQVYF/bTC1EsdbeO9DtsOtAZMOLA=="], + "archiver-utils/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], @@ -6026,6 +6031,10 @@ "ai-gateway-provider/@ai-sdk/google-vertex/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="], + "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "archiver-utils/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "archiver-utils/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -6360,6 +6369,8 @@ "@vscode/test-cli/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "ai-gateway-provider/@ai-sdk/xai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "archiver-utils/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA=="], diff --git a/nix/hashes.json b/nix/hashes.json index 320f977a491..c10bd672c36 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-611oft4OE2TByjNpiGBm8ovkqkrTPZh7gWW90CZ2B6U=", - "aarch64-linux": "sha256-NNFZV21HTDNLwdjdm9XYqS5EqdyG9+a6cxJn8odwKaQ=", - "aarch64-darwin": "sha256-jYnaqpcU+wyncxM4o5vsXchBBSLMeo8FOwS/fC7G7I8=", - "x86_64-darwin": "sha256-vGXN1BUdFs8w/U7Y3JrMkfmJosyNdBF5orJXP0LWuMM=" + "x86_64-linux": "sha256-UHxMHmx17Jex0yXgbpXCvIORubs9cFMsIXGacvs9+gA=", + "aarch64-linux": "sha256-tTnW84VaNEbfo46H24ETKZgFumZMwu6pgNDlY8KYkqo=", + "aarch64-darwin": "sha256-tBi4D1tfACA4ogYMdyjUK8sDb370rAGE2q8baeJjpdA=", + "x86_64-darwin": "sha256-MPIai9M+EQps81qp7RBmTsW/qPjcrWd7GGJQyeNCz/U=" } } diff --git a/package.json b/package.json index b92784ea002..543649f5030 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "type": "module", "packageManager": "bun@1.3.14", "scripts": { - "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", - "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", + "dev": "bun run --cwd packages/opencode --conditions=node src/index.ts", "dev:local": "bun run packages/opencode/script/dev-local.ts", + "dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev", "dev:storybook": "bun --cwd packages/storybook storybook", "lint": "oxlint", "typecheck": "bun turbo typecheck", @@ -153,17 +153,18 @@ "@opentui/keymap": "catalog:" }, "patchedDependencies": { - "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", "@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch", "@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "virtua@0.49.1": "patches/virtua@0.49.1.patch", "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", - "@ai-sdk/xai@3.0.92": "patches/@ai-sdk%2Fxai@3.0.92.patch", + "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", + "@ff-labs/fff-bun@0.9.4": "patches/@ff-labs%2Ffff-bun@0.9.4.patch", + "@ai-sdk/xai@3.0.102": "patches/@ai-sdk%2Fxai@3.0.102.patch", "pacote@21.5.1": "patches/pacote@21.5.1.patch", "mammoth@1.12.0": "patches/mammoth@1.12.0.patch" }, - "version": "7.4.13", + "version": "7.4.16", "peerDependencies": {} } diff --git a/packages/core/migration/20260127222353_familiar_lady_ursula/migration.sql b/packages/core/migration/20260127222353_familiar_lady_ursula/migration.sql deleted file mode 100644 index 775c1a1173d..00000000000 --- a/packages/core/migration/20260127222353_familiar_lady_ursula/migration.sql +++ /dev/null @@ -1,90 +0,0 @@ -CREATE TABLE `project` ( - `id` text PRIMARY KEY, - `worktree` text NOT NULL, - `vcs` text, - `name` text, - `icon_url` text, - `icon_color` text, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - `time_initialized` integer, - `sandboxes` text NOT NULL -); ---> statement-breakpoint -CREATE TABLE `message` ( - `id` text PRIMARY KEY, - `session_id` text NOT NULL, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - `data` text NOT NULL, - CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -CREATE TABLE `part` ( - `id` text PRIMARY KEY, - `message_id` text NOT NULL, - `session_id` text NOT NULL, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - `data` text NOT NULL, - CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -CREATE TABLE `permission` ( - `project_id` text PRIMARY KEY, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - `data` text NOT NULL, - CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -CREATE TABLE `session` ( - `id` text PRIMARY KEY, - `project_id` text NOT NULL, - `parent_id` text, - `slug` text NOT NULL, - `directory` text NOT NULL, - `title` text NOT NULL, - `version` text NOT NULL, - `share_url` text, - `summary_additions` integer, - `summary_deletions` integer, - `summary_files` integer, - `summary_diffs` text, - `revert` text, - `permission` text, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - `time_compacting` integer, - `time_archived` integer, - CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -CREATE TABLE `todo` ( - `session_id` text NOT NULL, - `content` text NOT NULL, - `status` text NOT NULL, - `priority` text NOT NULL, - `position` integer NOT NULL, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `position`), - CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -CREATE TABLE `session_share` ( - `session_id` text PRIMARY KEY, - `id` text NOT NULL, - `secret` text NOT NULL, - `url` text NOT NULL, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint -CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint -CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint -CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint -CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint -CREATE INDEX `todo_session_idx` ON `todo` (`session_id`); \ No newline at end of file diff --git a/packages/core/migration/20260127222353_familiar_lady_ursula/snapshot.json b/packages/core/migration/20260127222353_familiar_lady_ursula/snapshot.json deleted file mode 100644 index ff76ee209a1..00000000000 --- a/packages/core/migration/20260127222353_familiar_lady_ursula/snapshot.json +++ /dev/null @@ -1,796 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "068758ed-a97a-46f6-8a59-6c639ae7c20c", - "prevIds": ["00000000-0000-0000-0000-000000000000"], - "ddl": [ - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260211171708_add_project_commands/migration.sql b/packages/core/migration/20260211171708_add_project_commands/migration.sql deleted file mode 100644 index b63f147a0b2..00000000000 --- a/packages/core/migration/20260211171708_add_project_commands/migration.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `project` ADD `commands` text; \ No newline at end of file diff --git a/packages/core/migration/20260211171708_add_project_commands/snapshot.json b/packages/core/migration/20260211171708_add_project_commands/snapshot.json deleted file mode 100644 index 1182cc32de9..00000000000 --- a/packages/core/migration/20260211171708_add_project_commands/snapshot.json +++ /dev/null @@ -1,806 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "8bc2d11d-97fa-4ba8-8bfa-6c5956c49aeb", - "prevIds": ["068758ed-a97a-46f6-8a59-6c639ae7c20c"], - "ddl": [ - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260213144116_wakeful_the_professor/migration.sql b/packages/core/migration/20260213144116_wakeful_the_professor/migration.sql deleted file mode 100644 index 3085fe280f3..00000000000 --- a/packages/core/migration/20260213144116_wakeful_the_professor/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE `control_account` ( - `email` text NOT NULL, - `url` text NOT NULL, - `access_token` text NOT NULL, - `refresh_token` text NOT NULL, - `token_expiry` integer, - `active` integer NOT NULL, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - CONSTRAINT `control_account_pk` PRIMARY KEY(`email`, `url`) -); diff --git a/packages/core/migration/20260213144116_wakeful_the_professor/snapshot.json b/packages/core/migration/20260213144116_wakeful_the_professor/snapshot.json deleted file mode 100644 index 05c00a10cf3..00000000000 --- a/packages/core/migration/20260213144116_wakeful_the_professor/snapshot.json +++ /dev/null @@ -1,897 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "d2736e43-700f-4e9e-8151-9f2f0d967bc8", - "prevIds": ["8bc2d11d-97fa-4ba8-8bfa-6c5956c49aeb"], - "ddl": [ - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260225215848_workspace/migration.sql b/packages/core/migration/20260225215848_workspace/migration.sql deleted file mode 100644 index 5b1b4e5a479..00000000000 --- a/packages/core/migration/20260225215848_workspace/migration.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE `workspace` ( - `id` text PRIMARY KEY, - `branch` text, - `project_id` text NOT NULL, - `config` text NOT NULL, - CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE -); diff --git a/packages/core/migration/20260225215848_workspace/snapshot.json b/packages/core/migration/20260225215848_workspace/snapshot.json deleted file mode 100644 index a75001d58f4..00000000000 --- a/packages/core/migration/20260225215848_workspace/snapshot.json +++ /dev/null @@ -1,959 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40", - "prevIds": ["d2736e43-700f-4e9e-8151-9f2f0d967bc8"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "config", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260227213759_add_session_workspace_id/migration.sql b/packages/core/migration/20260227213759_add_session_workspace_id/migration.sql deleted file mode 100644 index f5488af2180..00000000000 --- a/packages/core/migration/20260227213759_add_session_workspace_id/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `session` ADD `workspace_id` text;--> statement-breakpoint -CREATE INDEX `session_workspace_idx` ON `session` (`workspace_id`); \ No newline at end of file diff --git a/packages/core/migration/20260227213759_add_session_workspace_id/snapshot.json b/packages/core/migration/20260227213759_add_session_workspace_id/snapshot.json deleted file mode 100644 index 8cd94d00527..00000000000 --- a/packages/core/migration/20260227213759_add_session_workspace_id/snapshot.json +++ /dev/null @@ -1,983 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "572fb732-56f4-4b1e-b981-77152c9980dd", - "prevIds": ["1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "config", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260228203230_blue_harpoon/migration.sql b/packages/core/migration/20260228203230_blue_harpoon/migration.sql deleted file mode 100644 index 85be58c88df..00000000000 --- a/packages/core/migration/20260228203230_blue_harpoon/migration.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TABLE `account` ( - `id` text PRIMARY KEY, - `email` text NOT NULL, - `url` text NOT NULL, - `access_token` text NOT NULL, - `refresh_token` text NOT NULL, - `token_expiry` integer, - `selected_org_id` text, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL -); ---> statement-breakpoint -CREATE TABLE `account_state` ( - `id` integer PRIMARY KEY NOT NULL, - `active_account_id` text, - FOREIGN KEY (`active_account_id`) REFERENCES `account`(`id`) ON UPDATE no action ON DELETE set null -); diff --git a/packages/core/migration/20260228203230_blue_harpoon/snapshot.json b/packages/core/migration/20260228203230_blue_harpoon/snapshot.json deleted file mode 100644 index 80d9451bae1..00000000000 --- a/packages/core/migration/20260228203230_blue_harpoon/snapshot.json +++ /dev/null @@ -1,1102 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "325559b7-104f-4d2a-a02c-934cfad7cfcc", - "prevIds": ["1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40"], - "ddl": [ - { - "name": "account", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "selected_org_id", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "config", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260303231226_add_workspace_fields/migration.sql b/packages/core/migration/20260303231226_add_workspace_fields/migration.sql deleted file mode 100644 index 185de59133b..00000000000 --- a/packages/core/migration/20260303231226_add_workspace_fields/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE `workspace` ADD `type` text NOT NULL;--> statement-breakpoint -ALTER TABLE `workspace` ADD `name` text;--> statement-breakpoint -ALTER TABLE `workspace` ADD `directory` text;--> statement-breakpoint -ALTER TABLE `workspace` ADD `extra` text;--> statement-breakpoint -ALTER TABLE `workspace` DROP COLUMN `config`; \ No newline at end of file diff --git a/packages/core/migration/20260303231226_add_workspace_fields/snapshot.json b/packages/core/migration/20260303231226_add_workspace_fields/snapshot.json deleted file mode 100644 index 4fe320a2cc3..00000000000 --- a/packages/core/migration/20260303231226_add_workspace_fields/snapshot.json +++ /dev/null @@ -1,1013 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "4ec9de62-88a7-4bec-91cc-0a759e84db21", - "prevIds": ["572fb732-56f4-4b1e-b981-77152c9980dd"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260309230000_move_org_to_state/migration.sql b/packages/core/migration/20260309230000_move_org_to_state/migration.sql deleted file mode 100644 index 4d1c7bccd00..00000000000 --- a/packages/core/migration/20260309230000_move_org_to_state/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE `account_state` ADD `active_org_id` text;--> statement-breakpoint -UPDATE `account_state` SET `active_org_id` = (SELECT `selected_org_id` FROM `account` WHERE `account`.`id` = `account_state`.`active_account_id`);--> statement-breakpoint -ALTER TABLE `account` DROP COLUMN `selected_org_id`; diff --git a/packages/core/migration/20260309230000_move_org_to_state/snapshot.json b/packages/core/migration/20260309230000_move_org_to_state/snapshot.json deleted file mode 100644 index 488ecefffb7..00000000000 --- a/packages/core/migration/20260309230000_move_org_to_state/snapshot.json +++ /dev/null @@ -1,1156 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "fb311f30-9948-4131-b15c-7d308478a878", - "prevIds": ["325559b7-104f-4d2a-a02c-934cfad7cfcc", "4ec9de62-88a7-4bec-91cc-0a759e84db21"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260312043431_session_message_cursor/migration.sql b/packages/core/migration/20260312043431_session_message_cursor/migration.sql deleted file mode 100644 index e2bd08137c4..00000000000 --- a/packages/core/migration/20260312043431_session_message_cursor/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS `message_session_idx`;--> statement-breakpoint -DROP INDEX IF EXISTS `part_message_idx`;--> statement-breakpoint -CREATE INDEX `message_session_time_created_id_idx` ON `message` (`session_id`,`time_created`,`id`);--> statement-breakpoint -CREATE INDEX `part_message_id_id_idx` ON `part` (`message_id`,`id`); \ No newline at end of file diff --git a/packages/core/migration/20260312043431_session_message_cursor/snapshot.json b/packages/core/migration/20260312043431_session_message_cursor/snapshot.json deleted file mode 100644 index 48958804ab0..00000000000 --- a/packages/core/migration/20260312043431_session_message_cursor/snapshot.json +++ /dev/null @@ -1,1168 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "37e1554d-af4c-43f2-aa7c-307fb49a315e", - "prevIds": ["fb311f30-9948-4131-b15c-7d308478a878"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260323234822_events/migration.sql b/packages/core/migration/20260323234822_events/migration.sql deleted file mode 100644 index b0fe7e4e6be..00000000000 --- a/packages/core/migration/20260323234822_events/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE `event_sequence` ( - `aggregate_id` text PRIMARY KEY, - `seq` integer NOT NULL -); ---> statement-breakpoint -CREATE TABLE `event` ( - `id` text PRIMARY KEY, - `aggregate_id` text NOT NULL, - `seq` integer NOT NULL, - `type` text NOT NULL, - `data` text NOT NULL, - CONSTRAINT `fk_event_aggregate_id_event_sequence_aggregate_id_fk` FOREIGN KEY (`aggregate_id`) REFERENCES `event_sequence`(`aggregate_id`) ON DELETE CASCADE -); diff --git a/packages/core/migration/20260323234822_events/snapshot.json b/packages/core/migration/20260323234822_events/snapshot.json deleted file mode 100644 index 07519aab71a..00000000000 --- a/packages/core/migration/20260323234822_events/snapshot.json +++ /dev/null @@ -1,1271 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "f13dfa58-7fb4-47a2-8f6b-dc70258e14ed", - "prevIds": ["37e1554d-af4c-43f2-aa7c-307fb49a315e"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260410174513_workspace-name/migration.sql b/packages/core/migration/20260410174513_workspace-name/migration.sql deleted file mode 100644 index 2a27248e414..00000000000 --- a/packages/core/migration/20260410174513_workspace-name/migration.sql +++ /dev/null @@ -1,16 +0,0 @@ -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_workspace` ( - `id` text PRIMARY KEY, - `type` text NOT NULL, - `name` text DEFAULT '' NOT NULL, - `branch` text, - `directory` text, - `extra` text, - `project_id` text NOT NULL, - CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -INSERT INTO `__new_workspace`(`id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id`) SELECT `id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id` FROM `workspace`;--> statement-breakpoint -DROP TABLE `workspace`;--> statement-breakpoint -ALTER TABLE `__new_workspace` RENAME TO `workspace`;--> statement-breakpoint -PRAGMA foreign_keys=ON; \ No newline at end of file diff --git a/packages/core/migration/20260410174513_workspace-name/snapshot.json b/packages/core/migration/20260410174513_workspace-name/snapshot.json deleted file mode 100644 index 9adeeecbe89..00000000000 --- a/packages/core/migration/20260410174513_workspace-name/snapshot.json +++ /dev/null @@ -1,1271 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "b61476b8-3b92-49ae-9fa5-6eef586ed64b", - "prevIds": ["f13dfa58-7fb4-47a2-8f6b-dc70258e14ed"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260413175956_chief_energizer/migration.sql b/packages/core/migration/20260413175956_chief_energizer/migration.sql deleted file mode 100644 index e0c85895086..00000000000 --- a/packages/core/migration/20260413175956_chief_energizer/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE `session_entry` ( - `id` text PRIMARY KEY, - `session_id` text NOT NULL, - `type` text NOT NULL, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - `data` text NOT NULL, - CONSTRAINT `fk_session_entry_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -CREATE INDEX `session_entry_session_idx` ON `session_entry` (`session_id`);--> statement-breakpoint -CREATE INDEX `session_entry_session_type_idx` ON `session_entry` (`session_id`,`type`);--> statement-breakpoint -CREATE INDEX `session_entry_time_created_idx` ON `session_entry` (`time_created`); \ No newline at end of file diff --git a/packages/core/migration/20260413175956_chief_energizer/snapshot.json b/packages/core/migration/20260413175956_chief_energizer/snapshot.json deleted file mode 100644 index ac54a30af2d..00000000000 --- a/packages/core/migration/20260413175956_chief_energizer/snapshot.json +++ /dev/null @@ -1,1399 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "30b928c5-deef-472c-856d-b5b5064bf6d4", - "prevIds": ["b61476b8-3b92-49ae-9fa5-6eef586ed64b"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_entry", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_entry_session_id_session_id_fk", - "entityType": "fks", - "table": "session_entry" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_entry_pk", - "table": "session_entry", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_entry_session_idx", - "entityType": "indexes", - "table": "session_entry" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_entry_session_type_idx", - "entityType": "indexes", - "table": "session_entry" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_entry_time_created_idx", - "entityType": "indexes", - "table": "session_entry" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260423070820_add_icon_url_override/migration.sql b/packages/core/migration/20260423070820_add_icon_url_override/migration.sql deleted file mode 100644 index e28a1d4e989..00000000000 --- a/packages/core/migration/20260423070820_add_icon_url_override/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `project` ADD `icon_url_override` text; -UPDATE `project` SET `icon_url_override` = `icon_url` WHERE `icon_url` IS NOT NULL; diff --git a/packages/core/migration/20260423070820_add_icon_url_override/snapshot.json b/packages/core/migration/20260423070820_add_icon_url_override/snapshot.json deleted file mode 100644 index 06dae8e44b7..00000000000 --- a/packages/core/migration/20260423070820_add_icon_url_override/snapshot.json +++ /dev/null @@ -1,1409 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "66cbe0d7-def0-451b-b88a-7608513a9b44", - "prevIds": ["30b928c5-deef-472c-856d-b5b5064bf6d4"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_entry", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_entry" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_entry_session_id_session_id_fk", - "entityType": "fks", - "table": "session_entry" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_entry_pk", - "table": "session_entry", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_entry_session_idx", - "entityType": "indexes", - "table": "session_entry" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_entry_session_type_idx", - "entityType": "indexes", - "table": "session_entry" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_entry_time_created_idx", - "entityType": "indexes", - "table": "session_entry" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260427172553_slow_nightmare/migration.sql b/packages/core/migration/20260427172553_slow_nightmare/migration.sql deleted file mode 100644 index d5efe5f9e8b..00000000000 --- a/packages/core/migration/20260427172553_slow_nightmare/migration.sql +++ /dev/null @@ -1,17 +0,0 @@ -CREATE TABLE `session_message` ( - `id` text PRIMARY KEY, - `session_id` text NOT NULL, - `type` text NOT NULL, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - `data` text NOT NULL, - CONSTRAINT `fk_session_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -DROP INDEX IF EXISTS `session_entry_session_idx`;--> statement-breakpoint -DROP INDEX IF EXISTS `session_entry_session_type_idx`;--> statement-breakpoint -DROP INDEX IF EXISTS `session_entry_time_created_idx`;--> statement-breakpoint -CREATE INDEX `session_message_session_idx` ON `session_message` (`session_id`);--> statement-breakpoint -CREATE INDEX `session_message_session_type_idx` ON `session_message` (`session_id`,`type`);--> statement-breakpoint -CREATE INDEX `session_message_time_created_idx` ON `session_message` (`time_created`);--> statement-breakpoint -DROP TABLE `session_entry`; \ No newline at end of file diff --git a/packages/core/migration/20260427172553_slow_nightmare/snapshot.json b/packages/core/migration/20260427172553_slow_nightmare/snapshot.json deleted file mode 100644 index a237b4156ee..00000000000 --- a/packages/core/migration/20260427172553_slow_nightmare/snapshot.json +++ /dev/null @@ -1,1409 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "61f807f9-6398-4067-be05-804acc2561bc", - "prevIds": ["66cbe0d7-def0-451b-b88a-7608513a9b44"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260428004200_add_session_path/migration.sql b/packages/core/migration/20260428004200_add_session_path/migration.sql deleted file mode 100644 index e3ef6f99000..00000000000 --- a/packages/core/migration/20260428004200_add_session_path/migration.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `session` ADD `path` text; \ No newline at end of file diff --git a/packages/core/migration/20260428004200_add_session_path/snapshot.json b/packages/core/migration/20260428004200_add_session_path/snapshot.json deleted file mode 100644 index 740ba0e2546..00000000000 --- a/packages/core/migration/20260428004200_add_session_path/snapshot.json +++ /dev/null @@ -1,1419 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "aaa2ebeb-caa4-478d-8365-4fc595d16856", - "prevIds": ["61f807f9-6398-4067-be05-804acc2561bc"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260501142318_next_venus/migration.sql b/packages/core/migration/20260501142318_next_venus/migration.sql deleted file mode 100644 index e0ffe7823c4..00000000000 --- a/packages/core/migration/20260501142318_next_venus/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `session` ADD `agent` text;--> statement-breakpoint -ALTER TABLE `session` ADD `model` text; \ No newline at end of file diff --git a/packages/core/migration/20260501142318_next_venus/snapshot.json b/packages/core/migration/20260501142318_next_venus/snapshot.json deleted file mode 100644 index 1eb0cf0b07c..00000000000 --- a/packages/core/migration/20260501142318_next_venus/snapshot.json +++ /dev/null @@ -1,1439 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "2ec89846-dcf1-4977-ab5e-244ddc9e3d67", - "prevIds": ["aaa2ebeb-caa4-478d-8365-4fc595d16856"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260504145000_add_sync_owner/migration.sql b/packages/core/migration/20260504145000_add_sync_owner/migration.sql deleted file mode 100644 index 3bdf2b85e9c..00000000000 --- a/packages/core/migration/20260504145000_add_sync_owner/migration.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `event_sequence` ADD `owner_id` text; \ No newline at end of file diff --git a/packages/core/migration/20260504145000_add_sync_owner/snapshot.json b/packages/core/migration/20260504145000_add_sync_owner/snapshot.json deleted file mode 100644 index 7a0d10337d2..00000000000 --- a/packages/core/migration/20260504145000_add_sync_owner/snapshot.json +++ /dev/null @@ -1,1449 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "27114226-085b-421a-9a40-29b88747e29a", - "prevIds": ["2ec89846-dcf1-4977-ab5e-244ddc9e3d67"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260507164347_add_workspace_time/migration.sql b/packages/core/migration/20260507164347_add_workspace_time/migration.sql deleted file mode 100644 index c865526a88e..00000000000 --- a/packages/core/migration/20260507164347_add_workspace_time/migration.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `workspace` ADD `time_used` integer NOT NULL DEFAULT 0; diff --git a/packages/core/migration/20260507164347_add_workspace_time/snapshot.json b/packages/core/migration/20260507164347_add_workspace_time/snapshot.json deleted file mode 100644 index 57da763bb92..00000000000 --- a/packages/core/migration/20260507164347_add_workspace_time/snapshot.json +++ /dev/null @@ -1,1459 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "630a93f2-c6c6-4191-a351-868d8f3a05d4", - "prevIds": ["27114226-085b-421a-9a40-29b88747e29a"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260510033149_session_usage/migration.sql b/packages/core/migration/20260510033149_session_usage/migration.sql deleted file mode 100644 index 68e12aad09a..00000000000 --- a/packages/core/migration/20260510033149_session_usage/migration.sql +++ /dev/null @@ -1,6 +0,0 @@ -ALTER TABLE `session` ADD `cost` real DEFAULT 0 NOT NULL;--> statement-breakpoint -ALTER TABLE `session` ADD `tokens_input` integer DEFAULT 0 NOT NULL;--> statement-breakpoint -ALTER TABLE `session` ADD `tokens_output` integer DEFAULT 0 NOT NULL;--> statement-breakpoint -ALTER TABLE `session` ADD `tokens_reasoning` integer DEFAULT 0 NOT NULL;--> statement-breakpoint -ALTER TABLE `session` ADD `tokens_cache_read` integer DEFAULT 0 NOT NULL;--> statement-breakpoint -ALTER TABLE `session` ADD `tokens_cache_write` integer DEFAULT 0 NOT NULL; diff --git a/packages/core/migration/20260510033149_session_usage/snapshot.json b/packages/core/migration/20260510033149_session_usage/snapshot.json deleted file mode 100644 index ce5e56f48c4..00000000000 --- a/packages/core/migration/20260510033149_session_usage/snapshot.json +++ /dev/null @@ -1,1519 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "be5eae31-b7f8-4292-8827-c36a524abd1b", - "prevIds": ["630a93f2-c6c6-4191-a351-868d8f3a05d4"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260511000411_data_migration_state/migration.sql b/packages/core/migration/20260511000411_data_migration_state/migration.sql deleted file mode 100644 index ba36a7f078d..00000000000 --- a/packages/core/migration/20260511000411_data_migration_state/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ -CREATE TABLE `data_migration` ( - `name` text PRIMARY KEY, - `time_completed` integer NOT NULL -); diff --git a/packages/core/migration/20260511000411_data_migration_state/snapshot.json b/packages/core/migration/20260511000411_data_migration_state/snapshot.json deleted file mode 100644 index e84aa1a6a10..00000000000 --- a/packages/core/migration/20260511000411_data_migration_state/snapshot.json +++ /dev/null @@ -1,1490 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "fdfcccee-fb3a-481f-b801-b9835fa30d5d", - "prevIds": ["630a93f2-c6c6-4191-a351-868d8f3a05d4"], - "ddl": [ - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260511173437_session-metadata/migration.sql b/packages/core/migration/20260511173437_session-metadata/migration.sql deleted file mode 100644 index 1f8fcaf64a7..00000000000 --- a/packages/core/migration/20260511173437_session-metadata/migration.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `session` ADD `metadata` text; diff --git a/packages/core/migration/20260511173437_session-metadata/snapshot.json b/packages/core/migration/20260511173437_session-metadata/snapshot.json deleted file mode 100644 index 8c979997ca8..00000000000 --- a/packages/core/migration/20260511173437_session-metadata/snapshot.json +++ /dev/null @@ -1,1560 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "bf93c73b-5a48-4d63-9909-3c36a79b9788", - "prevIds": ["be5eae31-b7f8-4292-8827-c36a524abd1b", "fdfcccee-fb3a-481f-b801-b9835fa30d5d"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260601010001_normalize_storage_paths/migration.sql b/packages/core/migration/20260601010001_normalize_storage_paths/migration.sql deleted file mode 100644 index 7eede8fa2fa..00000000000 --- a/packages/core/migration/20260601010001_normalize_storage_paths/migration.sql +++ /dev/null @@ -1,7 +0,0 @@ -UPDATE project SET worktree = REPLACE(worktree, char(92), '/') WHERE worktree GLOB '[A-Za-z]:' || char(92) || '*' OR worktree LIKE char(92) || char(92) || '%'; ---> statement-breakpoint -UPDATE project SET sandboxes = REPLACE(sandboxes, char(92) || char(92), '/') WHERE instr(sandboxes, char(92)) > 0 AND (worktree GLOB '[A-Za-z]:*' OR worktree LIKE '//%'); ---> statement-breakpoint -UPDATE session SET directory = REPLACE(directory, char(92), '/') WHERE directory GLOB '[A-Za-z]:' || char(92) || '*' OR directory LIKE char(92) || char(92) || '%'; ---> statement-breakpoint -UPDATE session SET path = REPLACE(path, char(92), '/') WHERE path IS NOT NULL AND instr(path, char(92)) > 0 AND (directory GLOB '[A-Za-z]:*' OR directory LIKE '//%'); diff --git a/packages/core/migration/20260601010001_normalize_storage_paths/snapshot.json b/packages/core/migration/20260601010001_normalize_storage_paths/snapshot.json deleted file mode 100644 index 0f0faf7eee1..00000000000 --- a/packages/core/migration/20260601010001_normalize_storage_paths/snapshot.json +++ /dev/null @@ -1,1560 +0,0 @@ -{ - "id": "7f4866d3-a95b-4141-bb59-28e31c521605", - "prevIds": ["bf93c73b-5a48-4d63-9909-3c36a79b9788"], - "version": "7", - "dialect": "sqlite", - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["project_id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260601202201_amazing_prowler/migration.sql b/packages/core/migration/20260601202201_amazing_prowler/migration.sql deleted file mode 100644 index 92405490f61..00000000000 --- a/packages/core/migration/20260601202201_amazing_prowler/migration.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE `permission`; \ No newline at end of file diff --git a/packages/core/migration/20260601202201_amazing_prowler/snapshot.json b/packages/core/migration/20260601202201_amazing_prowler/snapshot.json deleted file mode 100644 index b506b5009d4..00000000000 --- a/packages/core/migration/20260601202201_amazing_prowler/snapshot.json +++ /dev/null @@ -1,1498 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "226375f1-a19f-4c7b-8aa2-ccc5513d3b0d", - "prevIds": ["bf93c73b-5a48-4d63-9909-3c36a79b9788"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260602002951_lowly_union_jack/migration.sql b/packages/core/migration/20260602002951_lowly_union_jack/migration.sql deleted file mode 100644 index aea79762f37..00000000000 --- a/packages/core/migration/20260602002951_lowly_union_jack/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE `permission` ( - `id` text PRIMARY KEY, - `project_id` text NOT NULL, - `action` text NOT NULL, - `resource` text NOT NULL, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -CREATE UNIQUE INDEX `permission_project_action_resource_idx` ON `permission` (`project_id`,`action`,`resource`); \ No newline at end of file diff --git a/packages/core/migration/20260602002951_lowly_union_jack/snapshot.json b/packages/core/migration/20260602002951_lowly_union_jack/snapshot.json deleted file mode 100644 index ca0be6da3e7..00000000000 --- a/packages/core/migration/20260602002951_lowly_union_jack/snapshot.json +++ /dev/null @@ -1,1602 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "80d6efb8-93fd-4ce5-b320-45a05aaebdd7", - "prevIds": ["226375f1-a19f-4c7b-8aa2-ccc5513d3b0d"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260602182828_add_project_directories/migration.sql b/packages/core/migration/20260602182828_add_project_directories/migration.sql deleted file mode 100644 index 0ab297096a0..00000000000 --- a/packages/core/migration/20260602182828_add_project_directories/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE `project_directory` ( - `project_id` text NOT NULL, - `directory` text NOT NULL, - `type` text NOT NULL, - `time_created` integer NOT NULL, - CONSTRAINT `project_directory_pk` PRIMARY KEY(`project_id`, `directory`), - CONSTRAINT `fk_project_directory_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE -); diff --git a/packages/core/migration/20260602182828_add_project_directories/snapshot.json b/packages/core/migration/20260602182828_add_project_directories/snapshot.json deleted file mode 100644 index c96598c2acd..00000000000 --- a/packages/core/migration/20260602182828_add_project_directories/snapshot.json +++ /dev/null @@ -1,1664 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "80f2378a-ed35-45cb-9d3b-9f4837fac801", - "prevIds": ["7f4866d3-a95b-4141-bb59-28e31c521605", "80d6efb8-93fd-4ce5-b320-45a05aaebdd7"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project_directory", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_project_directory_project_id_project_id_fk", - "entityType": "fks", - "table": "project_directory" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["project_id", "directory"], - "nameExplicit": false, - "name": "project_directory_pk", - "entityType": "pks", - "table": "project_directory" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260603001617_session_message_projection_indexes/migration.sql b/packages/core/migration/20260603001617_session_message_projection_indexes/migration.sql deleted file mode 100644 index ed6b728a1fc..00000000000 --- a/packages/core/migration/20260603001617_session_message_projection_indexes/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ -DROP INDEX IF EXISTS `session_message_session_idx`;--> statement-breakpoint -DROP INDEX IF EXISTS `session_message_session_type_idx`;--> statement-breakpoint -CREATE INDEX `event_aggregate_seq_idx` ON `event` (`aggregate_id`,`seq`);--> statement-breakpoint -CREATE INDEX `session_message_session_time_created_id_idx` ON `session_message` (`session_id`,`time_created`,`id`);--> statement-breakpoint -CREATE INDEX `session_message_session_type_time_created_id_idx` ON `session_message` (`session_id`,`type`,`time_created`,`id`); \ No newline at end of file diff --git a/packages/core/migration/20260603001617_session_message_projection_indexes/snapshot.json b/packages/core/migration/20260603001617_session_message_projection_indexes/snapshot.json deleted file mode 100644 index e89ee645551..00000000000 --- a/packages/core/migration/20260603001617_session_message_projection_indexes/snapshot.json +++ /dev/null @@ -1,1636 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "6a0e33d0-4866-402f-b287-de400200b05e", - "prevIds": ["80f2378a-ed35-45cb-9d3b-9f4837fac801"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "event_aggregate_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_time_created_id_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_time_created_id_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260603040000_session_message_projection_order/migration.sql b/packages/core/migration/20260603040000_session_message_projection_order/migration.sql deleted file mode 100644 index e603be927ba..00000000000 --- a/packages/core/migration/20260603040000_session_message_projection_order/migration.sql +++ /dev/null @@ -1,7 +0,0 @@ -DELETE FROM `session_message`;--> statement-breakpoint --- kilocode_change -ALTER TABLE `session_message` ADD `seq` integer;--> statement-breakpoint -DROP INDEX IF EXISTS `session_message_session_time_created_id_idx`;--> statement-breakpoint -DROP INDEX IF EXISTS `session_message_session_type_time_created_id_idx`;--> statement-breakpoint -CREATE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint -CREATE INDEX `session_message_session_type_seq_idx` ON `session_message` (`session_id`,`type`,`seq`); diff --git a/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json b/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json deleted file mode 100644 index 490f1a71820..00000000000 --- a/packages/core/migration/20260603040000_session_message_projection_order/snapshot.json +++ /dev/null @@ -1,1638 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "127d5585-9d6d-4b89-b126-15a36980392c", - "prevIds": ["6a0e33d0-4866-402f-b287-de400200b05e"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "event_aggregate_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260603141458_session_input_inbox/migration.sql b/packages/core/migration/20260603141458_session_input_inbox/migration.sql deleted file mode 100644 index c721ba897d4..00000000000 --- a/packages/core/migration/20260603141458_session_input_inbox/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE `session_input` ( - `seq` integer PRIMARY KEY AUTOINCREMENT, - `id` text NOT NULL UNIQUE, - `session_id` text NOT NULL, - `prompt` text NOT NULL, - `delivery` text NOT NULL, - `promoted_seq` integer, - `time_created` integer NOT NULL, - CONSTRAINT `fk_session_input_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -CREATE INDEX `session_input_session_pending_seq_idx` ON `session_input` (`session_id`,`promoted_seq`,`seq`); \ No newline at end of file diff --git a/packages/core/migration/20260603141458_session_input_inbox/snapshot.json b/packages/core/migration/20260603141458_session_input_inbox/snapshot.json deleted file mode 100644 index 9839a208207..00000000000 --- a/packages/core/migration/20260603141458_session_input_inbox/snapshot.json +++ /dev/null @@ -1,1759 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "442462d9-4f4f-409f-ab00-0f8fb585f1a4", - "prevIds": ["127d5585-9d6d-4b89-b126-15a36980392c"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_input", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": true, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "prompt", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "delivery", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "promoted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_input_session_id_session_id_fk", - "entityType": "fks", - "table": "session_input" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["seq"], - "nameExplicit": false, - "name": "session_input_pk", - "table": "session_input", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "event_aggregate_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_input_session_pending_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_input_id_unique", - "entityType": "uniques", - "table": "session_input" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260603160727_jittery_ezekiel_stane/migration.sql b/packages/core/migration/20260603160727_jittery_ezekiel_stane/migration.sql deleted file mode 100644 index 9a6909a48b3..00000000000 --- a/packages/core/migration/20260603160727_jittery_ezekiel_stane/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ -DROP INDEX IF EXISTS `session_input_session_pending_seq_idx`;--> statement-breakpoint -CREATE INDEX IF NOT EXISTS `event_aggregate_type_seq_idx` ON `event` (`aggregate_id`,`type`,`seq`);--> statement-breakpoint -CREATE INDEX IF NOT EXISTS `session_input_session_pending_delivery_seq_idx` ON `session_input` (`session_id`,`promoted_seq`,`delivery`,`seq`);--> statement-breakpoint -CREATE INDEX IF NOT EXISTS `session_message_session_time_created_id_idx` ON `session_message` (`session_id`,`time_created`,`id`); diff --git a/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json b/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json deleted file mode 100644 index 80c5ff8febb..00000000000 --- a/packages/core/migration/20260603160727_jittery_ezekiel_stane/snapshot.json +++ /dev/null @@ -1,1869 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "fc92fa34-8074-44c3-88f0-a5417f7fd92d", - "prevIds": ["442462d9-4f4f-409f-ab00-0f8fb585f1a4"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project_directory", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_input", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": true, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "prompt", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "delivery", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "promoted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_project_directory_project_id_project_id_fk", - "entityType": "fks", - "table": "project_directory" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_input_session_id_session_id_fk", - "entityType": "fks", - "table": "session_input" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["project_id", "directory"], - "nameExplicit": false, - "name": "project_directory_pk", - "entityType": "pks", - "table": "project_directory" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["seq"], - "nameExplicit": false, - "name": "session_input_pk", - "table": "session_input", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "event_aggregate_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "event_aggregate_type_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - }, - { - "value": "delivery", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_input_session_pending_delivery_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_time_created_id_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_input_id_unique", - "entityType": "uniques", - "table": "session_input" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260604172448_event_sourced_session_input/migration.sql b/packages/core/migration/20260604172448_event_sourced_session_input/migration.sql deleted file mode 100644 index 0c89cc80361..00000000000 --- a/packages/core/migration/20260604172448_event_sourced_session_input/migration.sql +++ /dev/null @@ -1,28 +0,0 @@ -DELETE FROM `session_input`;--> statement-breakpoint -DELETE FROM `session_message`;--> statement-breakpoint -DELETE FROM `event`;--> statement-breakpoint -DELETE FROM `event_sequence`;--> statement-breakpoint -UPDATE `session` SET `workspace_id` = NULL;--> statement-breakpoint -DELETE FROM `workspace`;--> statement-breakpoint -DROP INDEX IF EXISTS `event_aggregate_seq_idx`;--> statement-breakpoint -CREATE UNIQUE INDEX `event_aggregate_seq_idx` ON `event` (`aggregate_id`,`seq`);--> statement-breakpoint -DROP INDEX IF EXISTS `session_message_session_seq_idx`;--> statement-breakpoint -CREATE UNIQUE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint -PRAGMA foreign_keys=OFF;--> statement-breakpoint -CREATE TABLE `__new_session_input` ( - `id` text PRIMARY KEY, - `session_id` text NOT NULL, - `prompt` text NOT NULL, - `delivery` text NOT NULL, - `admitted_seq` integer NOT NULL, - `promoted_seq` integer, - `time_created` integer NOT NULL, - CONSTRAINT `fk_session_input_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -DROP TABLE `session_input`;--> statement-breakpoint -ALTER TABLE `__new_session_input` RENAME TO `session_input`;--> statement-breakpoint -PRAGMA foreign_keys=ON;--> statement-breakpoint -CREATE INDEX `session_input_session_pending_delivery_seq_idx` ON `session_input` (`session_id`,`promoted_seq`,`delivery`,`admitted_seq`);--> statement-breakpoint -CREATE UNIQUE INDEX `session_input_session_admitted_seq_idx` ON `session_input` (`session_id`,`admitted_seq`);--> statement-breakpoint -CREATE UNIQUE INDEX `session_input_session_promoted_seq_idx` ON `session_input` (`session_id`,`promoted_seq`); diff --git a/packages/core/migration/20260604172448_event_sourced_session_input/snapshot.json b/packages/core/migration/20260604172448_event_sourced_session_input/snapshot.json deleted file mode 100644 index 07d008d89e8..00000000000 --- a/packages/core/migration/20260604172448_event_sourced_session_input/snapshot.json +++ /dev/null @@ -1,1898 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "84c6ad6c-6116-48e1-b973-6fee4593496b", - "prevIds": ["fc92fa34-8074-44c3-88f0-a5417f7fd92d"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project_directory", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_input", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "prompt", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "delivery", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "admitted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "promoted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_project_directory_project_id_project_id_fk", - "entityType": "fks", - "table": "project_directory" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_input_session_id_session_id_fk", - "entityType": "fks", - "table": "session_input" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["project_id", "directory"], - "nameExplicit": false, - "name": "project_directory_pk", - "entityType": "pks", - "table": "project_directory" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_input_pk", - "table": "session_input", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "event_aggregate_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "event_aggregate_type_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - }, - { - "value": "delivery", - "isExpression": false - }, - { - "value": "admitted_seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_input_session_pending_delivery_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "admitted_seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_input_session_admitted_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_input_session_promoted_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_message_session_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_time_created_id_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260605003541_add_session_context_snapshot/migration.sql b/packages/core/migration/20260605003541_add_session_context_snapshot/migration.sql deleted file mode 100644 index ec98751154f..00000000000 --- a/packages/core/migration/20260605003541_add_session_context_snapshot/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE `session_context_epoch` ( - `session_id` text PRIMARY KEY, - `baseline` text NOT NULL, - `snapshot` text NOT NULL, - `baseline_seq` integer NOT NULL, - `replacement_seq` integer, - `revision` integer DEFAULT 0 NOT NULL, - CONSTRAINT `fk_session_context_epoch_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE -); diff --git a/packages/core/migration/20260605003541_add_session_context_snapshot/snapshot.json b/packages/core/migration/20260605003541_add_session_context_snapshot/snapshot.json deleted file mode 100644 index 97695397242..00000000000 --- a/packages/core/migration/20260605003541_add_session_context_snapshot/snapshot.json +++ /dev/null @@ -1,1980 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "40f7b9b8-83b4-4ea0-a59f-76a489679d88", - "prevIds": ["84c6ad6c-6116-48e1-b973-6fee4593496b"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project_directory", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_context_epoch", - "entityType": "tables" - }, - { - "name": "session_input", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "baseline", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "snapshot", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "baseline_seq", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "replacement_seq", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "revision", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "prompt", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "delivery", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "admitted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "promoted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_project_directory_project_id_project_id_fk", - "entityType": "fks", - "table": "project_directory" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_context_epoch_session_id_session_id_fk", - "entityType": "fks", - "table": "session_context_epoch" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_input_session_id_session_id_fk", - "entityType": "fks", - "table": "session_input" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["project_id", "directory"], - "nameExplicit": false, - "name": "project_directory_pk", - "entityType": "pks", - "table": "project_directory" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_context_epoch_pk", - "table": "session_context_epoch", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_input_pk", - "table": "session_input", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "event_aggregate_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "event_aggregate_type_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - }, - { - "value": "delivery", - "isExpression": false - }, - { - "value": "admitted_seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_input_session_pending_delivery_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "admitted_seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_input_session_admitted_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_input_session_promoted_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_message_session_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_time_created_id_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260605042240_add_context_epoch_agent/migration.sql b/packages/core/migration/20260605042240_add_context_epoch_agent/migration.sql deleted file mode 100644 index a9534b9b08f..00000000000 --- a/packages/core/migration/20260605042240_add_context_epoch_agent/migration.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE `session_context_epoch` ADD `agent` text DEFAULT 'build' NOT NULL; \ No newline at end of file diff --git a/packages/core/migration/20260605042240_add_context_epoch_agent/snapshot.json b/packages/core/migration/20260605042240_add_context_epoch_agent/snapshot.json deleted file mode 100644 index 0c8fb75d725..00000000000 --- a/packages/core/migration/20260605042240_add_context_epoch_agent/snapshot.json +++ /dev/null @@ -1,1990 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "d1bfa125-b81e-4c61-9b6e-e74abf6e488f", - "prevIds": ["40f7b9b8-83b4-4ea0-a59f-76a489679d88"], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project_directory", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_context_epoch", - "entityType": "tables" - }, - { - "name": "session_input", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "baseline", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "'build'", - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "snapshot", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "baseline_seq", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "replacement_seq", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "revision", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "prompt", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "delivery", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "admitted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "promoted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": ["active_account_id"], - "tableTo": "account", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": ["aggregate_id"], - "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_project_directory_project_id_project_id_fk", - "entityType": "fks", - "table": "project_directory" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": ["message_id"], - "tableTo": "message", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_context_epoch_session_id_session_id_fk", - "entityType": "fks", - "table": "session_context_epoch" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_input_session_id_session_id_fk", - "entityType": "fks", - "table": "session_input" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": ["project_id"], - "tableTo": "project", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": ["session_id"], - "tableTo": "session", - "columnsTo": ["id"], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": ["email", "url"], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": ["project_id", "directory"], - "nameExplicit": false, - "name": "project_directory_pk", - "entityType": "pks", - "table": "project_directory" - }, - { - "columns": ["session_id", "position"], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": ["name"], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": ["aggregate_id"], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_context_epoch_pk", - "table": "session_context_epoch", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_input_pk", - "table": "session_input", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": ["id"], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": ["session_id"], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "event_aggregate_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "event_aggregate_type_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - }, - { - "value": "delivery", - "isExpression": false - }, - { - "value": "admitted_seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_input_session_pending_delivery_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "admitted_seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_input_session_admitted_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_input_session_promoted_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_message_session_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_time_created_id_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/migration/20260611035744_credential/migration.sql b/packages/core/migration/20260611035744_credential/migration.sql deleted file mode 100644 index c950306e465..00000000000 --- a/packages/core/migration/20260611035744_credential/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE `credential` ( - `id` text PRIMARY KEY, - `connector_id` text NOT NULL, - `method_id` text NOT NULL, - `label` text NOT NULL, - `value` text NOT NULL, - `active` integer DEFAULT false NOT NULL, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `credential_connector_active_idx` ON `credential` (`connector_id`) WHERE "credential"."active" = 1; diff --git a/packages/core/migration/20260714141136_session-message-legacy-writer-compat/migration.sql b/packages/core/migration/20260714141136_session-message-legacy-writer-compat/migration.sql deleted file mode 100644 index 62a665543a8..00000000000 --- a/packages/core/migration/20260714141136_session-message-legacy-writer-compat/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ --- kilocode_change - new file -CREATE TABLE `__new_session_message` ( - `id` text PRIMARY KEY, - `session_id` text NOT NULL, - `type` text NOT NULL, - `seq` integer, - `time_created` integer NOT NULL, - `time_updated` integer NOT NULL, - `data` text NOT NULL, - CONSTRAINT `fk_session_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE -); ---> statement-breakpoint -INSERT INTO `__new_session_message`(`id`, `session_id`, `type`, `seq`, `time_created`, `time_updated`, `data`) SELECT `id`, `session_id`, `type`, `seq`, `time_created`, `time_updated`, `data` FROM `session_message`;--> statement-breakpoint -DROP TABLE `session_message`;--> statement-breakpoint -ALTER TABLE `__new_session_message` RENAME TO `session_message`;--> statement-breakpoint -CREATE UNIQUE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint -CREATE INDEX `session_message_session_type_seq_idx` ON `session_message` (`session_id`,`type`,`seq`);--> statement-breakpoint -CREATE INDEX `session_message_session_time_created_id_idx` ON `session_message` (`session_id`,`time_created`,`id`);--> statement-breakpoint -CREATE INDEX `session_message_time_created_idx` ON `session_message` (`time_created`); diff --git a/packages/core/migration/20260714141136_session-message-legacy-writer-compat/snapshot.json b/packages/core/migration/20260714141136_session-message-legacy-writer-compat/snapshot.json deleted file mode 100644 index ca05d7d5945..00000000000 --- a/packages/core/migration/20260714141136_session-message-legacy-writer-compat/snapshot.json +++ /dev/null @@ -1,2190 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "id": "6c030252-8b68-4107-b18a-f64f99b76895", - "prevIds": [ - "f25f9126-c7dc-4882-9ff4-af27e11d2da1" - ], - "ddl": [ - { - "name": "workspace", - "entityType": "tables" - }, - { - "name": "data_migration", - "entityType": "tables" - }, - { - "name": "account_state", - "entityType": "tables" - }, - { - "name": "account", - "entityType": "tables" - }, - { - "name": "control_account", - "entityType": "tables" - }, - { - "name": "credential", - "entityType": "tables" - }, - { - "name": "event_sequence", - "entityType": "tables" - }, - { - "name": "event", - "entityType": "tables" - }, - { - "name": "permission", - "entityType": "tables" - }, - { - "name": "project_directory", - "entityType": "tables" - }, - { - "name": "project", - "entityType": "tables" - }, - { - "name": "message", - "entityType": "tables" - }, - { - "name": "part", - "entityType": "tables" - }, - { - "name": "session_context_epoch", - "entityType": "tables" - }, - { - "name": "session_input", - "entityType": "tables" - }, - { - "name": "session_message", - "entityType": "tables" - }, - { - "name": "session", - "entityType": "tables" - }, - { - "name": "todo", - "entityType": "tables" - }, - { - "name": "session_share", - "entityType": "tables" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "''", - "generated": null, - "name": "name", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "branch", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "extra", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_used", - "entityType": "columns", - "table": "workspace" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_completed", - "entityType": "columns", - "table": "data_migration" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_account_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active_org_id", - "entityType": "columns", - "table": "account_state" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "email", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "access_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "refresh_token", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "token_expiry", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "active", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "control_account" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "credential" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "connector_id", - "entityType": "columns", - "table": "credential" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "method_id", - "entityType": "columns", - "table": "credential" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "label", - "entityType": "columns", - "table": "credential" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "value", - "entityType": "columns", - "table": "credential" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "false", - "generated": null, - "name": "active", - "entityType": "columns", - "table": "credential" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "credential" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "credential" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "owner_id", - "entityType": "columns", - "table": "event_sequence" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "aggregate_id", - "entityType": "columns", - "table": "event" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "event" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "action", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "resource", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "permission" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "permission" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project_directory" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "worktree", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "vcs", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "name", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_url_override", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "icon_color", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "project" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_initialized", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "sandboxes", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "commands", - "entityType": "columns", - "table": "project" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "message_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "part" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "part" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "baseline", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": "'build'", - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "snapshot", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "baseline_seq", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "replacement_seq", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "revision", - "entityType": "columns", - "table": "session_context_epoch" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "prompt", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "delivery", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "admitted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "promoted_seq", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_input" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "type", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "seq", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "data", - "entityType": "columns", - "table": "session_message" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "project_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "workspace_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "parent_id", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "slug", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "directory", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "path", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "title", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "version", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "share_url", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_additions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_deletions", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_files", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "summary_diffs", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "metadata", - "entityType": "columns", - "table": "session" - }, - { - "type": "real", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "cost", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_input", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_output", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_reasoning", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_read", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": "0", - "generated": null, - "name": "tokens_cache_write", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "revert", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "permission", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "agent", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "model", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_compacting", - "entityType": "columns", - "table": "session" - }, - { - "type": "integer", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_archived", - "entityType": "columns", - "table": "session" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "content", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "status", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "priority", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "position", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "todo" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "todo" - }, - { - "type": "text", - "notNull": false, - "autoincrement": false, - "default": null, - "generated": null, - "name": "session_id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "id", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "secret", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "url", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_created", - "entityType": "columns", - "table": "session_share" - }, - { - "type": "integer", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "time_updated", - "entityType": "columns", - "table": "session_share" - }, - { - "columns": [ - "project_id" - ], - "tableTo": "project", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_workspace_project_id_project_id_fk", - "entityType": "fks", - "table": "workspace" - }, - { - "columns": [ - "active_account_id" - ], - "tableTo": "account", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "SET NULL", - "nameExplicit": false, - "name": "fk_account_state_active_account_id_account_id_fk", - "entityType": "fks", - "table": "account_state" - }, - { - "columns": [ - "aggregate_id" - ], - "tableTo": "event_sequence", - "columnsTo": [ - "aggregate_id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", - "entityType": "fks", - "table": "event" - }, - { - "columns": [ - "project_id" - ], - "tableTo": "project", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_permission_project_id_project_id_fk", - "entityType": "fks", - "table": "permission" - }, - { - "columns": [ - "project_id" - ], - "tableTo": "project", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_project_directory_project_id_project_id_fk", - "entityType": "fks", - "table": "project_directory" - }, - { - "columns": [ - "session_id" - ], - "tableTo": "session", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_message_session_id_session_id_fk", - "entityType": "fks", - "table": "message" - }, - { - "columns": [ - "message_id" - ], - "tableTo": "message", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_part_message_id_message_id_fk", - "entityType": "fks", - "table": "part" - }, - { - "columns": [ - "session_id" - ], - "tableTo": "session", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_context_epoch_session_id_session_id_fk", - "entityType": "fks", - "table": "session_context_epoch" - }, - { - "columns": [ - "session_id" - ], - "tableTo": "session", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_input_session_id_session_id_fk", - "entityType": "fks", - "table": "session_input" - }, - { - "columns": [ - "session_id" - ], - "tableTo": "session", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_message_session_id_session_id_fk", - "entityType": "fks", - "table": "session_message" - }, - { - "columns": [ - "project_id" - ], - "tableTo": "project", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_project_id_project_id_fk", - "entityType": "fks", - "table": "session" - }, - { - "columns": [ - "session_id" - ], - "tableTo": "session", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_todo_session_id_session_id_fk", - "entityType": "fks", - "table": "todo" - }, - { - "columns": [ - "session_id" - ], - "tableTo": "session", - "columnsTo": [ - "id" - ], - "onUpdate": "NO ACTION", - "onDelete": "CASCADE", - "nameExplicit": false, - "name": "fk_session_share_session_id_session_id_fk", - "entityType": "fks", - "table": "session_share" - }, - { - "columns": [ - "email", - "url" - ], - "nameExplicit": false, - "name": "control_account_pk", - "entityType": "pks", - "table": "control_account" - }, - { - "columns": [ - "project_id", - "directory" - ], - "nameExplicit": false, - "name": "project_directory_pk", - "entityType": "pks", - "table": "project_directory" - }, - { - "columns": [ - "session_id", - "position" - ], - "nameExplicit": false, - "name": "todo_pk", - "entityType": "pks", - "table": "todo" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "workspace_pk", - "table": "workspace", - "entityType": "pks" - }, - { - "columns": [ - "name" - ], - "nameExplicit": false, - "name": "data_migration_pk", - "table": "data_migration", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "account_state_pk", - "table": "account_state", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "account_pk", - "table": "account", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "credential_pk", - "table": "credential", - "entityType": "pks" - }, - { - "columns": [ - "aggregate_id" - ], - "nameExplicit": false, - "name": "event_sequence_pk", - "table": "event_sequence", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "event_pk", - "table": "event", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "permission_pk", - "table": "permission", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "project_pk", - "table": "project", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "message_pk", - "table": "message", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "part_pk", - "table": "part", - "entityType": "pks" - }, - { - "columns": [ - "session_id" - ], - "nameExplicit": false, - "name": "session_context_epoch_pk", - "table": "session_context_epoch", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "session_input_pk", - "table": "session_input", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "session_message_pk", - "table": "session_message", - "entityType": "pks" - }, - { - "columns": [ - "id" - ], - "nameExplicit": false, - "name": "session_pk", - "table": "session", - "entityType": "pks" - }, - { - "columns": [ - "session_id" - ], - "nameExplicit": false, - "name": "session_share_pk", - "table": "session_share", - "entityType": "pks" - }, - { - "columns": [ - { - "value": "connector_id", - "isExpression": false - } - ], - "isUnique": true, - "where": "\"credential\".\"active\" = 1", - "origin": "manual", - "name": "credential_connector_active_idx", - "entityType": "indexes", - "table": "credential" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "event_aggregate_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "aggregate_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "event_aggregate_type_seq_idx", - "entityType": "indexes", - "table": "event" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - }, - { - "value": "action", - "isExpression": false - }, - { - "value": "resource", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "permission_project_action_resource_idx", - "entityType": "indexes", - "table": "permission" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "message_session_time_created_id_idx", - "entityType": "indexes", - "table": "message" - }, - { - "columns": [ - { - "value": "message_id", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_message_id_id_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "part_session_idx", - "entityType": "indexes", - "table": "part" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - }, - { - "value": "delivery", - "isExpression": false - }, - { - "value": "admitted_seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_input_session_pending_delivery_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "admitted_seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_input_session_admitted_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "promoted_seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_input_session_promoted_seq_idx", - "entityType": "indexes", - "table": "session_input" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": true, - "where": null, - "origin": "manual", - "name": "session_message_session_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "type", - "isExpression": false - }, - { - "value": "seq", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_type_seq_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - }, - { - "value": "time_created", - "isExpression": false - }, - { - "value": "id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_session_time_created_id_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "time_created", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_message_time_created_idx", - "entityType": "indexes", - "table": "session_message" - }, - { - "columns": [ - { - "value": "project_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_project_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "workspace_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_workspace_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "parent_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "session_parent_idx", - "entityType": "indexes", - "table": "session" - }, - { - "columns": [ - { - "value": "session_id", - "isExpression": false - } - ], - "isUnique": false, - "where": null, - "origin": "manual", - "name": "todo_session_idx", - "entityType": "indexes", - "table": "todo" - } - ], - "renames": [] -} diff --git a/packages/core/package.json b/packages/core/package.json index d098834e434..d5f73a3983a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.13", + "version": "7.4.16", "name": "@opencode-ai/core", "type": "module", "license": "MIT", @@ -101,7 +101,7 @@ "@ai-sdk/provider-utils": "4.0.23", "@ai-sdk/togetherai": "2.0.41", "@ai-sdk/vercel": "2.0.39", - "@ai-sdk/xai": "3.0.92", + "@ai-sdk/xai": "3.0.102", "@aws-sdk/credential-providers": "3.1057.0", "@openrouter/ai-sdk-provider": "2.9.0", "ai-gateway-provider": "3.1.2", diff --git a/packages/core/migration/20260611035744_credential/snapshot.json b/packages/core/schema.json similarity index 98% rename from packages/core/migration/20260611035744_credential/snapshot.json rename to packages/core/schema.json index cfc3930305c..38f9ce65c83 100644 --- a/packages/core/migration/20260611035744_credential/snapshot.json +++ b/packages/core/schema.json @@ -1,8 +1,8 @@ { "version": "7", "dialect": "sqlite", - "id": "f25f9126-c7dc-4882-9ff4-af27e11d2da1", - "prevIds": ["d1bfa125-b81e-4c61-9b6e-e74abf6e488f"], + "id": "169a0f0f-d58f-479f-b024-fa1c7b9a09db", + "prevIds": ["abd2f920-b822-49af-b8a7-2e48367d424f"], "ddl": [ { "name": "workspace", @@ -382,21 +382,11 @@ }, { "type": "text", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, - "name": "connector_id", - "entityType": "columns", - "table": "credential" - }, - { - "type": "text", - "notNull": true, - "autoincrement": false, - "default": null, - "generated": null, - "name": "method_id", + "name": "integration_id", "entityType": "columns", "table": "credential" }, @@ -421,10 +411,30 @@ "table": "credential" }, { - "type": "integer", - "notNull": true, + "type": "text", + "notNull": false, "autoincrement": false, - "default": "false", + "default": null, + "generated": null, + "name": "connector_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "method_id", + "entityType": "columns", + "table": "credential" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, "generated": null, "name": "active", "entityType": "columns", @@ -612,7 +622,7 @@ }, { "type": "text", - "notNull": true, + "notNull": false, "autoincrement": false, "default": null, "generated": null, @@ -620,6 +630,16 @@ "entityType": "columns", "table": "project_directory" }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "strategy", + "entityType": "columns", + "table": "project_directory" + }, { "type": "integer", "notNull": true, @@ -1766,20 +1786,6 @@ "table": "session_share", "entityType": "pks" }, - { - "columns": [ - { - "value": "connector_id", - "isExpression": false - } - ], - "isUnique": true, - "where": "\"credential\".\"active\" = 1", - "origin": "manual", - "name": "credential_connector_active_idx", - "entityType": "indexes", - "table": "credential" - }, { "columns": [ { diff --git a/packages/core/script/migration.ts b/packages/core/script/migration.ts index d8d0f65e893..48b555b5937 100644 --- a/packages/core/script/migration.ts +++ b/packages/core/script/migration.ts @@ -8,9 +8,10 @@ import { pathToFileURL } from "url" import { parseArgs } from "util" const root = path.resolve(import.meta.dirname, "../../..") -const sqlDir = path.join(root, "packages/core/migration") +const snapshot = path.join(root, "packages/core/schema.json") const tsDir = path.join(root, "packages/core/src/database/migration") const registry = path.join(root, "packages/core/src/database/migration.gen.ts") +const schema = path.join(root, "packages/core/src/database/schema.gen.ts") const args = parseArgs({ args: process.argv.slice(2), options: { @@ -24,57 +25,62 @@ if (args.values.check) { process.exit(0) } -await $`bun drizzle-kit generate ${args.values.name ? ["--name", args.values.name] : []}`.cwd( - path.join(root, "packages/core"), -) +await generate() -const sqlMigrations = (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: sqlDir }))) - .map((file) => file.split("/")[0]) - .filter((name) => name !== undefined) - .sort() +async function generate() { + const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-")) + const incremental = path.join(temporary, "incremental") + const full = path.join(temporary, "full") + try { + await fs.mkdir(incremental) + await fs.mkdir(path.join(incremental, "baseline")) + await fs.copyFile(snapshot, path.join(incremental, "baseline/snapshot.json")) + await drizzle(temporary, incremental, args.values.name) -for (const name of sqlMigrations) { - if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue - await Bun.write( - path.join(tsDir, `${name}.ts`), - renderMigration(name, await Bun.file(path.join(sqlDir, name, "migration.sql")).text()), - ) + const generated = await generatedMigrations(incremental) + if (generated.length > 1) throw new Error(`Expected one generated migration, found ${generated.length}.`) + const name = generated[0] + if (name) { + const target = path.join(tsDir, `${name}.ts`) + if (await Bun.file(target).exists()) throw new Error(`Database migration already exists: ${name}`) + await Bun.write( + target, + renderMigration(name, await Bun.file(path.join(incremental, name, "migration.sql")).text()), + ) + await fs.copyFile(path.join(incremental, name, "snapshot.json"), snapshot) + } + + await fs.mkdir(full) + await drizzle(temporary, full, "schema") + await Bun.write(schema, renderSchema(await generatedSql(full))) + await Bun.write(registry, renderRegistry(await typescriptMigrations())) + } finally { + await fs.rm(temporary, { recursive: true, force: true }) + } } -await Bun.write(registry, renderRegistry(sqlMigrations)) - async function check() { const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-check-")) - const output = path.join(temporary, "migration") + const incremental = path.join(temporary, "incremental") + const full = path.join(temporary, "full") try { - await fs.cp(sqlDir, output, { recursive: true }) - const config = path.join(temporary, "drizzle.config.ts") - await Bun.write( - config, - `import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)} - -export default { ...config, out: ${JSON.stringify(output)} } -`, - ) - const before = await snapshot(output) - await $`bun drizzle-kit generate --config ${config}`.cwd(path.join(root, "packages/core")) - const after = await snapshot(output) - if (JSON.stringify(after) !== JSON.stringify(before)) { + await fs.mkdir(incremental) + await fs.mkdir(path.join(incremental, "baseline")) + await fs.copyFile(snapshot, path.join(incremental, "baseline/snapshot.json")) + await drizzle(temporary, incremental) + if ((await generatedMigrations(incremental)).length > 0) { throw new Error( "Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.", ) } - const migrations = before - .map((entry) => entry.path.split("/")[0]) - .filter((name, index, all) => name !== undefined && all.indexOf(name) === index) - .sort() - for (const name of migrations) { - if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue - throw new Error( - `Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`, - ) + await fs.mkdir(full) + await drizzle(temporary, full, "schema") + if ((await Bun.file(schema).text()) !== renderSchema(await generatedSql(full))) { + throw new Error("Current database schema is stale. Run `bun script/migration.ts` from packages/core.") } + + const migrations = await typescriptMigrations() if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) { throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.") } @@ -83,11 +89,37 @@ export default { ...config, out: ${JSON.stringify(output)} } } } -async function snapshot(directory: string) { - const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: directory, onlyFiles: true })) - return Promise.all( - files.sort().map(async (file) => ({ path: file, contents: await Bun.file(path.join(directory, file)).text() })), +async function drizzle(temporary: string, output: string, name?: string) { + const config = path.join(temporary, `${path.basename(output)}.config.ts`) + await Bun.write( + config, + `import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)} + +export default { ...config, out: ${JSON.stringify(output)} } +`, ) + await $`bun drizzle-kit generate --config ${config} ${name ? ["--name", name] : []}`.cwd( + path.join(root, "packages/core"), + ) +} + +async function generatedMigrations(directory: string) { + return (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: directory }))) + .map((file) => file.split("/")[0]) + .filter((name): name is string => name !== undefined) + .sort() +} + +async function generatedSql(directory: string) { + const generated = await generatedMigrations(directory) + if (generated.length !== 1) throw new Error(`Expected one full schema migration, found ${generated.length}.`) + return Bun.file(path.join(directory, generated[0]!, "migration.sql")).text() +} + +async function typescriptMigrations() { + return (await Array.fromAsync(new Bun.Glob("*.ts").scan({ cwd: tsDir }))) + .map((file) => path.basename(file, ".ts")) + .sort() } function renderMigration(name: string, sql: string) { @@ -98,18 +130,36 @@ export default { id: ${JSON.stringify(name)}, up(tx) { return Effect.gen(function* () { -${sql - .split("--> statement-breakpoint") - .map((statement) => statement.trim()) - .filter((statement) => statement.length > 0) - .map(renderRun) - .join("\n")} +${renderStatements(sql)} }) }, } satisfies DatabaseMigration.Migration ` } +function renderSchema(sql: string) { + return `import { Effect } from "effect" +import type { DatabaseMigration } from "./migration" + +export default { + up(tx) { + return Effect.gen(function* () { +${renderStatements(sql)} + }) + }, +} satisfies Omit +` +} + +function renderStatements(sql: string) { + return sql + .split("--> statement-breakpoint") + .map((statement) => statement.trim()) + .filter((statement) => statement.length > 0) + .map(renderRun) + .join("\n") +} + function renderRun(statement: string) { const lines = statement.replaceAll("\t", " ").split("\n") if (lines.length === 1) return ` yield* tx.run(\`${escapeTemplate(lines[0])}\`)` diff --git a/packages/core/src/catalog.ts b/packages/core/src/catalog.ts index d219c763898..82ab2b82136 100644 --- a/packages/core/src/catalog.ts +++ b/packages/core/src/catalog.ts @@ -1,6 +1,6 @@ export * as Catalog from "./catalog" -import { Context, Effect, Layer, Option, Order, pipe, Schema, Array, Scope, Stream } from "effect" +import { Array, Context, Effect, Layer, Option, Order, pipe, Schema, Scope, Stream } from "effect" import { castDraft, enableMapSet, type Draft } from "immer" import { ModelV2 } from "./model" import { ModelRequest } from "./model-request" @@ -11,7 +11,7 @@ import { EventV2 } from "./event" import { Policy } from "./policy" import { State } from "./state" import { Credential } from "./credential" -import { ConnectorSchema } from "./connector/schema" +import { IntegrationSchema } from "./integration/schema" export type ProviderRecord = { provider: ProviderV2.Info @@ -99,8 +99,8 @@ export const layer = Layer.effect( const credentials = yield* Credential.Service const scope = yield* Scope.Scope - const project = (provider: ProviderV2.Info, active: Map) => { - const credential = active.get(ConnectorSchema.ID.make(provider.id)) + const project = (provider: ProviderV2.Info, active: Map) => { + const credential = active.get(IntegrationSchema.ID.make(provider.id)) if (!credential) return provider const body = { ...provider.request.body } if (credential.value.type === "key") { @@ -216,7 +216,9 @@ export const layer = Layer.effect( } }), }) - const active = () => credentials.activeAll().pipe(Effect.orDie) + const active = Effect.fn("CatalogV2.active")(function* () { + return new Map((yield* credentials.all()).map((credential) => [credential.integrationID, credential])) + }) yield* events.subscribe(PluginV2.Event.Added).pipe( // Plugin registries are location scoped even though the event bus is process scoped. diff --git a/packages/core/src/connector.ts b/packages/core/src/connector.ts deleted file mode 100644 index 24656a0067a..00000000000 --- a/packages/core/src/connector.ts +++ /dev/null @@ -1,538 +0,0 @@ -export * as Connector from "./connector" - -import { Cause, Clock, Context, Duration, Effect, Exit, Layer, Schedule, Schema, Scope, SynchronizedRef } from "effect" -import { castDraft, enableMapSet, type Draft } from "immer" -import { Credential } from "./credential" -import { ConnectorSchema } from "./connector/schema" -import { withStatics } from "./schema" -import { State } from "./state" -import { Identifier } from "./util/identifier" -import { KeyedMutex } from "./effect/keyed-mutex" -import { EventV2 } from "./event" - -export const ID = ConnectorSchema.ID -export type ID = ConnectorSchema.ID - -export const MethodID = ConnectorSchema.MethodID -export type MethodID = ConnectorSchema.MethodID - -export const AttemptID = Schema.String.pipe( - Schema.brand("Connector.AttemptID"), - withStatics((schema) => ({ create: () => schema.make("con_" + Identifier.ascending()) })), -) -export type AttemptID = typeof AttemptID.Type - -export const When = Schema.Struct({ - key: Schema.String, - op: Schema.Literals(["eq", "neq"]), - value: Schema.String, -}).annotate({ identifier: "Connector.When" }) -export type When = typeof When.Type - -export class TextPrompt extends Schema.Class("Connector.TextPrompt")({ - type: Schema.Literal("text"), - key: Schema.String, - message: Schema.String, - placeholder: Schema.optional(Schema.String), - when: Schema.optional(When), -}) {} - -export class SelectPrompt extends Schema.Class("Connector.SelectPrompt")({ - type: Schema.Literal("select"), - key: Schema.String, - message: Schema.String, - options: Schema.Array( - Schema.Struct({ - label: Schema.String, - value: Schema.String, - hint: Schema.optional(Schema.String), - }), - ), - when: Schema.optional(When), -}) {} - -export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type")) -export type Prompt = typeof Prompt.Type - -export class OAuthMethod extends Schema.Class("Connector.OAuthMethod")({ - id: MethodID, - type: Schema.Literal("oauth"), - label: Schema.String, - prompts: Schema.optional(Schema.Array(Prompt)), -}) {} - -export class KeyMethod extends Schema.Class("Connector.KeyMethod")({ - id: MethodID, - type: Schema.Literal("key"), - label: Schema.String, - prompts: Schema.optional(Schema.Array(Prompt)), -}) {} - -export const Method = Schema.Union([OAuthMethod, KeyMethod]).pipe(Schema.toTaggedUnion("type")) -export type Method = typeof Method.Type - -export class Info extends Schema.Class("Connector.Info")({ - id: ID, - name: Schema.String, - methods: Schema.Array(Method), -}) {} - -export type Inputs = Readonly<{ [key: string]: string }> - -export type OAuthAuthorization = { - readonly url: string - readonly instructions: string -} & ( - | { - readonly mode: "auto" - readonly callback: Effect.Effect - } - | { - readonly mode: "code" - readonly callback: (code: string) => Effect.Effect - } -) - -export interface OAuthImplementation { - readonly connectorID: ID - readonly method: OAuthMethod - readonly authorize: (inputs: Inputs) => Effect.Effect - readonly refresh?: (credential: Credential.OAuth) => Effect.Effect -} - -export interface KeyImplementation { - readonly connectorID: ID - readonly method: KeyMethod - readonly authorize: (key: string, inputs: Inputs) => Effect.Effect -} - -export type Implementation = OAuthImplementation | KeyImplementation - -function isKeyImplementation(implementation: Implementation): implementation is KeyImplementation { - return implementation.method.type === "key" -} - -function isOAuthImplementation(implementation: Implementation): implementation is OAuthImplementation { - return implementation.method.type === "oauth" -} - -export class Attempt extends Schema.Class("Connector.Attempt")({ - attemptID: AttemptID, - url: Schema.String, - instructions: Schema.String, - mode: Schema.Literals(["auto", "code"]), - time: Schema.Struct({ - created: Schema.Number, - expires: Schema.Number, - }), -}) {} - -const Time = Schema.Struct({ - created: Schema.Number, - expires: Schema.Number, -}) - -export const AttemptStatus = Schema.Union([ - Schema.Struct({ status: Schema.Literal("pending"), time: Time }), - Schema.Struct({ status: Schema.Literal("complete"), time: Time }), - Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: Time }), - Schema.Struct({ status: Schema.Literal("expired"), time: Time }), -]).pipe(Schema.toTaggedUnion("status")) -export type AttemptStatus = typeof AttemptStatus.Type - -export class CodeRequiredError extends Schema.TaggedErrorClass()("Connector.CodeRequired", { - attemptID: AttemptID, -}) {} - -export class AuthorizationError extends Schema.TaggedErrorClass()("Connector.Authorization", { - cause: Schema.Defect, -}) {} - -export type Error = CodeRequiredError | AuthorizationError - -export const Event = { - Updated: EventV2.define({ - type: "connector.updated", - schema: {}, - }), -} - -type Entry = { - connector: Info - implementations: Map -} - -type Data = { - connectors: Map -} - -export type Editor = { - list: () => readonly Info[] - get: (id: ID) => Info | undefined - update: (id: ID, update: (connector: Draft>) => void) => void - remove: (id: ID) => void - method: { - update: (implementation: Implementation) => void - remove: (connectorID: ID, methodID: MethodID) => void - } -} - -export interface Interface { - /** Registers a scoped transform over the connector registry. */ - readonly transform: State.Interface["transform"] - /** Registers and immediately applies a scoped connector registry update. */ - readonly update: State.Interface["update"] - /** Returns one connector with its serializable login methods. */ - readonly get: (id: ID) => Effect.Effect - /** Returns all connectors with their serializable login methods. */ - readonly list: () => Effect.Effect - /** Refreshes an OAuth credential with its originating method. */ - readonly refresh: (credentialID: Credential.ID) => Effect.Effect - readonly connect: { - /** Runs a key method and stores the resulting credential. */ - readonly key: (input: { - /** Connector receiving the credential. */ - readonly connectorID: ID - /** Key method selected by the caller. */ - readonly methodID: MethodID - /** Secret entered by the user. */ - readonly key: string - /** Answers to the method's optional prompts. */ - readonly inputs: Inputs - /** User-facing label for the stored credential. */ - readonly label?: string - }) => Effect.Effect - readonly oauth: { - /** Starts a stateful OAuth attempt. */ - readonly begin: (input: { - /** Connector being authenticated. */ - readonly connectorID: ID - /** OAuth method selected by the caller. */ - readonly methodID: MethodID - /** Answers to the method's optional prompts. */ - readonly inputs: Inputs - /** User-facing label for the credential created on completion. */ - readonly label?: string - }) => Effect.Effect - /** Returns the current state of an OAuth attempt. */ - readonly status: (attemptID: AttemptID) => Effect.Effect - /** Completes the attempt and stores its credential. */ - readonly complete: (input: { - /** Opaque handle returned by `begin`. */ - readonly attemptID: AttemptID - /** Authorization code required by attempts in code mode. */ - readonly code?: string - }) => Effect.Effect - /** Cancels an attempt and releases its resources. */ - readonly cancel: (attemptID: AttemptID) => Effect.Effect - } - } -} - -export class Service extends Context.Service()("@opencode/v2/Connector") {} - -enableMapSet() - -const attemptLifetime = Duration.toMillis(Duration.minutes(10)) -const terminalRetention = Duration.toMillis(Duration.minutes(1)) -const scrubInterval = Duration.seconds(30) -const settlementTimeout = Duration.seconds(30) // kilocode_change - bound retained OAuth attempt secrets - -type AttemptTime = { created: number; expires: number } -type PendingAttempt = { - status: "pending" - completing: boolean - settling: boolean // kilocode_change - cancellation and expiry cannot overtake credential persistence - authorization: OAuthAuthorization - connectorID: ID - methodID: MethodID - label?: string - scope: Scope.Closeable - time: AttemptTime -} -type TerminalAttempt = { - status: "complete" | "failed" | "expired" - message?: string - removeAt: number - time: AttemptTime -} -type AttemptEntry = PendingAttempt | TerminalAttempt - -export const locationLayer = Layer.effect( - Service, - Effect.gen(function* () { - const credentials = yield* Credential.Service - const events = yield* EventV2.Service - const scope = yield* Scope.Scope - const attempts = SynchronizedRef.makeUnsafe(new Map()) - const refreshLocks = KeyedMutex.makeUnsafe() - const state = State.create({ - initial: () => ({ connectors: new Map() }), - editor: (draft) => ({ - list: () => Array.from(draft.connectors.values(), (entry) => entry.connector) as Info[], - get: (id) => draft.connectors.get(id)?.connector as Info | undefined, - update: (id, update) => { - const current = - draft.connectors.get(id) ?? - castDraft({ connector: new Info({ id, name: id, methods: [] }), implementations: new Map() }) - if (!draft.connectors.has(id)) draft.connectors.set(id, current) - update(current.connector) - current.connector.id = id - }, - remove: (id) => draft.connectors.delete(id), - method: { - update: (implementation) => { - const current = - draft.connectors.get(implementation.connectorID) ?? - castDraft({ - connector: new Info({ id: implementation.connectorID, name: implementation.connectorID, methods: [] }), - implementations: new Map(), - }) - if (!draft.connectors.has(implementation.connectorID)) { - draft.connectors.set(implementation.connectorID, current) - } - const index = current.connector.methods.findIndex((method) => method.id === implementation.method.id) - if (index === -1) current.connector.methods.push(castDraft(implementation.method)) - else current.connector.methods[index] = castDraft(implementation.method) - current.implementations.set(implementation.method.id, castDraft(implementation)) - }, - remove: (connectorID, methodID) => { - const current = draft.connectors.get(connectorID) - if (!current) return - const index = current.connector.methods.findIndex((method) => method.id === methodID) - if (index !== -1) current.connector.methods.splice(index, 1) - current.implementations.delete(methodID) - }, - }, - }), - finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), - }) - - const authorize = (effect: Effect.Effect) => - effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause }))) - - const close = (attemptScope: Scope.Closeable) => - Scope.close(attemptScope, Exit.void).pipe(Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid) - - const message = (cause: Cause.Cause) => { - const error = Cause.squash(cause) - return error instanceof Error ? error.message : String(error) - } - - // kilocode_change start - persist before exposing completion and make settlement atomic with cancellation - const settle = Effect.fnUntraced(function* ( - attemptID: AttemptID, - exit: Exit.Exit, - owned = false, - ) { - return yield* Effect.uninterruptibleMask((restore) => - Effect.gen(function* () { - const pending = yield* SynchronizedRef.modify(attempts, (current) => { - const attempt = current.get(attemptID) - if (!attempt || attempt.status !== "pending") return [undefined, current] - if (owned) return attempt.settling ? [attempt, current] : [undefined, current] - if (attempt.settling) return [undefined, current] - return [attempt, new Map(current).set(attemptID, { ...attempt, settling: true })] - }) - if (!pending) return - const settled = Exit.isSuccess(exit) - ? yield* restore( - credentials - .create({ - connectorID: pending.connectorID, - methodID: pending.methodID, - label: pending.label, - value: exit.value, - }) - .pipe( - Effect.timeout(settlementTimeout), - Effect.mapError((cause) => new AuthorizationError({ cause })), - ), - ).pipe(Effect.asVoid, Effect.exit) - : Exit.failCause(exit.cause) - const now = yield* Clock.currentTimeMillis - const result = yield* SynchronizedRef.modify(attempts, (current) => { - const attempt = current.get(attemptID) - if (!attempt || attempt.status !== "pending") return [undefined, current] - const terminal: TerminalAttempt = Exit.isSuccess(settled) - ? { status: "complete", time: attempt.time, removeAt: now + terminalRetention } - : { - status: "failed", - message: message(settled.cause), - time: attempt.time, - removeAt: now + terminalRetention, - } - return [attempt, new Map(current).set(attemptID, terminal)] - }) - if (!result) return settled - yield* close(result.scope) - return settled - }), - ) - }) - // kilocode_change end - - const scrub = Effect.fnUntraced(function* () { - const now = yield* Clock.currentTimeMillis - const expired = yield* SynchronizedRef.modify(attempts, (current) => { - const next = new Map(current) - const scopes: Scope.Closeable[] = [] - for (const [id, attempt] of current) { - if (attempt.status === "pending" && !attempt.settling && attempt.time.expires <= now) { // kilocode_change - scopes.push(attempt.scope) - next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention }) - continue - } - if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id) - } - return [scopes, next] - }) - yield* Effect.forEach(expired, close, { discard: true }) - }) - - yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope)) - - return Service.of({ - transform: state.transform, - update: state.update, - get: Effect.fn("Connector.get")(function* (id) { - return state.get().connectors.get(id)?.connector - }), - list: Effect.fn("Connector.list")(function* () { - return Array.from(state.get().connectors.values(), (record) => record.connector).toSorted((a, b) => - a.name.localeCompare(b.name), - ) - }), - refresh: Effect.fn("Connector.refresh")(function* (credentialID) { - yield* refreshLocks.withLock(credentialID)( - Effect.gen(function* () { - const credential = yield* credentials.get(credentialID) - if (!credential || credential.value.type !== "oauth") { - return yield* Effect.die(`OAuth credential not found: ${credentialID}`) - } - const implementation = state - .get() - .connectors.get(credential.connectorID) - ?.implementations.get(credential.methodID) - if (!implementation || !isOAuthImplementation(implementation) || !implementation.refresh) { - return yield* Effect.die( - `OAuth refresh method not found: ${credential.connectorID}/${credential.methodID}`, - ) - } - const value = yield* authorize(implementation.refresh(credential.value)) - yield* credentials.update(credential.id, { value }) - }), - ) - }), - connect: { - key: Effect.fn("Connector.connect.key")(function* (input) { - const method = state.get().connectors.get(input.connectorID)?.implementations.get(input.methodID) - if (!method || !isKeyImplementation(method)) { - return yield* Effect.die(`Key method not found: ${input.connectorID}/${input.methodID}`) - } - const value = yield* authorize(method.authorize(input.key, input.inputs)) - yield* credentials.create({ - connectorID: input.connectorID, - methodID: input.methodID, - label: input.label, - value, - }) - }), - oauth: { - begin: Effect.fn("Connector.connect.oauth.begin")(function* (input) { - const method = state.get().connectors.get(input.connectorID)?.implementations.get(input.methodID) - if (!method || !isOAuthImplementation(method)) { - return yield* Effect.die(`OAuth method not found: ${input.connectorID}/${input.methodID}`) - } - const attemptScope = yield* Scope.fork(scope) - const authorization = yield* authorize(method.authorize(input.inputs)).pipe( - Scope.provide(attemptScope), - Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)), - ) - const id = AttemptID.create() - const created = yield* Clock.currentTimeMillis - const time = { created, expires: created + attemptLifetime } - yield* SynchronizedRef.update(attempts, (current) => - new Map(current).set(id, { - status: "pending", - completing: authorization.mode === "auto", - settling: false, // kilocode_change - authorization, - connectorID: input.connectorID, - methodID: input.methodID, - label: input.label, - scope: attemptScope, - time, - }), - ) - if (authorization.mode === "auto") { - // kilocode_change start - settle persistence atomically with cancellation - yield* authorize(authorization.callback).pipe( - Effect.exit, - Effect.flatMap((exit) => settle(id, exit)), - Effect.forkIn(attemptScope, { startImmediately: true }), - ) - // kilocode_change end - } - return new Attempt({ - attemptID: id, - url: authorization.url, - instructions: authorization.instructions, - mode: authorization.mode, - time, - }) - }), - status: Effect.fn("Connector.connect.oauth.status")(function* (attemptID) { - const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID) - if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`) - if (attempt.status === "failed") { - return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time } - } - return { status: attempt.status, time: attempt.time } - }), - complete: Effect.fn("Connector.connect.oauth.complete")(function* (input) { - const attempt = yield* SynchronizedRef.modify(attempts, (current) => { - const match = current.get(input.attemptID) - if (!match || match.status !== "pending" || match.completing) return [match, current] - if (match.authorization.mode === "code" && input.code === undefined) return [match, current] - return [match, new Map(current).set(input.attemptID, { ...match, completing: true, settling: true })] // kilocode_change - }) - if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`) - if (attempt.status !== "pending") return - if (attempt.authorization.mode === "code" && input.code === undefined) { - return yield* new CodeRequiredError({ attemptID: input.attemptID }) - } - if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`) - const callback = - attempt.authorization.mode === "auto" - ? attempt.authorization.callback - : attempt.authorization.callback(input.code as string) - // kilocode_change start - an interrupted or timed-out callback still settles and releases its attempt. - return yield* Effect.uninterruptibleMask((restore) => - Effect.gen(function* () { - const exit = yield* restore(authorize(callback)).pipe( - Effect.timeout(settlementTimeout), - Effect.mapError((cause) => new AuthorizationError({ cause })), - Effect.exit, - ) - const settled = yield* settle(input.attemptID, exit, true) - if (settled && Exit.isFailure(settled)) return yield* settled - }), - ) - // kilocode_change end - }), - cancel: Effect.fn("Connector.connect.oauth.cancel")(function* (attemptID) { - const attempt = yield* SynchronizedRef.modify(attempts, (current) => { - const match = current.get(attemptID) - if (!match || match.status !== "pending" || match.settling) return [undefined, current] // kilocode_change - const next = new Map(current) - next.delete(attemptID) - return [match, next] - }) - if (attempt) yield* Scope.close(attempt.scope, Exit.void) - }), - }, - }, - }) - }), -) diff --git a/packages/core/src/connector/schema.ts b/packages/core/src/connector/schema.ts deleted file mode 100644 index 40c43faab84..00000000000 --- a/packages/core/src/connector/schema.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * as ConnectorSchema from "./schema" - -import { Schema } from "effect" - -export const ID = Schema.String.pipe(Schema.brand("Connector.ID")) -export type ID = typeof ID.Type - -export const MethodID = Schema.String.pipe(Schema.brand("Connector.MethodID")) -export type MethodID = typeof MethodID.Type diff --git a/packages/core/src/credential.ts b/packages/core/src/credential.ts index 5b532eb8c20..11a987fac37 100644 --- a/packages/core/src/credential.ts +++ b/packages/core/src/credential.ts @@ -1,20 +1,21 @@ export * as Credential from "./credential" +import { asc, desc, eq } from "drizzle-orm" // kilocode_change // kilocode_change start -import { and, asc, desc, eq, ne } from "drizzle-orm" import { Context, Effect, Layer, Option, Schema, Semaphore } from "effect" // kilocode_change end import { Database } from "./database/database" -import { ConnectorSchema } from "./connector/schema" -import { EventV2 } from "./event" +import { IntegrationSchema } from "./integration/schema" import { NonNegativeInt, withStatics } from "./schema" -import { CredentialTable } from "./credential/sql" import { Identifier } from "./util/identifier" +import { CredentialTable } from "./credential/sql" +// kilocode_change start import { FSUtil } from "./fs-util" import { Global } from "./global" import { DataMigrationTable } from "./data-migration.sql" import path from "path" -import { parse as parseKiloAccounts } from "./kilocode/credential-migration" // kilocode_change +import { parse as parseKiloAccounts } from "./kilocode/credential-migration" +// kilocode_change end export const ID = Schema.String.pipe( Schema.brand("Credential.ID"), @@ -24,6 +25,7 @@ export type ID = typeof ID.Type export class OAuth extends Schema.Class("Credential.OAuth")({ type: Schema.Literal("oauth"), + methodID: IntegrationSchema.MethodID, refresh: Schema.String, access: Schema.String, expires: NonNegativeInt, @@ -36,11 +38,19 @@ export class Key extends Schema.Class("Credential.Key")({ metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), }) {} -export const Value = Schema.Union([OAuth, Key]) +export const Info = Schema.Union([OAuth, Key]) .pipe(Schema.toTaggedUnion("type")) - .annotate({ identifier: "Credential.Value" }) -export type Value = Schema.Schema.Type + .annotate({ identifier: "Credential.Info" }) +export type Info = Schema.Schema.Type +export class Stored extends Schema.Class("Credential.Stored")({ + id: ID, + integrationID: IntegrationSchema.ID, + label: Schema.String, + value: Info, +}) {} + +// kilocode_change start - legacy JSON credential stores that predate the integration credential table const LegacyOAuth = Schema.Struct({ type: Schema.Literal("oauth"), refresh: Schema.String, @@ -56,7 +66,7 @@ const LegacyKey = Schema.Struct({ metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), }) -// kilocode_change start - recognize config-bootstrap credentials without projecting them into model credentials +// recognize config-bootstrap credentials without projecting them into model credentials const LegacyWellKnown = Schema.Struct({ type: Schema.Literal("wellknown"), key: Schema.String, @@ -65,107 +75,95 @@ const LegacyWellKnown = Schema.Struct({ const LegacyValue = Schema.Union([LegacyOAuth, LegacyKey]) const LegacyAuth = Schema.Union([LegacyOAuth, LegacyKey, LegacyWellKnown]) + +const legacyMethod = (integration: IntegrationSchema.ID, type: "oauth" | "api") => + IntegrationSchema.MethodID.make( + type === "api" ? "api-key" : integration === IntegrationSchema.ID.make("openai") ? "chatgpt-browser" : "oauth", + ) + +const legacyValue = (integration: IntegrationSchema.ID, credential: Schema.Schema.Type): Info => + credential.type === "api" + ? new Key({ type: "key", key: credential.key, metadata: credential.metadata }) + : new OAuth({ + type: "oauth", + methodID: legacyMethod(integration, credential.type), + refresh: credential.refresh, + access: credential.access, + expires: credential.expires, + metadata: { + ...(credential.accountId ? { accountID: credential.accountId } : {}), + ...(credential.enterpriseUrl ? { enterpriseURL: credential.enterpriseUrl } : {}), + }, + }) // kilocode_change end -export class Info extends Schema.Class("Credential.Info")({ - id: ID, - connectorID: ConnectorSchema.ID, - methodID: ConnectorSchema.MethodID, - label: Schema.String, - value: Value, -}) {} - -export const Event = { - Added: EventV2.define({ - type: "credential.added", - schema: { credential: Info }, - }), - Removed: EventV2.define({ - type: "credential.removed", - schema: { credential: Info }, - }), - Switched: EventV2.define({ - type: "credential.switched", - schema: { - connectorID: ConnectorSchema.ID, - from: Schema.optional(ID), - to: Schema.optional(ID), - }, - }), -} - export interface Interface { - readonly get: (id: ID) => Effect.Effect - readonly all: () => Effect.Effect + /** Returns every stored credential. */ + readonly all: () => Effect.Effect + /** Returns stored credentials belonging to one integration. */ + readonly list: (integrationID: IntegrationSchema.ID) => Effect.Effect + /** Replaces any credential for an integration and returns the new record. */ readonly create: (input: { - connectorID: ConnectorSchema.ID - methodID: ConnectorSchema.MethodID - value: Value - label?: string - }) => Effect.Effect - readonly update: (id: ID, updates: Partial>) => Effect.Effect + readonly integrationID: IntegrationSchema.ID + readonly value: Info + readonly label?: string + }) => Effect.Effect + /** Updates the label or secret value of a stored credential. */ + readonly update: (id: ID, updates: Partial>) => Effect.Effect + /** Removes a stored credential. */ readonly remove: (id: ID) => Effect.Effect - readonly activate: (id: ID) => Effect.Effect - readonly active: (connectorID: ConnectorSchema.ID) => Effect.Effect - readonly activeAll: () => Effect.Effect> - readonly forConnector: (connectorID: ConnectorSchema.ID) => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/Credential") {} +// kilocode_change start - preserve Kilo's account JSON stores and reconcile auth.json on every startup export const legacyImportLayer = Layer.effectDiscard( Effect.gen(function* () { const { db } = yield* Database.Service const fs = yield* FSUtil.Service const global = yield* Global.Service - // kilocode_change start - preserve Kilo's multi-account JSON stores before the upstream auth.json fallback - const kiloName = "credential.kilo-account-json" + // v3 repairs the active-only v2 import while remaining safe for users who already ran it. + const kiloName = "credential.kilo-account-json-v3" if (!(yield* db.select().from(DataMigrationTable).where(eq(DataMigrationTable.name, kiloName)).get())) { const current = yield* fs.readJson(path.join(global.data, "account.json")).pipe(Effect.option) const prior = yield* fs.readJson(path.join(global.data, "auth-v2.json")).pipe(Effect.option) const raw = Option.isSome(current) ? current.value : Option.getOrUndefined(prior) - const values = parseKiloAccounts(raw) + const values = parseKiloAccounts(raw).toSorted( + (a, b) => a.connectorID.localeCompare(b.connectorID) || Number(a.active) - Number(b.active), + ) if (values.length > 0) { yield* db.transaction((tx) => Effect.gen(function* () { - const existing = new Set( - (yield* tx.select({ connectorID: CredentialTable.connector_id }).from(CredentialTable).all()).map( - (item) => item.connectorID, - ), - ) - for (const item of values) { - const connector = ConnectorSchema.ID.make(item.connectorID.replace(/\/+$/, "")) - if (existing.has(connector)) continue - const value: Value = - item.credential.type === "api" - ? new Key({ - type: "key", - key: item.credential.key, - metadata: item.credential.metadata, - }) - : new OAuth({ - type: "oauth", - refresh: item.credential.refresh, - access: item.credential.access, - expires: item.credential.expires, - metadata: { - ...(item.credential.accountId ? { accountID: item.credential.accountId } : {}), - ...(item.credential.enterpriseUrl ? { enterpriseURL: item.credential.enterpriseUrl } : {}), - }, - }) + const existing = yield* tx.select().from(CredentialTable).all() + const used = new Set() + const created = Date.now() + for (const [index, item] of values.entries()) { + const integration = IntegrationSchema.ID.make(item.connectorID.replace(/\/+$/, "")) + const value = legacyValue(integration, item.credential) + const current = existing.find( + (row) => + !used.has(row.id) && + row.integration_id === integration && + row.label === item.label && + JSON.stringify(row.value) === JSON.stringify(value), + ) + const time = created + index + if (current) { + used.add(current.id) + yield* tx + .update(CredentialTable) + .set({ time_created: time, time_updated: time }) + .where(eq(CredentialTable.id, current.id)) + .run() + continue + } yield* tx.insert(CredentialTable).values({ - id: ID.create(), - connector_id: connector, - method_id: ConnectorSchema.MethodID.make( - item.credential.type === "api" - ? "api-key" - : connector === ConnectorSchema.ID.make("openai") - ? "chatgpt-browser" - : "oauth", - ), + id: ID.make(`cred_kilo_${Buffer.from(item.id).toString("base64url")}`), + integration_id: integration, label: item.label, value, - active: item.active, + time_created: time, + time_updated: time, }) } yield* tx.insert(DataMigrationTable).values({ name: kiloName, time_completed: Date.now() }).run() @@ -173,108 +171,74 @@ export const legacyImportLayer = Layer.effectDiscard( ) } } - // kilocode_change end const name = "credential.auth-json" const raw = yield* fs.readJson(path.join(global.data, "auth.json")).pipe(Effect.option) if (Option.isNone(raw) || typeof raw.value !== "object" || raw.value === null || Array.isArray(raw.value)) return const decode = Schema.decodeUnknownOption(LegacyValue) - const values = Object.entries(raw.value).flatMap(([connectorID, value]) => { + const values = Object.entries(raw.value).flatMap(([integrationID, value]) => { const decoded = decode(value) if (Option.isNone(decoded)) return [] - const credential = decoded.value - const id = ID.create() - const connector = ConnectorSchema.ID.make(connectorID.replace(/\/+$/, "")) - const methodID = ConnectorSchema.MethodID.make( - credential.type === "api" - ? "api-key" - : connector === ConnectorSchema.ID.make("openai") - ? "chatgpt-browser" - : "oauth", - ) - const next: Value = - credential.type === "api" - ? new Key({ type: "key", key: credential.key, metadata: credential.metadata }) - : new OAuth({ - type: "oauth", - refresh: credential.refresh, - access: credential.access, - expires: credential.expires, - metadata: { - ...(credential.accountId ? { accountID: credential.accountId } : {}), - ...(credential.enterpriseUrl ? { enterpriseURL: credential.enterpriseUrl } : {}), - }, - }) - return [{ id, connectorID: connector, methodID, value: next }] + const integration = IntegrationSchema.ID.make(integrationID.replace(/\/+$/, "")) + return [{ integration, value: legacyValue(integration, decoded.value) }] }) yield* db.transaction((tx) => Effect.gen(function* () { for (const item of values) { - // kilocode_change start - reconcile on every startup so a released client can update auth.json after import. + // reconcile on every startup so a released client can update auth.json after import. const current = yield* tx .select() .from(CredentialTable) - .where(eq(CredentialTable.connector_id, item.connectorID)) - .orderBy(desc(CredentialTable.active), asc(CredentialTable.time_created)) + .where(eq(CredentialTable.integration_id, item.integration)) + .orderBy(desc(CredentialTable.time_created)) // kilocode_change - reconcile the active imported account .get() - yield* tx - .update(CredentialTable) - .set({ active: false }) - .where(eq(CredentialTable.connector_id, item.connectorID)) - .run() if (current) { - yield* tx - .update(CredentialTable) - .set({ method_id: item.methodID, value: item.value, active: true }) - .where(eq(CredentialTable.id, current.id)) - .run() + yield* tx.update(CredentialTable).set({ value: item.value }).where(eq(CredentialTable.id, current.id)).run() continue } yield* tx.insert(CredentialTable).values({ - id: item.id, - connector_id: item.connectorID, - method_id: item.methodID, + id: ID.create(), + integration_id: item.integration, label: "Imported", value: item.value, - active: true, }) - // kilocode_change end } yield* tx.insert(DataMigrationTable).values({ name, time_completed: Date.now() }).onConflictDoNothing().run() }), ) }).pipe(Effect.orDie), ) +// kilocode_change end export const layer = Layer.effect( Service, Effect.gen(function* () { const { db } = yield* Database.Service - const events = yield* EventV2.Service // kilocode_change start const fs = Option.getOrUndefined(yield* Effect.serviceOption(FSUtil.Service)) const global = Option.getOrUndefined(yield* Effect.serviceOption(Global.Service)) // kilocode_change end - const decodeValue = Schema.decodeUnknownSync(Value) - const info = (row: typeof CredentialTable.$inferSelect) => - new Info({ + const decode = Schema.decodeUnknownSync(Info) + const stored = (row: typeof CredentialTable.$inferSelect) => { + if (!row.integration_id) return + return new Stored({ id: row.id, - connectorID: row.connector_id, - methodID: row.method_id, + integrationID: row.integration_id, label: row.label, - value: decodeValue(row.value), + value: decode(row.value), }) + } // kilocode_change start - process-local workspace credentials override host storage without being persisted const content = process.env.KILO_AUTH_CONTENT const injected = yield* content === undefined - ? Effect.succeed(new Map()) + ? Effect.succeed(new Map()) : Effect.try({ try: () => JSON.parse(content) as unknown, catch: (cause) => cause, }).pipe( Effect.flatMap((raw) => { if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { - return Effect.succeed(new Map()) + return Effect.succeed(new Map()) } const decode = Schema.decodeUnknownOption(LegacyAuth) return Effect.succeed( @@ -282,36 +246,15 @@ export const layer = Layer.effect( Object.entries(raw).flatMap(([name, raw]) => { const decoded = decode(raw) if (Option.isNone(decoded) || decoded.value.type === "wellknown") return [] - const credential = decoded.value - const connectorID = ConnectorSchema.ID.make(name.replace(/\/+$/, "")) - const value: Value = - credential.type === "api" - ? new Key({ type: "key", key: credential.key, metadata: credential.metadata }) - : new OAuth({ - type: "oauth", - refresh: credential.refresh, - access: credential.access, - expires: credential.expires, - metadata: { - ...(credential.accountId ? { accountID: credential.accountId } : {}), - ...(credential.enterpriseUrl ? { enterpriseURL: credential.enterpriseUrl } : {}), - }, - }) + const integration = IntegrationSchema.ID.make(name.replace(/\/+$/, "")) return [ [ - connectorID, - new Info({ - id: ID.make(`cred_env_${Buffer.from(connectorID).toString("base64url")}`), - connectorID, - methodID: ConnectorSchema.MethodID.make( - credential.type === "api" - ? "api-key" - : connectorID === ConnectorSchema.ID.make("openai") - ? "chatgpt-browser" - : "oauth", - ), + integration, + new Stored({ + id: ID.make(`cred_env_${Buffer.from(integration).toString("base64url")}`), + integrationID: integration, label: "Environment", - value, + value: legacyValue(integration, decoded.value), }), ] as const, ] @@ -321,16 +264,16 @@ export const layer = Layer.effect( }), Effect.catch((cause) => Effect.logWarning("invalid KILO_AUTH_CONTENT; using no process-local credentials", { cause }).pipe( - Effect.as(new Map()), + Effect.as(new Map()), ), ), ) const isolated = content !== undefined - const local = new Map([...injected.values()].map((credential) => [credential.id, credential])) - const selected = new Map([...injected].map(([connectorID, credential]) => [connectorID, credential.id])) + const local = new Map(injected) + const find = (id: ID) => [...local.values()].find((credential) => credential.id === id) const lock = Semaphore.makeUnsafe(1) - const writeLegacy = (connectorID: ConnectorSchema.ID) => + const writeLegacy = (integration: IntegrationSchema.ID) => lock.withPermit( Effect.gen(function* () { if (!fs || !global || isolated) return @@ -351,14 +294,15 @@ export const layer = Layer.effect( const row = yield* db .select() .from(CredentialTable) - .where(and(eq(CredentialTable.connector_id, connectorID), eq(CredentialTable.active, true))) + .where(eq(CredentialTable.integration_id, integration)) + .orderBy(desc(CredentialTable.time_created)) // kilocode_change - persist the active imported account .get() .pipe(Effect.orDie) - delete data[connectorID + "/"] - if (!row) delete data[connectorID] + delete data[integration + "/"] + if (!row) delete data[integration] else { - const value = decodeValue(row.value) - data[connectorID] = + const value = decode(row.value) + data[integration] = value.type === "key" ? { type: "api", key: value.key, metadata: value.metadata } : { @@ -375,48 +319,7 @@ export const layer = Layer.effect( ) // kilocode_change end - const activate = Effect.fn("Credential.activate")(function* (id: ID) { - // kilocode_change start - isolated credential state remains process-local - if (isolated) { - const credential = local.get(id) - if (!credential) return - const from = selected.get(credential.connectorID) - if (from === id) return - selected.set(credential.connectorID, id) - yield* events.publish(Event.Switched, { connectorID: credential.connectorID, from, to: id }) - return - } - // kilocode_change end - const switched = yield* db - .transaction((tx) => - Effect.gen(function* () { - const credential = yield* tx.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get() - if (!credential || credential.active) return - const current = yield* tx - .select({ id: CredentialTable.id }) - .from(CredentialTable) - .where(and(eq(CredentialTable.connector_id, credential.connector_id), eq(CredentialTable.active, true))) - .get() - yield* tx - .update(CredentialTable) - .set({ active: false }) - .where(eq(CredentialTable.connector_id, credential.connector_id)) - .run() - yield* tx.update(CredentialTable).set({ active: true }).where(eq(CredentialTable.id, id)).run() - return { connectorID: credential.connector_id, from: current?.id, to: id } - }), - ) - .pipe(Effect.orDie) - if (switched) yield* events.publish(Event.Switched, switched) - if (switched) yield* writeLegacy(switched.connectorID) // kilocode_change - }) - return Service.of({ - get: Effect.fn("Credential.get")(function* (id) { - if (isolated) return local.get(id) // kilocode_change - const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie) - return row ? info(row) : undefined - }), all: Effect.fn("Credential.all")(function* () { if (isolated) return [...local.values()] // kilocode_change return (yield* db @@ -424,107 +327,73 @@ export const layer = Layer.effect( .from(CredentialTable) .orderBy(asc(CredentialTable.time_created)) .all() - .pipe(Effect.orDie)).map(info) + .pipe(Effect.orDie)).flatMap((row) => { + const credential = stored(row) + return credential ? [credential] : [] + }) }), - active: Effect.fn("Credential.active")(function* (connectorID) { - if (isolated) return local.get(selected.get(connectorID)!) // kilocode_change - const row = yield* db - .select() - .from(CredentialTable) - .where(and(eq(CredentialTable.connector_id, connectorID), eq(CredentialTable.active, true))) - .get() - .pipe(Effect.orDie) - return row ? info(row) : undefined - }), - activeAll: Effect.fn("Credential.activeAll")(function* () { - // kilocode_change start - project process-local selections without touching host storage + list: Effect.fn("Credential.list")(function* (integrationID) { + // kilocode_change start if (isolated) { - return new Map( - [...selected].flatMap(([connectorID, id]) => { - const credential = local.get(id) - return credential ? [[connectorID, credential] as const] : [] - }), - ) + const credential = local.get(integrationID) + return credential ? [credential] : [] } // kilocode_change end - const rows = yield* db - .select() - .from(CredentialTable) - .where(eq(CredentialTable.active, true)) - .all() - .pipe(Effect.orDie) - return new Map(rows.map((row) => [row.connector_id, info(row)])) - }), - forConnector: Effect.fn("Credential.forConnector")(function* (connectorID) { - if (isolated) return [...local.values()].filter((credential) => credential.connectorID === connectorID) // kilocode_change return (yield* db .select() .from(CredentialTable) - .where(eq(CredentialTable.connector_id, connectorID)) + .where(eq(CredentialTable.integration_id, integrationID)) .orderBy(asc(CredentialTable.time_created)) .all() - .pipe(Effect.orDie)).map(info) + .pipe(Effect.orDie)).flatMap((row) => { + const credential = stored(row) + return credential ? [credential] : [] + }) }), create: Effect.fn("Credential.create")(function* (input) { - const credential = new Info({ + const credential = new Stored({ id: ID.create(), - connectorID: input.connectorID, - methodID: input.methodID, + integrationID: input.integrationID, label: input.label ?? "default", value: input.value, }) - // kilocode_change start - OAuth and key changes in isolated workspaces are process-local + // kilocode_change start - credential changes in isolated workspaces are process-local if (isolated) { - const from = selected.get(credential.connectorID) - local.set(credential.id, credential) - selected.set(credential.connectorID, credential.id) - yield* events.publish(Event.Added, { credential }) - yield* events.publish(Event.Switched, { connectorID: credential.connectorID, from, to: credential.id }) + local.set(credential.integrationID, credential) return credential } // kilocode_change end - const from = yield* db + yield* db .transaction((tx) => Effect.gen(function* () { - const current = yield* tx - .select({ id: CredentialTable.id }) - .from(CredentialTable) - .where(and(eq(CredentialTable.connector_id, input.connectorID), eq(CredentialTable.active, true))) - .get() yield* tx - .update(CredentialTable) - .set({ active: false }) - .where(eq(CredentialTable.connector_id, input.connectorID)) + .delete(CredentialTable) + .where(eq(CredentialTable.integration_id, credential.integrationID)) .run() yield* tx .insert(CredentialTable) .values({ id: credential.id, - connector_id: credential.connectorID, - method_id: credential.methodID, + integration_id: credential.integrationID, label: credential.label, value: credential.value, - active: true, }) .run() - return current?.id }), ) .pipe(Effect.orDie) - yield* events.publish(Event.Added, { credential }) - yield* events.publish(Event.Switched, { connectorID: credential.connectorID, from, to: credential.id }) - yield* writeLegacy(credential.connectorID) // kilocode_change + yield* writeLegacy(credential.integrationID) // kilocode_change return credential }), update: Effect.fn("Credential.update")(function* (id, updates) { if (!updates.label && !updates.value) return // kilocode_change start - isolated updates never reach the host database if (isolated) { - const credential = local.get(id) + const credential = find(id) if (!credential) return local.set( - id, - new Info({ + credential.integrationID, + new Stored({ ...credential, label: updates.label ?? credential.label, value: updates.value ?? credential.value, @@ -540,76 +409,29 @@ export const layer = Layer.effect( .where(eq(CredentialTable.id, id)) .run() .pipe(Effect.orDie) - if (row?.active) yield* writeLegacy(row.connector_id) // kilocode_change + if (row?.integration_id) yield* writeLegacy(row.integration_id) // kilocode_change }), remove: Effect.fn("Credential.remove")(function* (id) { - // kilocode_change start - isolated removals and fallback selection remain process-local + // kilocode_change start - isolated removals remain process-local if (isolated) { - const credential = local.get(id) - if (!credential) return - local.delete(id) - const active = selected.get(credential.connectorID) - const replacement = - active === id ? [...local.values()].find((item) => item.connectorID === credential.connectorID) : undefined - if (active === id) { - if (replacement) selected.set(credential.connectorID, replacement.id) - else selected.delete(credential.connectorID) - } - yield* events.publish(Event.Removed, { credential }) - if (active === id) { - yield* events.publish(Event.Switched, { - connectorID: credential.connectorID, - from: id, - to: replacement?.id, - }) - } + const credential = find(id) + if (credential) local.delete(credential.integrationID) return } + const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie) // kilocode_change end - const removed = yield* db - .transaction((tx) => - Effect.gen(function* () { - const row = yield* tx.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get() - if (!row) return - yield* tx.delete(CredentialTable).where(eq(CredentialTable.id, id)).run() - if (!row.active) return { credential: info(row) } - const replacement = yield* tx - .select() - .from(CredentialTable) - .where(and(eq(CredentialTable.connector_id, row.connector_id), ne(CredentialTable.id, id))) - .orderBy(asc(CredentialTable.time_created)) - .get() - if (replacement) { - yield* tx - .update(CredentialTable) - .set({ active: true }) - .where(eq(CredentialTable.id, replacement.id)) - .run() - } - return { - credential: info(row), - switched: { connectorID: row.connector_id, from: id, to: replacement?.id }, - } - }), - ) - .pipe(Effect.orDie) - if (!removed) return - yield* events.publish(Event.Removed, { credential: removed.credential }) - if (removed.switched) yield* events.publish(Event.Switched, removed.switched) - yield* writeLegacy(removed.credential.connectorID) // kilocode_change + yield* db.delete(CredentialTable).where(eq(CredentialTable.id, id)).run().pipe(Effect.orDie) + if (row?.integration_id) yield* writeLegacy(row.integration_id) // kilocode_change }), - activate, }) }), ) export const defaultLayer = layer.pipe( Layer.provide(Database.defaultLayer), - Layer.provide(EventV2.defaultLayer), // kilocode_change start Layer.provide(FSUtil.defaultLayer), Layer.provide(Global.defaultLayer), - // kilocode_change end Layer.provideMerge( legacyImportLayer.pipe( Layer.provide(Database.defaultLayer), @@ -617,4 +439,5 @@ export const defaultLayer = layer.pipe( Layer.provide(Global.defaultLayer), ), ), + // kilocode_change end ) diff --git a/packages/core/src/credential/sql.ts b/packages/core/src/credential/sql.ts index ff2c50fb083..a849092ea05 100644 --- a/packages/core/src/credential/sql.ts +++ b/packages/core/src/credential/sql.ts @@ -1,23 +1,15 @@ -import { sql } from "drizzle-orm" -import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" import { Timestamps } from "../database/schema.sql" -import type { ConnectorSchema } from "../connector/schema" +import type { IntegrationSchema } from "../integration/schema" import type { Credential } from "../credential" -export const CredentialTable = sqliteTable( - "credential", - { - id: text().$type().primaryKey(), - connector_id: text().$type().notNull(), - method_id: text().$type().notNull(), - label: text().notNull(), - value: text({ mode: "json" }).$type().notNull(), - active: integer({ mode: "boolean" }).notNull().default(false), - ...Timestamps, - }, - (table) => [ - uniqueIndex("credential_connector_active_idx") - .on(table.connector_id) - .where(sql`${table.active} = 1`), - ], -) +export const CredentialTable = sqliteTable("credential", { + id: text().$type().primaryKey(), + integration_id: text().$type(), + label: text().notNull(), + value: text({ mode: "json" }).$type().notNull(), + connector_id: text(), + method_id: text(), + active: integer({ mode: "boolean" }), + ...Timestamps, +}) diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts index 0898339f3da..6cecde15043 100644 --- a/packages/core/src/database/database.ts +++ b/packages/core/src/database/database.ts @@ -7,6 +7,7 @@ import { Global } from "../global" import { Flag } from "../flag/flag" import { isAbsolute, join } from "path" import { existsSync } from "fs" // kilocode_change +import { DbPreflight } from "../kilocode/db-preflight" // kilocode_change import { DatabaseMigration } from "./migration" import { InstallationChannel } from "../installation/version" import { LayerNode } from "../effect/layer-node" @@ -38,6 +39,7 @@ export const layer = Layer.effect( ) export function layerFromPath(filename: string) { + DbPreflight.assertWritable(filename) // kilocode_change - actionable error (and self-heal for kilo-owned files) instead of an opaque wal_checkpoint crash on read-only db files return layer.pipe(Layer.provide(sqliteLayer({ filename }))) } @@ -67,4 +69,5 @@ export const defaultLayer = Layer.unwrap( }), ).pipe(Layer.provide(Global.defaultLayer)) -export const node = LayerNode.make(layerFromPath(path()), []) +// kilocode_change - resolve the database path when the layer builds, not at module evaluation, so KILO_DB overrides set after import (tests, embedded hosts) take effect +export const node = LayerNode.make(Layer.unwrap(Effect.sync(() => layerFromPath(path()))), []) diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index f9b6415c75e..42c40253a87 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -35,6 +35,8 @@ export const migrations = ( import("./migration/20260605003541_add_session_context_snapshot"), import("./migration/20260605042240_add_context_epoch_agent"), import("./migration/20260611035744_credential"), + import("./migration/20260611192811_lush_chimera"), + import("./migration/20260612174303_project_dir_strategy"), import("./migration/20260714141136_session-message-legacy-writer-compat"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration.ts b/packages/core/src/database/migration.ts index dfc445e3ebb..90dee8acbf3 100644 --- a/packages/core/src/database/migration.ts +++ b/packages/core/src/database/migration.ts @@ -4,6 +4,7 @@ import { sql } from "drizzle-orm" import { Effect, Semaphore } from "effect" import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" import { migrations } from "./migration.gen" +import schema from "./schema.gen" type Database = EffectDrizzleSqlite.EffectSQLiteDatabase type Transaction = Parameters[0]>[0] @@ -15,7 +16,28 @@ export type Migration = { } export function apply(db: Database) { - return lock.withPermit(applyOnly(db, migrations)) + return lock.withPermit( + Effect.gen(function* () { + const tables = yield* db.all<{ name: string }>( + sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`, + ) + if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations) + if (tables.length > 0) return yield* Effect.die("Database is not empty and has no session table") + yield* db.transaction((tx) => + Effect.gen(function* () { + yield* schema.up(tx) + yield* tx.run( + sql`CREATE TABLE ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`, + ) + yield* Effect.forEach(migrations, (migration) => + tx.run( + sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`, + ), + ) + }), + ) + }), + ) } export function applyOnly(db: Database, input: Migration[]) { @@ -48,7 +70,7 @@ export function applyOnly(db: Database, input: Migration[]) { if (completed.has(migration.id)) continue yield* db.transaction((tx) => Effect.gen(function* () { - if (!process.env.KILO_SKIP_MIGRATIONS) yield* migration.up(tx) + yield* migration.up(tx) yield* tx.run( sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`, ) diff --git a/packages/core/src/database/migration/20260611192811_lush_chimera.ts b/packages/core/src/database/migration/20260611192811_lush_chimera.ts new file mode 100644 index 00000000000..306b10d5327 --- /dev/null +++ b/packages/core/src/database/migration/20260611192811_lush_chimera.ts @@ -0,0 +1,25 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260611192811_lush_chimera", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS \`credential_connector_active_idx\`;`) + yield* tx.run(`DROP TABLE \`credential\`;`) + yield* tx.run(` + CREATE TABLE \`credential\` ( + \`id\` text PRIMARY KEY, + \`integration_id\` text, + \`label\` text NOT NULL, + \`value\` text NOT NULL, + \`connector_id\` text, + \`method_id\` text, + \`active\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260612174303_project_dir_strategy.ts b/packages/core/src/database/migration/20260612174303_project_dir_strategy.ts new file mode 100644 index 00000000000..1f09c40a707 --- /dev/null +++ b/packages/core/src/database/migration/20260612174303_project_dir_strategy.ts @@ -0,0 +1,29 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260612174303_project_dir_strategy", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE \`project_directory\` ADD \`strategy\` text;`) + yield* tx.run(`PRAGMA foreign_keys=OFF;`) + yield* tx.run(` + CREATE TABLE \`__new_project_directory\` ( + \`project_id\` text NOT NULL, + \`directory\` text NOT NULL, + \`type\` text, + \`strategy\` text, + \`time_created\` integer NOT NULL, + CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`), + CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run( + `INSERT INTO \`__new_project_directory\`(\`project_id\`, \`directory\`, \`type\`, \`time_created\`) SELECT \`project_id\`, \`directory\`, \`type\`, \`time_created\` FROM \`project_directory\`;`, + ) + yield* tx.run(`DROP TABLE \`project_directory\`;`) + yield* tx.run(`ALTER TABLE \`__new_project_directory\` RENAME TO \`project_directory\`;`) + yield* tx.run(`PRAGMA foreign_keys=ON;`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts new file mode 100644 index 00000000000..16611f9d75b --- /dev/null +++ b/packages/core/src/database/schema.gen.ts @@ -0,0 +1,261 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "./migration" + +export default { + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`workspace\` ( + \`id\` text PRIMARY KEY, + \`type\` text NOT NULL, + \`name\` text DEFAULT '' NOT NULL, + \`branch\` text, + \`directory\` text, + \`extra\` text, + \`project_id\` text NOT NULL, + \`time_used\` integer NOT NULL, + CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`data_migration\` ( + \`name\` text PRIMARY KEY, + \`time_completed\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account_state\` ( + \`id\` integer PRIMARY KEY, + \`active_account_id\` text, + \`active_org_id\` text, + CONSTRAINT \`fk_account_state_active_account_id_account_id_fk\` FOREIGN KEY (\`active_account_id\`) REFERENCES \`account\`(\`id\`) ON DELETE SET NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`account\` ( + \`id\` text PRIMARY KEY, + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`control_account\` ( + \`email\` text NOT NULL, + \`url\` text NOT NULL, + \`access_token\` text NOT NULL, + \`refresh_token\` text NOT NULL, + \`token_expiry\` integer, + \`active\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`control_account_pk\` PRIMARY KEY(\`email\`, \`url\`) + ); + `) + yield* tx.run(` + CREATE TABLE \`credential\` ( + \`id\` text PRIMARY KEY, + \`integration_id\` text, + \`label\` text NOT NULL, + \`value\` text NOT NULL, + \`connector_id\` text, + \`method_id\` text, + \`active\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE TABLE \`event_sequence\` ( + \`aggregate_id\` text PRIMARY KEY, + \`seq\` integer NOT NULL, + \`owner_id\` text + ); + `) + yield* tx.run(` + CREATE TABLE \`event\` ( + \`id\` text PRIMARY KEY, + \`aggregate_id\` text NOT NULL, + \`seq\` integer NOT NULL, + \`type\` text NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`permission\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`action\` text NOT NULL, + \`resource\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`project_directory\` ( + \`project_id\` text NOT NULL, + \`directory\` text NOT NULL, + \`type\` text, + \`strategy\` text, + \`time_created\` integer NOT NULL, + CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`), + CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`project\` ( + \`id\` text PRIMARY KEY, + \`worktree\` text NOT NULL, + \`vcs\` text, + \`name\` text, + \`icon_url\` text, + \`icon_url_override\` text, + \`icon_color\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_initialized\` integer, + \`sandboxes\` text NOT NULL, + \`commands\` text + ); + `) + yield* tx.run(` + CREATE TABLE \`message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`part\` ( + \`id\` text PRIMARY KEY, + \`message_id\` text NOT NULL, + \`session_id\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_context_epoch\` ( + \`session_id\` text PRIMARY KEY, + \`baseline\` text NOT NULL, + \`agent\` text DEFAULT 'build' NOT NULL, + \`snapshot\` text NOT NULL, + \`baseline_seq\` integer NOT NULL, + \`replacement_seq\` integer, + \`revision\` integer DEFAULT 0 NOT NULL, + CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_input\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`prompt\` text NOT NULL, + \`delivery\` text NOT NULL, + \`admitted_seq\` integer NOT NULL, + \`promoted_seq\` integer, + \`time_created\` integer NOT NULL, + CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_message\` ( + \`id\` text PRIMARY KEY, + \`session_id\` text NOT NULL, + \`type\` text NOT NULL, + \`seq\` integer, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`data\` text NOT NULL, + CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session\` ( + \`id\` text PRIMARY KEY, + \`project_id\` text NOT NULL, + \`workspace_id\` text, + \`parent_id\` text, + \`slug\` text NOT NULL, + \`directory\` text NOT NULL, + \`path\` text, + \`title\` text NOT NULL, + \`version\` text NOT NULL, + \`share_url\` text, + \`summary_additions\` integer, + \`summary_deletions\` integer, + \`summary_files\` integer, + \`summary_diffs\` text, + \`metadata\` text, + \`cost\` real DEFAULT 0 NOT NULL, + \`tokens_input\` integer DEFAULT 0 NOT NULL, + \`tokens_output\` integer DEFAULT 0 NOT NULL, + \`tokens_reasoning\` integer DEFAULT 0 NOT NULL, + \`tokens_cache_read\` integer DEFAULT 0 NOT NULL, + \`tokens_cache_write\` integer DEFAULT 0 NOT NULL, + \`revert\` text, + \`permission\` text, + \`agent\` text, + \`model\` text, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + \`time_compacting\` integer, + \`time_archived\` integer, + CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`todo\` ( + \`session_id\` text NOT NULL, + \`content\` text NOT NULL, + \`status\` text NOT NULL, + \`priority\` text NOT NULL, + \`position\` integer NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`), + CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(` + CREATE TABLE \`session_share\` ( + \`session_id\` text PRIMARY KEY, + \`id\` text NOT NULL, + \`secret\` text NOT NULL, + \`url\` text NOT NULL, + \`time_created\` integer NOT NULL, + \`time_updated\` integer NOT NULL, + CONSTRAINT \`fk_session_share_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE + ); + `) + yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`) + yield* tx.run(`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`) + 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 \`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\`);`) + yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`) + yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) + yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) + yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) + yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`) + yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`) + }) + }, +} satisfies Omit diff --git a/packages/core/src/effect/layer-node.ts b/packages/core/src/effect/layer-node.ts index c6ee6b23693..ae015930d5a 100644 --- a/packages/core/src/effect/layer-node.ts +++ b/packages/core/src/effect/layer-node.ts @@ -17,7 +17,7 @@ declare const $ErrorType: unique symbol export type Node = { readonly kind: "layer" | "group" readonly implementation?: Layer.Any - readonly dependencies: readonly AnyNode[] + readonly dependencies: readonly AnyNode[] | (() => readonly AnyNode[]) readonly [$OutputType]?: () => A readonly [$ErrorType]?: () => E } @@ -78,7 +78,7 @@ export function buildLayer(node: Node, options?: { readonly replacem visiting.add(node) stack.push(node) try { - const dependencies = node.dependencies.map(visit) + const dependencies = (typeof node.dependencies === "function" ? node.dependencies() : node.dependencies).map(visit) const nonEmpty = dependencies as [RuntimeLayer, ...RuntimeLayer[]] const result = node.kind === "group" @@ -99,4 +99,24 @@ export function buildLayer(node: Node, options?: { readonly replacem return visit(node) as unknown as Layer.Layer } +// kilocode_change start - defer node construction to break circular dependency chains +export function suspend(fn: () => Node): Node { + let cached: Node | undefined + const getNode = () => { + if (!cached) cached = fn() + return cached + } + return { + kind: "layer", + implementation: Layer.suspend( + () => (getNode().implementation ?? Layer.empty) as Layer.Layer, + ), + dependencies: () => { + const raw = getNode().dependencies + return typeof raw === "function" ? raw() : raw + }, + } +} +// kilocode_change end + export * as LayerNode from "./layer-node" diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts new file mode 100644 index 00000000000..64053ee44ca --- /dev/null +++ b/packages/core/src/integration.ts @@ -0,0 +1,567 @@ +export * as Integration from "./integration" + +import { Cause, Clock, Context, Duration, Effect, Exit, Layer, Schedule, Schema, Scope, SynchronizedRef } from "effect" +import { castDraft, enableMapSet, type Draft } from "immer" +import { Credential } from "./credential" +import { IntegrationSchema } from "./integration/schema" +import { withStatics } from "./schema" +import { State } from "./state" +import { Identifier } from "./util/identifier" +import { EventV2 } from "./event" +import { IntegrationConnection } from "./integration/connection" + +export const ID = IntegrationSchema.ID +export type ID = IntegrationSchema.ID + +export const MethodID = IntegrationSchema.MethodID +export type MethodID = IntegrationSchema.MethodID + +export const AttemptID = Schema.String.pipe( + Schema.brand("Integration.AttemptID"), + withStatics((schema) => ({ create: () => schema.make("con_" + Identifier.ascending()) })), +) +export type AttemptID = typeof AttemptID.Type + +export const When = Schema.Struct({ + key: Schema.String, + op: Schema.Literals(["eq", "neq"]), + value: Schema.String, +}).annotate({ identifier: "Integration.When" }) +export type When = typeof When.Type + +export class TextPrompt extends Schema.Class("Integration.TextPrompt")({ + type: Schema.Literal("text"), + key: Schema.String, + message: Schema.String, + placeholder: Schema.optional(Schema.String), + when: Schema.optional(When), +}) {} + +export class SelectPrompt extends Schema.Class("Integration.SelectPrompt")({ + type: Schema.Literal("select"), + key: Schema.String, + message: Schema.String, + options: Schema.Array( + Schema.Struct({ + label: Schema.String, + value: Schema.String, + hint: Schema.optional(Schema.String), + }), + ), + when: Schema.optional(When), +}) {} + +export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type")) +export type Prompt = typeof Prompt.Type + +export class OAuthMethod extends Schema.Class("Integration.OAuthMethod")({ + id: MethodID, + type: Schema.Literal("oauth"), + label: Schema.String, + prompts: Schema.optional(Schema.Array(Prompt)), +}) {} + +export class KeyMethod extends Schema.Class("Integration.KeyMethod")({ + type: Schema.Literal("key"), + label: Schema.optional(Schema.String), +}) {} + +export class EnvMethod extends Schema.Class("Integration.EnvMethod")({ + type: Schema.Literal("env"), + names: Schema.Array(Schema.String), +}) {} + +export const Method = Schema.Union([OAuthMethod, KeyMethod, EnvMethod]).pipe(Schema.toTaggedUnion("type")) +export type Method = typeof Method.Type + +export class Info extends Schema.Class("Integration.Info")({ + id: ID, + name: Schema.String, + methods: Schema.Array(Method), + connections: Schema.Array(IntegrationConnection.Info), +}) {} + +export type Inputs = Readonly<{ [key: string]: string }> + +export type OAuthAuthorization = { + readonly url: string + readonly instructions: string +} & ( + | { + readonly mode: "auto" + readonly callback: Effect.Effect + } + | { + readonly mode: "code" + readonly callback: (code: string) => Effect.Effect + } +) + +export interface OAuthImplementation { + readonly integrationID: ID + readonly method: OAuthMethod + readonly authorize: (inputs: Inputs) => Effect.Effect + readonly refresh?: (credential: Credential.OAuth) => Effect.Effect +} + +export interface KeyImplementation { + readonly integrationID: ID + readonly method: KeyMethod +} + +export interface EnvImplementation { + readonly integrationID: ID + readonly method: EnvMethod +} + +export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation + +function isOAuthImplementation(implementation: Implementation): implementation is OAuthImplementation { + return implementation.method.type === "oauth" +} + +export class Attempt extends Schema.Class("Integration.Attempt")({ + attemptID: AttemptID, + url: Schema.String, + instructions: Schema.String, + mode: Schema.Literals(["auto", "code"]), + time: Schema.Struct({ + created: Schema.Number, + expires: Schema.Number, + }), +}) {} + +const Time = Schema.Struct({ + created: Schema.Number, + expires: Schema.Number, +}) + +export const AttemptStatus = Schema.Union([ + Schema.Struct({ status: Schema.Literal("pending"), time: Time }), + Schema.Struct({ status: Schema.Literal("complete"), time: Time }), + Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: Time }), + Schema.Struct({ status: Schema.Literal("expired"), time: Time }), +]).pipe(Schema.toTaggedUnion("status")) +export type AttemptStatus = typeof AttemptStatus.Type + +export class CodeRequiredError extends Schema.TaggedErrorClass()("Integration.CodeRequired", { + attemptID: AttemptID, +}) {} + +export class AuthorizationError extends Schema.TaggedErrorClass()("Integration.Authorization", { + cause: Schema.Defect, +}) {} + +export type Error = CodeRequiredError | AuthorizationError + +export const Event = { + Updated: EventV2.define({ + type: "integration.updated", + schema: {}, + }), +} + +export type Ref = { + id: ID + name: string +} + +type Entry = { + ref: Ref + methods: Method[] + implementations: Map +} + +type Data = { + integrations: Map +} + +export type Editor = { + list: () => readonly Ref[] + get: (id: ID) => Ref | undefined + update: (id: ID, update: (integration: Draft) => void) => void + remove: (id: ID) => void + method: { + list: (integrationID: ID) => readonly Method[] + update: (implementation: Implementation) => void + remove: (integrationID: ID, method: Method) => void + } +} + +export interface Interface { + /** Registers a scoped transform over the integration registry. */ + readonly transform: State.Interface["transform"] + /** Registers and immediately applies a scoped integration registry update. */ + readonly update: State.Interface["update"] + /** Returns one integration with its methods and current connections. */ + readonly get: (id: ID) => Effect.Effect + /** Returns all integrations with their methods and current connections. */ + readonly list: () => Effect.Effect + readonly connect: { + /** Runs a key method and stores the resulting credential. */ + readonly key: (input: { + /** Integration receiving the credential. */ + readonly integrationID: ID + /** Secret entered by the user. */ + readonly key: string + /** User-facing label for the stored credential. */ + readonly label?: string + }) => Effect.Effect + /** Starts a stateful OAuth attempt. */ + readonly oauth: (input: { + /** Integration being authenticated. */ + readonly integrationID: ID + /** OAuth method selected by the caller. */ + readonly methodID: MethodID + /** Answers to the method's optional prompts. */ + readonly inputs: Inputs + /** User-facing label for the credential created on completion. */ + readonly label?: string + }) => Effect.Effect + } + readonly attempt: { + /** Returns the current state of an OAuth attempt. */ + readonly status: (attemptID: AttemptID) => Effect.Effect + /** Completes the attempt and stores its credential. */ + readonly complete: (input: { + /** Opaque handle returned by `oauth`. */ + readonly attemptID: AttemptID + /** Authorization code required by attempts in code mode. */ + readonly code?: string + }) => Effect.Effect + /** Cancels an attempt and releases its resources. */ + readonly cancel: (attemptID: AttemptID) => Effect.Effect + } +} + +export class Service extends Context.Service()("@opencode/v2/Integration") {} + +enableMapSet() + +const attemptLifetime = Duration.toMillis(Duration.minutes(10)) +const terminalRetention = Duration.toMillis(Duration.minutes(1)) +const scrubInterval = Duration.seconds(30) +const settlementTimeout = Duration.seconds(30) // kilocode_change - bound retained OAuth attempt secrets + +type AttemptTime = { created: number; expires: number } +type PendingAttempt = { + status: "pending" + completing: boolean + settling: boolean // kilocode_change - cancellation and expiry cannot overtake credential persistence + authorization: OAuthAuthorization + integrationID: ID + methodID: MethodID + label?: string + scope: Scope.Closeable + time: AttemptTime +} +type TerminalAttempt = { + status: "complete" | "failed" | "expired" + message?: string + removeAt: number + time: AttemptTime +} +type AttemptEntry = PendingAttempt | TerminalAttempt + +export const locationLayer = Layer.effect( + Service, + Effect.gen(function* () { + const credentials = yield* Credential.Service + const events = yield* EventV2.Service + const scope = yield* Scope.Scope + const attempts = SynchronizedRef.makeUnsafe(new Map()) + const state = State.create({ + initial: () => ({ integrations: new Map() }), + editor: (draft) => ({ + list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[], + get: (id) => draft.integrations.get(id)?.ref as Ref | undefined, + update: (id, update) => { + const current = + draft.integrations.get(id) ?? + castDraft({ ref: { id, name: id } as Ref, methods: [], implementations: new Map() }) + if (!draft.integrations.has(id)) draft.integrations.set(id, current) + update(current.ref) + current.ref.id = id + }, + remove: (id) => draft.integrations.delete(id), + method: { + list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [], + update: (implementation) => { + const current = + draft.integrations.get(implementation.integrationID) ?? + castDraft({ + ref: { + id: implementation.integrationID, + name: implementation.integrationID, + } as Ref, + methods: [], + implementations: new Map(), + }) + if (!draft.integrations.has(implementation.integrationID)) { + draft.integrations.set(implementation.integrationID, current) + } + const index = current.methods.findIndex((method) => { + if (method.type !== implementation.method.type) return false + if (method.type !== "oauth" || implementation.method.type !== "oauth") return true + return method.id === implementation.method.id + }) + if (index === -1) current.methods.push(castDraft(implementation.method)) + else current.methods[index] = castDraft(implementation.method) + if (isOAuthImplementation(implementation)) { + current.implementations.set(implementation.method.id, castDraft(implementation)) + } + }, + remove: (integrationID, method) => { + const current = draft.integrations.get(integrationID) + if (!current) return + const index = current.methods.findIndex((candidate) => { + if (candidate.type !== method.type) return false + if (candidate.type !== "oauth" || method.type !== "oauth") return true + return candidate.id === method.id + }) + if (index !== -1) current.methods.splice(index, 1) + if (method.type === "oauth") current.implementations.delete(method.id) + }, + }, + }), + finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid), + }) + + const connections = (entry: Entry, saved: readonly Credential.Stored[]): IntegrationConnection.Info[] => { + const connected = saved.map( + (credential) => + new IntegrationConnection.CredentialInfo({ type: "credential", id: credential.id, label: credential.label }), + ) + const detected = entry.methods + .filter((method) => method.type === "env") + .flatMap((method) => method.names.filter((name) => process.env[name])) + .map( + (name, index) => + new IntegrationConnection.EnvInfo({ + type: "env", + name, + }), + ) + return [...connected, ...detected] + } + + const project = (entry: Entry, saved: readonly Credential.Stored[]) => + new Info({ + id: entry.ref.id, + name: entry.ref.name, + methods: entry.methods, + connections: connections(entry, saved), + }) + + const authorize = (effect: Effect.Effect) => + effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause }))) + + const close = (attemptScope: Scope.Closeable) => + Scope.close(attemptScope, Exit.void).pipe(Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid) + + const message = (cause: Cause.Cause) => { + const error = Cause.squash(cause) + return error instanceof Error ? error.message : String(error) + } + + // kilocode_change start - persist before exposing completion and make settlement atomic with cancellation + const settle = Effect.fnUntraced(function* ( + attemptID: AttemptID, + exit: Exit.Exit, + owned = false, + ) { + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const pending = yield* SynchronizedRef.modify(attempts, (current) => { + const attempt = current.get(attemptID) + if (!attempt || attempt.status !== "pending") return [undefined, current] + if (owned) return attempt.settling ? [attempt, current] : [undefined, current] + if (attempt.settling) return [undefined, current] + return [attempt, new Map(current).set(attemptID, { ...attempt, settling: true })] + }) + if (!pending) return + const settled = Exit.isSuccess(exit) + ? yield* restore( + credentials + .create({ + integrationID: pending.integrationID, + label: pending.label, + value: + exit.value.type === "oauth" + ? new Credential.OAuth({ ...exit.value, methodID: pending.methodID }) + : exit.value, + }) + .pipe( + Effect.timeout(settlementTimeout), + Effect.mapError((cause) => new AuthorizationError({ cause })), + ), + ).pipe(Effect.asVoid, Effect.exit) + : Exit.failCause(exit.cause) + const now = yield* Clock.currentTimeMillis + const result = yield* SynchronizedRef.modify(attempts, (current) => { + const attempt = current.get(attemptID) + if (!attempt || attempt.status !== "pending") return [undefined, current] + const terminal: TerminalAttempt = Exit.isSuccess(settled) + ? { status: "complete", time: attempt.time, removeAt: now + terminalRetention } + : { + status: "failed", + message: message(settled.cause), + time: attempt.time, + removeAt: now + terminalRetention, + } + return [attempt, new Map(current).set(attemptID, terminal)] + }) + if (!result) return settled + yield* close(result.scope) + return settled + }), + ) + }) + // kilocode_change end + + const scrub = Effect.fnUntraced(function* () { + const now = yield* Clock.currentTimeMillis + const expired = yield* SynchronizedRef.modify(attempts, (current) => { + const next = new Map(current) + const scopes: Scope.Closeable[] = [] + for (const [id, attempt] of current) { + if (attempt.status === "pending" && !attempt.settling && attempt.time.expires <= now) { // kilocode_change + scopes.push(attempt.scope) + next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention }) + continue + } + if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id) + } + return [scopes, next] + }) + yield* Effect.forEach(expired, close, { discard: true }) + }) + + yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope)) + + return Service.of({ + transform: state.transform, + update: state.update, + get: Effect.fn("Integration.get")(function* (id) { + const entry = state.get().integrations.get(id) + if (!entry) return undefined + return project(entry, yield* credentials.list(id)) + }), + list: Effect.fn("Integration.list")(function* () { + return (yield* Effect.forEach(state.get().integrations.values(), (entry) => + Effect.gen(function* () { + return project(entry, yield* credentials.list(entry.ref.id)) + }), + )).toSorted((a, b) => a.name.localeCompare(b.name)) + }), + connect: { + key: Effect.fn("Integration.connect.key")(function* (input) { + const method = state + .get() + .integrations.get(input.integrationID) + ?.methods.some((method) => method.type === "key") + if (!method) return yield* Effect.die(`Key method not found: ${input.integrationID}`) + yield* credentials.create({ + integrationID: input.integrationID, + label: input.label, + value: new Credential.Key({ type: "key", key: input.key }), + }) + }), + oauth: Effect.fn("Integration.connect.oauth")(function* (input) { + const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID) + if (!method) { + return yield* Effect.die(`OAuth method not found: ${input.integrationID}/${input.methodID}`) + } + const attemptScope = yield* Scope.fork(scope) + const authorization = yield* authorize(method.authorize(input.inputs)).pipe( + Scope.provide(attemptScope), + Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)), + ) + const id = AttemptID.create() + const created = yield* Clock.currentTimeMillis + const time = { created, expires: created + attemptLifetime } + yield* SynchronizedRef.update(attempts, (current) => + new Map(current).set(id, { + status: "pending", + completing: authorization.mode === "auto", + settling: false, // kilocode_change + authorization, + integrationID: input.integrationID, + methodID: input.methodID, + label: input.label, + scope: attemptScope, + time, + }), + ) + if (authorization.mode === "auto") { + // kilocode_change start - settle persistence atomically with cancellation + yield* authorize(authorization.callback).pipe( + Effect.exit, + Effect.flatMap((exit) => settle(id, exit)), + Effect.forkIn(attemptScope, { startImmediately: true }), + ) + // kilocode_change end + } + return new Attempt({ + attemptID: id, + url: authorization.url, + instructions: authorization.instructions, + mode: authorization.mode, + time, + }) + }), + }, + attempt: { + status: Effect.fn("Integration.attempt.status")(function* (attemptID) { + const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID) + if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`) + if (attempt.status === "failed") { + return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time } + } + return { status: attempt.status, time: attempt.time } + }), + complete: Effect.fn("Integration.attempt.complete")(function* (input) { + const attempt = yield* SynchronizedRef.modify(attempts, (current) => { + const match = current.get(input.attemptID) + if (!match || match.status !== "pending" || match.completing) return [match, current] + if (match.authorization.mode === "code" && input.code === undefined) return [match, current] + return [match, new Map(current).set(input.attemptID, { ...match, completing: true, settling: true })] // kilocode_change + }) + if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`) + if (attempt.status !== "pending") return + if (attempt.authorization.mode === "code" && input.code === undefined) { + return yield* new CodeRequiredError({ attemptID: input.attemptID }) + } + if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`) + const callback = + attempt.authorization.mode === "auto" + ? attempt.authorization.callback + : attempt.authorization.callback(input.code as string) + // kilocode_change start - an interrupted or timed-out callback still settles and releases its attempt. + return yield* Effect.uninterruptibleMask((restore) => + Effect.gen(function* () { + const exit = yield* restore(authorize(callback)).pipe( + Effect.timeout(settlementTimeout), + Effect.mapError((cause) => new AuthorizationError({ cause })), + Effect.exit, + ) + const settled = yield* settle(input.attemptID, exit, true) + if (settled && Exit.isFailure(settled)) return yield* settled + }), + ) + // kilocode_change end + }), + cancel: Effect.fn("Integration.attempt.cancel")(function* (attemptID) { + const attempt = yield* SynchronizedRef.modify(attempts, (current) => { + const match = current.get(attemptID) + if (!match || match.status !== "pending" || match.settling) return [undefined, current] // kilocode_change + const next = new Map(current) + next.delete(attemptID) + return [match, next] + }) + if (attempt) yield* Scope.close(attempt.scope, Exit.void) + }), + }, + }) + }), +) diff --git a/packages/core/src/integration/connection.ts b/packages/core/src/integration/connection.ts new file mode 100644 index 00000000000..a190ebf78c0 --- /dev/null +++ b/packages/core/src/integration/connection.ts @@ -0,0 +1,20 @@ +export * as IntegrationConnection from "./connection" + +import { Schema } from "effect" +import { Credential } from "../credential" + +export class CredentialInfo extends Schema.Class("Connection.CredentialInfo")({ + type: Schema.Literal("credential"), + id: Credential.ID, + label: Schema.String, +}) {} + +export class EnvInfo extends Schema.Class("Connection.EnvInfo")({ + type: Schema.Literal("env"), + name: Schema.String, +}) {} + +export const Info = Schema.Union([CredentialInfo, EnvInfo]) + .pipe(Schema.toTaggedUnion("type")) + .annotate({ identifier: "Connection.Info" }) +export type Info = typeof Info.Type diff --git a/packages/core/src/integration/schema.ts b/packages/core/src/integration/schema.ts new file mode 100644 index 00000000000..472f4e0609c --- /dev/null +++ b/packages/core/src/integration/schema.ts @@ -0,0 +1,9 @@ +export * as IntegrationSchema from "./schema" + +import { Schema } from "effect" + +export const ID = Schema.String.pipe(Schema.brand("Integration.ID")) +export type ID = typeof ID.Type + +export const MethodID = Schema.String.pipe(Schema.brand("Integration.MethodID")) +export type MethodID = typeof MethodID.Type diff --git a/packages/core/src/kilocode/credential-migration.ts b/packages/core/src/kilocode/credential-migration.ts index 387c9dce0f6..590643ceb67 100644 --- a/packages/core/src/kilocode/credential-migration.ts +++ b/packages/core/src/kilocode/credential-migration.ts @@ -37,6 +37,7 @@ export function parse(input: unknown) { const fallback = !first.has(account.serviceID) first.add(account.serviceID) return { + id: account.id, connectorID: account.serviceID, label: account.description, credential: account.credential, diff --git a/packages/core/src/kilocode/db-preflight.ts b/packages/core/src/kilocode/db-preflight.ts new file mode 100644 index 00000000000..4f211980804 --- /dev/null +++ b/packages/core/src/kilocode/db-preflight.ts @@ -0,0 +1,68 @@ +export * as DbPreflight from "./db-preflight" + +import { accessSync, chmodSync, constants, statSync } from "fs" +import path from "path" +import { Global } from "../global" +import { Log } from "../util/log" + +const log = Log.create({ service: "db-preflight" }) + +function writable(target: string) { + try { + accessSync(target, constants.W_OK) + return true + } catch { + return false + } +} + +function exists(target: string) { + try { + statSync(target) + return true + } catch { + return false + } +} + +// Startup runs `PRAGMA wal_checkpoint(PASSIVE)`, which must write the database and its +// WAL sidecars. A stray read-only file otherwise kills the process deep inside Effect +// with an opaque "attempt to write a readonly database". +export function assertWritable(filename: string, trusted: string = Global.Path.data) { + if (!filename || filename === ":memory:" || filename.startsWith("file:")) return + const dir = path.dirname(filename) + const owned = path.resolve(dir) === path.resolve(trusted) + let missing = false + for (const file of [filename, `${filename}-wal`, `${filename}-shm`]) { + if (!exists(file)) { + missing = true + continue + } + if (writable(file)) continue + let cause: unknown + if (owned) { + // chmod only succeeds for files the current user owns, which is exactly the safe repair scope + try { + chmodSync(file, statSync(file).mode | 0o600) + } catch (err) { + cause = err + } + if (writable(file)) { + // visible trail: if files keep losing their write bit, something outside kilo is doing it + log.warn("repaired read-only database file", { file }) + continue + } + } + throw new Error( + `Database file is not writable: ${file}. Fix its permissions (chmod u+w "${file}") or point KILO_DB at a writable location.`, + cause === undefined ? undefined : { cause }, + ) + } + if (missing && !writable(dir)) { + if (!exists(dir)) + throw new Error(`Database directory does not exist: ${dir}. Create it or point KILO_DB at an existing location.`) + throw new Error( + `Database directory is not writable: ${dir}. SQLite must create WAL files next to the database. Fix its permissions or point KILO_DB at a writable location.`, + ) + } +} diff --git a/packages/core/src/location-layer.ts b/packages/core/src/location-layer.ts index 39a3948a596..cdaefe2cff1 100644 --- a/packages/core/src/location-layer.ts +++ b/packages/core/src/location-layer.ts @@ -4,16 +4,19 @@ import { Policy } from "./policy" import { Config } from "./config" import { PluginV2 } from "./plugin" import { Catalog } from "./catalog" -import { Connector } from "./connector" +import { Integration } from "./integration" import { CommandV2 } from "./command" import { AgentV2 } from "./agent" import { PluginBoot } from "./plugin/boot" import { Project } from "./project" +import { ProjectCopy } from "./project/copy" +import { ProjectDirectories } from "./project/directories" import { EventV2 } from "./event" import { Credential } from "./credential" import { Npm } from "./npm" import { ModelsDev } from "./models-dev" import { FSUtil } from "./fs-util" +import { Git } from "./git" import { Global } from "./global" import { Database } from "./database/database" import { PermissionV2 } from "./permission" @@ -59,10 +62,11 @@ export class LocationServiceMap extends LayerMap.Service()(" Reference.locationLayer, PluginV2.locationLayer, Catalog.locationLayer, - Connector.locationLayer, + Integration.locationLayer, CommandV2.locationLayer, AgentV2.locationLayer, PluginBoot.locationLayer, + ProjectCopy.locationLayer, FileSystem.locationLayer, Watcher.locationLayer, Pty.locationLayer, @@ -98,6 +102,11 @@ export class LocationServiceMap extends LayerMap.Service()(" Layer.provide(skillGuidance), Layer.provide(referenceGuidance), ) + + // Kick off a background project copy refresh to update locations now that we + // have a location + const projectCopyRefresh = Layer.effectDiscard(ProjectCopy.refreshAfterBoot).pipe(Layer.provide(services)) + return Layer.mergeAll( boot, services, @@ -110,6 +119,7 @@ export class LocationServiceMap extends LayerMap.Service()(" runner, builtInTools, referenceGuidance, + projectCopyRefresh, ).pipe(Layer.fresh) }, idleTimeToLive: "60 minutes", @@ -120,10 +130,12 @@ export class LocationServiceMap extends LayerMap.Service()(" Npm.defaultLayer, ModelsDev.defaultLayer, FSUtil.defaultLayer, + Git.defaultLayer, AppProcess.defaultLayer, Global.defaultLayer, Ripgrep.defaultLayer, Database.defaultLayer, + ProjectDirectories.defaultLayer, SessionStore.layer.pipe(Layer.provide(Database.defaultLayer)), PermissionSaved.defaultLayer, RepositoryCache.defaultLayer, diff --git a/packages/core/src/plugin/boot.ts b/packages/core/src/plugin/boot.ts index 6235fe9af26..75782714098 100644 --- a/packages/core/src/plugin/boot.ts +++ b/packages/core/src/plugin/boot.ts @@ -2,7 +2,7 @@ export * as PluginBoot from "./boot" import { Context, Deferred, Effect, Layer } from "effect" import { Credential } from "../credential" -import { Connector } from "../connector" +import { Integration } from "../integration" import { AgentV2 } from "../agent" import { Catalog } from "../catalog" import { CommandV2 } from "../command" @@ -33,7 +33,7 @@ type Plugin = { | Catalog.Service | CommandV2.Service | Credential.Service - | Connector.Service + | Integration.Service | AgentV2.Service | Npm.Service | EventV2.Service @@ -61,7 +61,7 @@ export const layer = Layer.effect( const commands = yield* CommandV2.Service const plugin = yield* PluginV2.Service const credentials = yield* Credential.Service - const connectors = yield* Connector.Service + const integrations = yield* Integration.Service const agents = yield* AgentV2.Service const config = yield* Config.Service const location = yield* Location.Service @@ -81,7 +81,7 @@ export const layer = Layer.effect( Effect.provideService(Catalog.Service, catalog), Effect.provideService(CommandV2.Service, commands), Effect.provideService(Credential.Service, credentials), - Effect.provideService(Connector.Service, connectors), + Effect.provideService(Integration.Service, integrations), Effect.provideService(AgentV2.Service, agents), Effect.provideService(Config.Service, config), Effect.provideService(Location.Service, location), @@ -126,7 +126,7 @@ export const layer = Layer.effect( ) export const locationLayer = layer.pipe( - Layer.provideMerge(Connector.locationLayer), + Layer.provideMerge(Integration.locationLayer), Layer.provideMerge(Catalog.locationLayer), Layer.provideMerge(CommandV2.locationLayer), Layer.provideMerge(Config.locationLayer), diff --git a/packages/core/src/plugin/models-dev.ts b/packages/core/src/plugin/models-dev.ts index b406a1aaf02..e34c11f7889 100644 --- a/packages/core/src/plugin/models-dev.ts +++ b/packages/core/src/plugin/models-dev.ts @@ -1,7 +1,6 @@ import { DateTime, Effect, Scope, Stream } from "effect" import { Catalog } from "../catalog" -import { Connector } from "../connector" -import { Credential } from "../credential" +import { Integration } from "../integration" import { EventV2 } from "../event" import { ModelV2 } from "../model" import { ModelRequest } from "../model-request" @@ -56,27 +55,31 @@ export const ModelsDevPlugin = PluginV2.define({ id: PluginV2.ID.make("models-dev"), effect: Effect.gen(function* () { const catalog = yield* Catalog.Service - const connectors = yield* Connector.Service + const integrations = yield* Integration.Service const modelsDev = yield* ModelsDev.Service const events = yield* EventV2.Service const scope = yield* Scope.Scope const transform = yield* catalog.transform() - const connectorTransform = yield* connectors.transform() + const integrationTransform = yield* integrations.transform() const refresh = Effect.fn("ModelsDevPlugin.refresh")(function* () { const data = yield* modelsDev.get() - yield* connectorTransform((connectors) => { + yield* integrationTransform((integrations) => { for (const item of Object.values(data)) { if (item.env.length === 0) continue - const connectorID = Connector.ID.make(item.id) - connectors.update(connectorID, (connector) => (connector.name = item.name)) - connectors.method.update({ - connectorID, - method: new Connector.KeyMethod({ - id: Connector.MethodID.make("api-key"), + const integrationID = Integration.ID.make(item.id) + integrations.update(integrationID, (integration) => (integration.name = item.name)) + integrations.method.update({ + integrationID, + method: new Integration.KeyMethod({ type: "key", - label: "API Key", }), - authorize: (key: string) => Effect.succeed(new Credential.Key({ type: "key", key })), + }) + integrations.method.update({ + integrationID, + method: new Integration.EnvMethod({ + type: "env", + names: [...item.env], + }), }) } }) diff --git a/packages/core/src/plugin/provider/openai-auth.ts b/packages/core/src/plugin/provider/openai-auth.ts index 61fea09f3c7..c28757ebdf8 100644 --- a/packages/core/src/plugin/provider/openai-auth.ts +++ b/packages/core/src/plugin/provider/openai-auth.ts @@ -1,6 +1,6 @@ import { createServer } from "node:http" import { Deferred, Effect } from "effect" -import { Connector } from "../../connector" +import { Integration } from "../../integration" import { Credential } from "../../credential" import { InstallationVersion } from "../../installation/version" @@ -27,10 +27,13 @@ type Claims = { "https://api.openai.com/auth"?: { chatgpt_account_id?: string } } +const browserMethodID = Integration.MethodID.make("chatgpt-browser") +const headlessMethodID = Integration.MethodID.make("chatgpt-headless") + export const browser = { - connectorID: Connector.ID.make("openai"), - method: new Connector.OAuthMethod({ - id: Connector.MethodID.make("chatgpt-browser"), + integrationID: Integration.ID.make("openai"), + method: new Integration.OAuthMethod({ + id: browserMethodID, type: "oauth", label: "ChatGPT Pro/Plus (browser)", }), @@ -83,17 +86,17 @@ export const browser = { instructions: "Complete authorization in your browser. This window will close automatically.", callback: Deferred.await(code).pipe( Effect.flatMap((value) => exchange(value, redirect, pkce)), - Effect.map(credential), + Effect.map((tokens) => credential(browserMethodID, tokens)), ), } }), refresh: (value) => refresh(value), -} satisfies Connector.OAuthImplementation +} satisfies Integration.OAuthImplementation export const headless = { - connectorID: Connector.ID.make("openai"), - method: new Connector.OAuthMethod({ - id: Connector.MethodID.make("chatgpt-headless"), + integrationID: Integration.ID.make("openai"), + method: new Integration.OAuthMethod({ + id: headlessMethodID, type: "oauth", label: "ChatGPT Pro/Plus (headless)", }), @@ -130,6 +133,7 @@ export const headless = { code_verifier: string } return credential( + headlessMethodID, yield* exchange(data.authorization_code, `${issuer}/deviceauth/callback`, { verifier: data.code_verifier, challenge: "", @@ -145,7 +149,7 @@ export const headless = { } }), refresh: (value) => refresh(value), -} satisfies Connector.OAuthImplementation +} satisfies Integration.OAuthImplementation function headers(contentType: string) { return { "Content-Type": contentType, "User-Agent": `kilo/${InstallationVersion}` } // kilocode_change @@ -176,7 +180,7 @@ function refresh(value: Credential.OAuth) { }).toString(), }).pipe( Effect.map((tokens) => { - const next = credential(tokens) + const next = credential(value.methodID, tokens) return new Credential.OAuth({ ...next, metadata: next.metadata ?? value.metadata, @@ -196,10 +200,11 @@ function request(url: string, init: RequestInit) { }) } -function credential(tokens: TokenResponse) { +function credential(methodID: Integration.MethodID, tokens: TokenResponse) { const accountID = extractAccountID(tokens) return new Credential.OAuth({ type: "oauth", + methodID, refresh: tokens.refresh_token, access: tokens.access_token, expires: Date.now() + (tokens.expires_in ?? 3600) * 1000, diff --git a/packages/core/src/plugin/provider/openai.ts b/packages/core/src/plugin/provider/openai.ts index 1142a39b4f0..d58bd784f52 100644 --- a/packages/core/src/plugin/provider/openai.ts +++ b/packages/core/src/plugin/provider/openai.ts @@ -2,14 +2,14 @@ import { Effect } from "effect" import { ModelV2 } from "../../model" import { PluginV2 } from "../../plugin" import { ProviderV2 } from "../../provider" -import { Connector } from "../../connector" +import { Integration } from "../../integration" import { browser, headless } from "./openai-auth" export const OpenAIPlugin = PluginV2.define({ id: PluginV2.ID.make("openai"), effect: Effect.gen(function* () { - const connectors = yield* Connector.Service - yield* connectors.update((editor) => { + const integrations = yield* Integration.Service + yield* integrations.update((editor) => { editor.method.update(browser) editor.method.update(headless) }) diff --git a/packages/core/src/plugin/provider/snowflake-cortex.ts b/packages/core/src/plugin/provider/snowflake-cortex.ts index 8dcabf26b13..0971f3518d1 100644 --- a/packages/core/src/plugin/provider/snowflake-cortex.ts +++ b/packages/core/src/plugin/provider/snowflake-cortex.ts @@ -70,14 +70,17 @@ export const SnowflakeCortexPlugin = PluginV2.define({ return { "aisdk.sdk": Effect.fn(function* (evt) { if (evt.model.providerID !== ProviderV2.ID.make("snowflake-cortex")) return - const pat = - process.env.SNOWFLAKE_CORTEX_PAT ?? (typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined) + const token = + process.env.SNOWFLAKE_CORTEX_TOKEN ?? + process.env.SNOWFLAKE_CORTEX_PAT ?? + (typeof evt.options.token === "string" ? evt.options.token : undefined) ?? + (typeof evt.options.apiKey === "string" ? evt.options.apiKey : undefined) const upstream = typeof evt.options.fetch === "function" ? (evt.options.fetch as FetchLike) : undefined if (evt.options.includeUsage !== false) evt.options.includeUsage = true const mod = yield* Effect.promise(() => import("@ai-sdk/openai-compatible")) evt.sdk = mod.createOpenAICompatible({ ...evt.options, - ...(pat ? { apiKey: pat } : {}), + ...(token ? { apiKey: token } : {}), fetch: cortexFetch(upstream) as typeof fetch, } as any) }), diff --git a/packages/core/src/project.ts b/packages/core/src/project.ts index 41c78ee92f8..4f941930986 100644 --- a/packages/core/src/project.ts +++ b/packages/core/src/project.ts @@ -2,60 +2,41 @@ export * as ProjectV2 from "./project" export * as Project from "./project" import { Context, Effect, Layer, Schema } from "effect" -import { asc, desc, eq } from "drizzle-orm" import path from "path" -import { AbsolutePath, withStatics } from "./schema" +import { AbsolutePath } from "./schema" import { FSUtil } from "./fs-util" -import { Database } from "./database/database" import { Git } from "./git" import { LayerNode } from "./effect/layer-node" import { Hash } from "./util/hash" -import { ProjectDirectoryTable } from "./project/sql" +import { ProjectDirectories } from "./project/directories" +import { ProjectSchema } from "./project/schema" -export const ID = Schema.String.pipe( - Schema.brand("Project.ID"), - withStatics((schema) => ({ - global: schema.make("global"), - })), -) -export type ID = typeof ID.Type +export const ID = ProjectSchema.ID +export type ID = ProjectSchema.ID -export const Vcs = Schema.Union([ - Schema.Struct({ - type: Schema.Literal("git"), - store: AbsolutePath, - }), -]) -export type Vcs = typeof Vcs.Type +export const Vcs = ProjectSchema.Vcs +export type Vcs = ProjectSchema.Vcs export class Info extends Schema.Class("Project.Info")({ id: ID, }) {} -export const DirectoriesInput = Schema.Struct({ - projectID: ID, -}).annotate({ identifier: "Project.DirectoriesInput" }) +export const DirectoriesInput = ProjectDirectories.ListInput export type DirectoriesInput = typeof DirectoriesInput.Type -export const Directories = Schema.Array( - Schema.Struct({ - directory: AbsolutePath, - type: Schema.Literals(["main", "root", "git_worktree"]), - }), -).annotate({ identifier: "Project.Directories" }) +export const Directories = ProjectDirectories.ListOutput export type Directories = typeof Directories.Type +export interface Resolved { + readonly previous?: ID + readonly id: ID + readonly directory: AbsolutePath + readonly vcs?: Vcs +} + export interface Interface { readonly directories: (input: DirectoriesInput) => Effect.Effect - readonly resolve: (input: AbsolutePath) => Effect.Effect< - { - previous?: ID - id: ID - directory: AbsolutePath - vcs?: Vcs - }, - never - > + readonly resolve: (input: AbsolutePath) => Effect.Effect /** * Temporary bridge method for writing the resolved project ID to the repo-local cache. * @@ -73,19 +54,12 @@ export class Service extends Context.Service()("@opencode/Pr export const layer = Layer.effect( Service, Effect.gen(function* () { - const db = (yield* Database.Service).db const fs = yield* FSUtil.Service const git = yield* Git.Service + const projectDirectories = yield* ProjectDirectories.Service const directories = Effect.fn("Project.directories")(function* (input: DirectoriesInput) { - const rows = yield* db - .select({ directory: ProjectDirectoryTable.directory, type: ProjectDirectoryTable.type }) - .from(ProjectDirectoryTable) - .where(eq(ProjectDirectoryTable.project_id, input.projectID)) - .orderBy(desc(ProjectDirectoryTable.time_created), asc(ProjectDirectoryTable.directory)) - .all() - .pipe(Effect.orDie) - return rows.map((row) => ({ directory: AbsolutePath.make(row.directory), type: row.type })) + return yield* projectDirectories.list(input.projectID) }) const cached = Effect.fnUntraced(function* (dir: string) { @@ -156,8 +130,8 @@ export const layer = Layer.effect( ) export const defaultLayer = layer.pipe( - Layer.provide(Database.defaultLayer), Layer.provide(FSUtil.defaultLayer), Layer.provide(Git.defaultLayer), + Layer.provideMerge(ProjectDirectories.defaultLayer), ) -export const node = LayerNode.make(layer, [Database.node, FSUtil.node, Git.node]) +export const node = LayerNode.make(layer, [FSUtil.node, Git.node, ProjectDirectories.node]) diff --git a/packages/core/src/project/copy-strategies.ts b/packages/core/src/project/copy-strategies.ts index 3e67ea66d3f..1199964f6cf 100644 --- a/packages/core/src/project/copy-strategies.ts +++ b/packages/core/src/project/copy-strategies.ts @@ -1,20 +1,18 @@ import path from "path" import { Effect } from "effect" import { AbsolutePath } from "../schema" -import { FSUtil } from "../fs-util" import { Git } from "../git" -import { DirectoryUnavailableError, type Copy, type Strategy, type StrategyID } from "./copy" +import { DirectoryUnavailableError, StrategyID, type ListEntry, type Strategy } from "./copy" -export function makeStrategies(input: { +export function makeGitWorktreeStrategy(input: { git: Git.Interface - fs: FSUtil.Interface canonical: (directory: AbsolutePath) => Effect.Effect }) { const repo = (sourceDirectory: AbsolutePath) => ({ directory: sourceDirectory, store: sourceDirectory }) satisfies Git.Repo - const gitWorktree: Strategy = { - id: "git_worktree", + return { + id: StrategyID.make("git_worktree"), create: Effect.fn("ProjectCopy.GitWorktree.create")(function* (options) { yield* input.git.worktreeCreate({ repo: repo(options.sourceDirectory), directory: options.directory }) return { directory: yield* input.canonical(options.directory) } @@ -30,18 +28,11 @@ export function makeStrategies(input: { const core = path.basename(found.store) === ".git" ? path.dirname(found.store) : found.store const entries = yield* input.git.worktreeList(found) return yield* Effect.forEach(entries, (entry) => - entry === core - ? Effect.succeed(undefined) - : input.canonical(entry).pipe( - Effect.map((directory) => ({ directory })), - Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed(undefined)), - ), - ).pipe(Effect.map((items) => items.filter((item): item is Copy => item !== undefined))) + input.canonical(entry).pipe( + Effect.map((directory) => ({ directory, type: entry === core ? "root" : "copy" }) as const), + Effect.catchTag("ProjectCopy.DirectoryUnavailableError", () => Effect.succeed(undefined)), + ), + ).pipe(Effect.map((items) => items.filter((item): item is ListEntry => item !== undefined))) }), - detect: Effect.fn("ProjectCopy.GitWorktree.detect")(function* (inputDirectory) { - return yield* input.fs.isFile(path.join(inputDirectory, ".git")) - }), - } - - return new Map([[gitWorktree.id, gitWorktree]]) + } satisfies Strategy } diff --git a/packages/core/src/project/copy.ts b/packages/core/src/project/copy.ts index de2beda9816..670b0d2dcba 100644 --- a/packages/core/src/project/copy.ts +++ b/packages/core/src/project/copy.ts @@ -1,34 +1,29 @@ export * as ProjectCopy from "./copy" -import { and, eq, inArray } from "drizzle-orm" import { Context, Effect, Layer, Schema } from "effect" import path from "path" import { AbsolutePath } from "../schema" import { FSUtil } from "../fs-util" import { Git } from "../git" -import { Database } from "../database/database" -import { EventV2 } from "../event" import { LayerNode } from "../effect/layer-node" import { Project } from "../project" -import { ProjectDirectoryTable } from "./sql" -import { makeStrategies } from "./copy-strategies" +import { ProjectDirectories } from "./directories" +import { makeGitWorktreeStrategy } from "./copy-strategies" import { Slug } from "../util/slug" +import { EventV2 } from "../event" +import { Database } from "../database/database" +import { Location } from "../location" +import { PluginBoot } from "../plugin/boot" -export const StrategyID = Schema.Literal("git_worktree") +export const StrategyID = Schema.Trim.pipe(Schema.check(Schema.isNonEmpty()), Schema.brand("ProjectCopy.StrategyID")) export type StrategyID = typeof StrategyID.Type -export const DetectInput = Schema.Struct({ - directory: AbsolutePath, -}).annotate({ identifier: "ProjectCopy.DetectInput" }) -export type DetectInput = typeof DetectInput.Type - export const CreateInput = Schema.Struct({ projectID: Project.ID, strategy: StrategyID, sourceDirectory: AbsolutePath, directory: AbsolutePath, name: Schema.optional(Schema.String), - context: Schema.optional(Schema.String), }).annotate({ identifier: "ProjectCopy.CreateInput" }) export type CreateInput = typeof CreateInput.Type @@ -44,12 +39,22 @@ export const RefreshInput = Schema.Struct({ }).annotate({ identifier: "ProjectCopy.RefreshInput" }) export type RefreshInput = typeof RefreshInput.Type +export const RefreshResult = Schema.Struct({ + updated: Schema.Array(AbsolutePath), + removed: Schema.Array(AbsolutePath), +}).annotate({ identifier: "ProjectCopy.RefreshResult" }) +export type RefreshResult = typeof RefreshResult.Type + export const Copy = Schema.Struct({ directory: AbsolutePath, }).annotate({ identifier: "ProjectCopy.Copy" }) export type Copy = typeof Copy.Type -export type DirectoryType = "main" | "root" | StrategyID +export const ListEntry = Schema.Struct({ + directory: AbsolutePath, + type: Schema.Literals(["root", "copy"]), +}).annotate({ identifier: "ProjectCopy.ListEntry" }) +export type ListEntry = typeof ListEntry.Type export class SourceDirectoryNotFoundError extends Schema.TaggedErrorClass()( "ProjectCopy.SourceDirectoryNotFoundError", @@ -66,16 +71,27 @@ export class DirectoryUnavailableError extends Schema.TaggedErrorClass()( - "ProjectCopy.StrategyNotFoundError", +export class InvalidDirectoryError extends Schema.TaggedErrorClass()( + "ProjectCopy.InvalidDirectoryError", { directory: AbsolutePath }, ) {} +export class StrategyUnavailableError extends Schema.TaggedErrorClass()( + "ProjectCopy.StrategyUnavailableError", + { strategy: StrategyID }, +) {} + +export class DuplicateStrategyError extends Schema.TaggedErrorClass()( + "ProjectCopy.DuplicateStrategyError", + { strategy: StrategyID }, +) {} + export type Error = | SourceDirectoryNotFoundError | DestinationExistsError | DirectoryUnavailableError - | StrategyNotFoundError + | InvalidDirectoryError + | StrategyUnavailableError | Git.WorktreeError export interface Strategy { @@ -88,8 +104,7 @@ export interface Strategy { directory: AbsolutePath force: boolean }) => Effect.Effect - readonly list: (directory: AbsolutePath) => Effect.Effect - readonly detect: (directory: AbsolutePath) => Effect.Effect + readonly list: (directory: AbsolutePath) => Effect.Effect } export const Event = { @@ -100,21 +115,46 @@ export const Event = { } export interface Interface { - readonly detect: (input: DetectInput) => Effect.Effect + readonly register: (strategy: Strategy) => Effect.Effect readonly create: (input: CreateInput) => Effect.Effect readonly remove: (input: RemoveInput) => Effect.Effect - readonly refresh: (input: RefreshInput) => Effect.Effect + readonly refresh: (input: RefreshInput) => Effect.Effect } export class Service extends Context.Service()("@opencode/ProjectCopy") {} +export const refreshAfterBoot = Effect.gen(function* () { + const location = yield* Location.Service + const boot = yield* PluginBoot.Service + const copies = yield* Service + yield* Effect.gen(function* () { + yield* boot.wait() + yield* Effect.logInfo("project copy refresh started", { projectID: location.project.id }) + const result = yield* copies.refresh({ projectID: location.project.id }) + yield* Effect.logInfo("project copy refresh done", { + projectID: location.project.id, + updated: result.updated, + removed: result.removed, + }) + }).pipe( + Effect.catchCause((cause) => Effect.logWarning("project copy refresh failed", { cause })), + Effect.forkScoped, + Effect.asVoid, + ) +}) + export const layer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* FSUtil.Service const git = yield* Git.Service - const events = yield* EventV2.Service + const directories = yield* ProjectDirectories.Service const db = (yield* Database.Service).db + const events = yield* EventV2.Service + + const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) { + if (update) yield* events.publish(Event.Updated, { projectID }) + }) const canonical = Effect.fnUntraced(function* (input: AbsolutePath) { const resolved = AbsolutePath.make(FSUtil.resolve(input)) @@ -122,76 +162,34 @@ export const layer = Layer.effect( return resolved }) - const registry = makeStrategies({ git, fs, canonical }) + const registry = new Map() + + const register = Effect.fn("ProjectCopy.register")(function* (strategy: Strategy) { + if (registry.has(strategy.id)) return yield* new DuplicateStrategyError({ strategy: strategy.id }) + registry.set(strategy.id, strategy) + }) + + // Register default strategies + yield* register(makeGitWorktreeStrategy({ git, canonical })).pipe(Effect.orDie) + + const strategies = () => Array.from(registry.values()) const source = Effect.fnUntraced(function* (input: AbsolutePath, projectID: Project.ID) { const sourceDirectory = yield* canonical(input) - const row = yield* db - .select({ directory: ProjectDirectoryTable.directory }) - .from(ProjectDirectoryTable) - .where( - and(eq(ProjectDirectoryTable.project_id, projectID), eq(ProjectDirectoryTable.directory, sourceDirectory)), - ) - .get() - .pipe(Effect.orDie) - if (!row) return yield* new SourceDirectoryNotFoundError({ directory: sourceDirectory }) + if (!(yield* directories.contains({ projectID, directory: sourceDirectory }))) + return yield* new SourceDirectoryNotFoundError({ directory: sourceDirectory }) return sourceDirectory }) - const insert = Effect.fnUntraced(function* (projectID: Project.ID, copyDirectory: AbsolutePath, type: StrategyID) { - return yield* db - .transaction( - (tx) => - Effect.gen(function* () { - const row = yield* tx - .select({ directory: ProjectDirectoryTable.directory }) - .from(ProjectDirectoryTable) - .where( - and( - eq(ProjectDirectoryTable.project_id, projectID), - eq(ProjectDirectoryTable.directory, copyDirectory), - ), - ) - .get() - if (row) return false - yield* tx - .insert(ProjectDirectoryTable) - .values({ project_id: projectID, directory: copyDirectory, type }) - .run() - return true - }), - { behavior: "immediate" }, - ) - .pipe(Effect.orDie) - }) - - const removeStored = Effect.fnUntraced(function* (projectID: Project.ID, copyDirectory: AbsolutePath) { - return ( - (yield* db - .delete(ProjectDirectoryTable) - .where( - and(eq(ProjectDirectoryTable.project_id, projectID), eq(ProjectDirectoryTable.directory, copyDirectory)), - ) - .returning({ directory: ProjectDirectoryTable.directory }) - .get() - .pipe(Effect.orDie)) !== undefined - ) - }) - - const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) { - if (update) yield* events.publish(Event.Updated, { projectID }) - }) - - const strategy = (id: StrategyID) => registry.get(id) as Strategy - - const detect = Effect.fn("ProjectCopy.detect")(function* (input: DetectInput) { - for (const strategy of registry.values()) { - if (yield* strategy.detect(input.directory)) return strategy.id - } - return undefined + const getStrategy = Effect.fnUntraced(function* (id: StrategyID) { + const found = registry.get(id) + if (!found) return yield* new StrategyUnavailableError({ strategy: id }) + return found }) const create = Effect.fn("ProjectCopy.create")(function* (input: CreateInput) { + const selected = yield* getStrategy(input.strategy) + const sourceDirectory = yield* source(input.sourceDirectory, input.projectID) yield* fs.makeDirectory(input.directory, { recursive: true }).pipe(Effect.orDie) const name = input.name ?? Slug.create() let suffix = 1 @@ -202,78 +200,100 @@ export const layer = Layer.effect( copyDirectory = AbsolutePath.make(path.join(input.directory, `${name}-${suffix}`)) } - const result = yield* strategy(input.strategy).create({ + const result = yield* selected.create({ directory: copyDirectory, - sourceDirectory: yield* source(input.sourceDirectory, input.projectID), + sourceDirectory, }) - yield* changed(input.projectID, yield* insert(input.projectID, result.directory, input.strategy)) + yield* changed( + input.projectID, + yield* directories.create({ + projectID: input.projectID, + directory: result.directory, + strategy: input.strategy, + behavior: "replace", + }), + ) return result }) const remove = Effect.fn("ProjectCopy.remove")(function* (input: RemoveInput) { const copyDirectory = yield* canonical(input.directory) - const id = yield* detect({ directory: copyDirectory }) - if (!id) return yield* new StrategyNotFoundError({ directory: copyDirectory }) - yield* strategy(id).remove({ directory: copyDirectory, force: input.force }) - yield* changed(input.projectID, yield* removeStored(input.projectID, copyDirectory)) + const stored = yield* directories.get({ projectID: input.projectID, directory: copyDirectory }) + if (!stored?.strategy) return yield* new InvalidDirectoryError({ directory: copyDirectory }) + yield* (yield* getStrategy(StrategyID.make(stored.strategy))).remove({ + directory: copyDirectory, + force: input.force, + }) + yield* changed( + input.projectID, + yield* directories.remove({ projectID: input.projectID, directory: copyDirectory }), + ) }) const refresh = Effect.fn("ProjectCopy.refresh")(function* (input: RefreshInput) { - const roots = yield* db - .select({ directory: ProjectDirectoryTable.directory }) - .from(ProjectDirectoryTable) - .where( - and( - eq(ProjectDirectoryTable.project_id, input.projectID), - inArray(ProjectDirectoryTable.type, ["main", "root"]), - ), - ) - .all() - .pipe(Effect.orDie) - const sourceDirectories = yield* Effect.forEach(roots, (item) => canonical(AbsolutePath.make(item.directory)), { - concurrency: "unbounded", - }) + const stored = yield* directories.list(input.projectID) + const checked = yield* Effect.forEach( + stored, + (item) => fs.isDir(item.directory).pipe(Effect.map((exists) => ({ ...item, exists }))), + { concurrency: "unbounded" }, + ) + const sourceDirectories = checked + .filter((item) => item.strategy === undefined && item.exists) + .map((item) => item.directory) const discovered = yield* Effect.forEach( sourceDirectories, (sourceDirectory) => - Effect.forEach(registry.values(), (strategy) => - strategy - .list(sourceDirectory) - .pipe(Effect.map((items) => items.map((item) => ({ ...item, type: strategy.id })))), + Effect.forEach(strategies(), (strategy) => + strategy.list(sourceDirectory).pipe( + Effect.map((items) => + items.map((item) => ({ + directory: item.directory, + strategy: item.type === "copy" ? strategy.id : undefined, + })), + ), + ), ), { concurrency: "unbounded" }, ).pipe( Effect.map((sets) => new Map(sets.flat(2).map((item) => [item.directory, item] as const)).values().toArray()), ) - const stored = yield* db - .select({ directory: ProjectDirectoryTable.directory }) - .from(ProjectDirectoryTable) - .where(eq(ProjectDirectoryTable.project_id, input.projectID)) - .all() - .pipe(Effect.orDie) - const inserted = yield* Effect.forEach(discovered, (item) => - insert(input.projectID, item.directory, item.type), - ).pipe(Effect.map((items) => items.some(Boolean))) - const removed = yield* Effect.forEach(stored, (item) => - fs - .isDir(item.directory) - .pipe( - Effect.flatMap((exists) => - exists ? Effect.succeed(false) : removeStored(input.projectID, AbsolutePath.make(item.directory)), + const removed = checked.filter((item) => !item.exists).map((item) => item.directory) + const result = yield* db + .transaction((tx) => + Effect.all({ + updated: Effect.forEach(discovered, (item) => + directories.create( + { + projectID: input.projectID, + directory: item.directory, + strategy: item.strategy, + behavior: "replace", + }, + tx, + ), ), - ), - ).pipe(Effect.map((items) => items.some(Boolean))) - yield* changed(input.projectID, inserted || removed) + removed: Effect.forEach(removed, (directory) => + directories.remove({ projectID: input.projectID, directory }, tx), + ), + }), + ) + .pipe(Effect.orDie) + const changes = { + updated: discovered.filter((_, index) => result.updated[index]).map((item) => item.directory), + removed: removed.filter((_, index) => result.removed[index]), + } + yield* changed(input.projectID, changes.updated.length > 0 || changes.removed.length > 0) + return changes }) - return Service.of({ detect, create, remove, refresh }) + return Service.of({ + register, + create, + remove, + refresh, + }) }), ) -export const defaultLayer = layer.pipe( - Layer.provide(Database.defaultLayer), - Layer.provide(FSUtil.defaultLayer), - Layer.provide(Git.defaultLayer), - Layer.provide(EventV2.defaultLayer), -) -export const node = LayerNode.make(layer, [FSUtil.node, Git.node, EventV2.node, Database.node]) +export const locationLayer = layer +export const node = LayerNode.make(layer, [FSUtil.node, Git.node, ProjectDirectories.node, EventV2.node, Database.node]) diff --git a/packages/core/src/project/directories.ts b/packages/core/src/project/directories.ts new file mode 100644 index 00000000000..7c0522107a6 --- /dev/null +++ b/packages/core/src/project/directories.ts @@ -0,0 +1,159 @@ +export * as ProjectDirectories from "./directories" + +import { and, asc, desc, eq, isNotNull, isNull, ne, or } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" +import { Database } from "../database/database" +import { LayerNode } from "../effect/layer-node" +import { AbsolutePath, optionalOmitUndefined } from "../schema" +import { ProjectSchema } from "./schema" +import { ProjectDirectoryTable } from "./sql" +import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" + +export interface Directory { + readonly directory: AbsolutePath + readonly strategy?: string +} + +export const CreateInput = Schema.Struct({ + projectID: ProjectSchema.ID, + directory: AbsolutePath, + strategy: Schema.optional(Schema.String), + behavior: Schema.Literals(["ignore", "replace"]).pipe(Schema.optional), +}) +export type CreateInput = typeof CreateInput.Type + +export const RemoveInput = Schema.Struct({ + projectID: ProjectSchema.ID, + directory: AbsolutePath, +}) +export type RemoveInput = typeof RemoveInput.Type + +type DatabaseClient = EffectDrizzleSqlite.EffectSQLiteDatabase +export type Transaction = Parameters[0]>[0] + +export const ListInput = Schema.Struct({ + projectID: ProjectSchema.ID, +}).annotate({ identifier: "Project.DirectoriesInput" }) +export type ListInput = typeof ListInput.Type + +export const ListOutput = Schema.Array( + Schema.Struct({ + directory: AbsolutePath, + strategy: optionalOmitUndefined(Schema.String), + }), +).annotate({ identifier: "Project.Directories" }) +export type ListOutput = typeof ListOutput.Type + +export interface Interface { + readonly list: (projectID: ProjectSchema.ID) => Effect.Effect> + readonly get: (input: { + projectID: ProjectSchema.ID + directory: AbsolutePath + }) => Effect.Effect + readonly contains: (input: { projectID: ProjectSchema.ID; directory: AbsolutePath }) => Effect.Effect + readonly create: (input: CreateInput, tx?: Transaction) => Effect.Effect + readonly remove: (input: RemoveInput, tx?: Transaction) => Effect.Effect +} + +export class Service extends Context.Service()("@opencode/ProjectDirectories") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const db = (yield* Database.Service).db + + const create = Effect.fn("ProjectDirectories.create")(function* (input: CreateInput, tx?: Transaction) { + const insert = (tx ?? db) + .insert(ProjectDirectoryTable) + .values({ project_id: input.projectID, directory: input.directory, strategy: input.strategy }) + const query = + input.behavior === "replace" + ? insert.onConflictDoUpdate({ + target: [ProjectDirectoryTable.project_id, ProjectDirectoryTable.directory], + set: { strategy: input.strategy ?? null }, + setWhere: input.strategy + ? or(isNull(ProjectDirectoryTable.strategy), ne(ProjectDirectoryTable.strategy, input.strategy)) + : isNotNull(ProjectDirectoryTable.strategy), + }) + : insert.onConflictDoNothing() + return ( + (yield* query.returning({ directory: ProjectDirectoryTable.directory }).get().pipe(Effect.orDie)) !== undefined + ) + }) + + const remove = Effect.fn("ProjectDirectories.remove")(function* (input: RemoveInput, tx?: Transaction) { + return ( + (yield* (tx ?? db) + .delete(ProjectDirectoryTable) + .where( + and( + eq(ProjectDirectoryTable.project_id, input.projectID), + eq(ProjectDirectoryTable.directory, input.directory), + ), + ) + .returning({ directory: ProjectDirectoryTable.directory }) + .get() + .pipe(Effect.orDie)) !== undefined + ) + }) + + const list = Effect.fn("ProjectDirectories.list")(function* (projectID: ProjectSchema.ID) { + const rows = yield* db + .select({ directory: ProjectDirectoryTable.directory, strategy: ProjectDirectoryTable.strategy }) + .from(ProjectDirectoryTable) + .where(eq(ProjectDirectoryTable.project_id, projectID)) + .orderBy(desc(ProjectDirectoryTable.time_created), asc(ProjectDirectoryTable.directory)) + .all() + .pipe(Effect.orDie) + return rows.map((row) => ({ directory: row.directory, strategy: row.strategy ?? undefined })) + }) + + const contains = Effect.fn("ProjectDirectories.contains")(function* (input: { + projectID: ProjectSchema.ID + directory: AbsolutePath + }) { + return ( + (yield* db + .select({ directory: ProjectDirectoryTable.directory }) + .from(ProjectDirectoryTable) + .where( + and( + eq(ProjectDirectoryTable.project_id, input.projectID), + eq(ProjectDirectoryTable.directory, input.directory), + ), + ) + .get() + .pipe(Effect.orDie)) !== undefined + ) + }) + + const get = Effect.fn("ProjectDirectories.get")(function* (input: { + projectID: ProjectSchema.ID + directory: AbsolutePath + }) { + const row = yield* db + .select({ directory: ProjectDirectoryTable.directory, strategy: ProjectDirectoryTable.strategy }) + .from(ProjectDirectoryTable) + .where( + and( + eq(ProjectDirectoryTable.project_id, input.projectID), + eq(ProjectDirectoryTable.directory, input.directory), + ), + ) + .get() + .pipe(Effect.orDie) + return row ? { directory: row.directory, strategy: row.strategy ?? undefined } : undefined + }) + + return Service.of({ + list, + get, + contains, + create, + remove, + }) + }), +) + +export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer)) +export const node = LayerNode.make(layer, [Database.node]) diff --git a/packages/core/src/project/schema.ts b/packages/core/src/project/schema.ts new file mode 100644 index 00000000000..51d9581cc65 --- /dev/null +++ b/packages/core/src/project/schema.ts @@ -0,0 +1,20 @@ +export * as ProjectSchema from "./schema" + +import { Schema } from "effect" +import { AbsolutePath, withStatics } from "../schema" + +export const ID = Schema.String.pipe( + Schema.brand("Project.ID"), + withStatics((schema) => ({ + global: schema.make("global"), + })), +) +export type ID = typeof ID.Type + +export const Vcs = Schema.Union([ + Schema.Struct({ + type: Schema.Literal("git"), + store: AbsolutePath, + }), +]) +export type Vcs = typeof Vcs.Type diff --git a/packages/core/src/project/sql.ts b/packages/core/src/project/sql.ts index c3954b771ea..ab05fdac4a1 100644 --- a/packages/core/src/project/sql.ts +++ b/packages/core/src/project/sql.ts @@ -1,10 +1,10 @@ import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core" import * as DatabasePath from "../database/path" import { Timestamps } from "../database/schema.sql" -import { ProjectV2 } from "../project" +import { ProjectSchema } from "./schema" export const ProjectTable = sqliteTable("project", { - id: text().$type().primaryKey(), + id: text().$type().primaryKey(), worktree: DatabasePath.absoluteColumn().notNull(), vcs: text(), name: text(), @@ -21,11 +21,12 @@ export const ProjectDirectoryTable = sqliteTable( "project_directory", { project_id: text() - .$type() + .$type() .notNull() .references(() => ProjectTable.id, { onDelete: "cascade" }), - directory: text().notNull(), - type: text().$type<"main" | "root" | "git_worktree">().notNull(), + directory: DatabasePath.absoluteColumn().notNull(), + type: text().$type<"main" | "root" | "git_worktree">(), + strategy: text(), time_created: integer() .notNull() .$default(() => Date.now()), diff --git a/packages/core/src/ripgrep.ts b/packages/core/src/ripgrep.ts index d1a58aea22c..12d4c0eb0ea 100644 --- a/packages/core/src/ripgrep.ts +++ b/packages/core/src/ripgrep.ts @@ -3,6 +3,7 @@ export * as Ripgrep from "./ripgrep" import { Context, Effect, Fiber, Layer, Schema, Stream } from "effect" import { ChildProcess } from "effect/unstable/process" import path from "path" +import { LayerNode } from "./effect/layer-node" import { Entry, Match } from "./filesystem/schema" import { FSUtil } from "./fs-util" import * as SpawnValidation from "./kilocode/spawn-validation" // kilocode_change @@ -312,3 +313,4 @@ export const layer = Layer.effect( ) export const defaultLayer = layer.pipe(Layer.provide(Layer.merge(RipgrepBinary.defaultLayer, AppProcess.defaultLayer))) +export const node = LayerNode.make(layer, [RipgrepBinary.node, AppProcess.node]) diff --git a/packages/core/src/ripgrep/binary.ts b/packages/core/src/ripgrep/binary.ts index a7cf7ae615c..4d34a7d4bd7 100644 --- a/packages/core/src/ripgrep/binary.ts +++ b/packages/core/src/ripgrep/binary.ts @@ -4,6 +4,8 @@ import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/ import { ChildProcess } from "effect/unstable/process" import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner" import { CrossSpawnSpawner } from "../cross-spawn-spawner" +import { LayerNode } from "../effect/layer-node" +import { httpClient } from "../effect/layer-node-platform" import { FSUtil } from "../fs-util" import { Global } from "../global" import { which } from "../util/which" @@ -128,4 +130,6 @@ export namespace RipgrepBinary { Layer.provide(FSUtil.defaultLayer), Layer.provide(CrossSpawnSpawner.defaultLayer), ) + + export const node = LayerNode.make(layer, [FSUtil.node, httpClient, CrossSpawnSpawner.node]) } diff --git a/packages/core/src/v1/session.ts b/packages/core/src/v1/session.ts index 68af4065ac3..eef2cee8df1 100644 --- a/packages/core/src/v1/session.ts +++ b/packages/core/src/v1/session.ts @@ -228,6 +228,16 @@ export const StepStartPart = Schema.Struct({ ...partBase, type: Schema.Literal("step-start"), snapshot: Schema.optional(Schema.String), + // kilocode_change start - wall-clock timestamps captured at the processor + // and consumed by the webview's weighted throughput aggregator. Marked + // optional so older persisted sessions (and synthetic messages) without + // timing still decode cleanly. + time: Schema.optional( + Schema.Struct({ + start: NonNegativeInt, + }), + ), + // kilocode_change end }).annotate({ identifier: "StepStartPart" }) export type StepStartPart = Types.DeepMutable> @@ -243,6 +253,27 @@ export const StepFinishPart = Schema.Struct({ modelID: ModelV2.ID, }), ), + generationID: Schema.optional(Schema.String), // kilocode_change + vercelID: Schema.optional(Schema.String), // kilocode_change + metrics: Schema.optional( + Schema.Struct({ + prompt: Schema.optional(Schema.Finite), + generation: Schema.optional(Schema.Finite), + source: Schema.Literals(["provider", "computed"]), + }), + ), + // Wall-clock timestamps + active generation duration captured at the + // session processor. The webview's weighted throughput aggregator uses + // `time.elapsed` (active model-generation duration in milliseconds, + // excluding tool execution and idle waiting) to weight the per-turn + // rate. Optional so legacy persisted sessions keep decoding. + time: Schema.optional( + Schema.Struct({ + start: NonNegativeInt, + end: NonNegativeInt, + elapsed: Schema.Finite, + }), + ), // kilocode_change end cost: Schema.Finite, tokens: Schema.Struct({ diff --git a/packages/core/test/catalog.test.ts b/packages/core/test/catalog.test.ts index 778c6493dc7..9b1cf89637d 100644 --- a/packages/core/test/catalog.test.ts +++ b/packages/core/test/catalog.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { DateTime, Effect, Layer, Option } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { Connector } from "@opencode-ai/core/connector" +import { Integration } from "@opencode-ai/core/integration" import { Credential } from "@opencode-ai/core/credential" import { EventV2 } from "@opencode-ai/core/event" import { Location } from "@opencode-ai/core/location" @@ -22,20 +22,24 @@ const it = testEffect( Catalog.locationLayer.pipe( Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer), - Layer.provideMerge(Layer.mock(Credential.Service)({ activeAll: () => Effect.succeed(new Map()) })), + Layer.provideMerge( + Layer.mock(Credential.Service)({ + all: () => Effect.succeed([]), + }), + ), ), ) describe("CatalogV2", () => { it.effect("projects Kilo organization routing from OAuth credentials", () => { - const connectorID = Connector.ID.make("kilocode") - const credential = new Credential.Info({ + const integrationID = Integration.ID.make("kilocode") + const credential = new Credential.Stored({ id: Credential.ID.create(), - connectorID, - methodID: Connector.MethodID.make("oauth"), + integrationID, label: "Organization", value: new Credential.OAuth({ type: "oauth", + methodID: Integration.MethodID.make("oauth"), access: "access", refresh: "refresh", expires: 1, @@ -47,7 +51,7 @@ describe("CatalogV2", () => { Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer), Layer.provideMerge( - Layer.mock(Credential.Service)({ activeAll: () => Effect.succeed(new Map([[connectorID, credential]])) }), + Layer.mock(Credential.Service)({ all: () => Effect.succeed([credential]) }), ), ) @@ -62,29 +66,28 @@ describe("CatalogV2", () => { }) it.effect("projects active credentials without rebuilding catalog state", () => { - const connectorID = Connector.ID.make("test") - const methodID = Connector.MethodID.make("api-key") - const first = new Credential.Info({ + const integrationID = Integration.ID.make("test") + const first = { id: Credential.ID.create(), - connectorID, - methodID, + integrationID, label: "First", value: new Credential.Key({ type: "key", key: "first", metadata: { tenant: "one" } }), - }) - const second = new Credential.Info({ + } + const second = { id: Credential.ID.create(), - connectorID, - methodID, + integrationID, label: "Second", value: new Credential.Key({ type: "key", key: "second", metadata: { tenant: "two" } }), - }) + } let active = first const layer = Catalog.locationLayer.pipe( Layer.fresh, Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer), Layer.provideMerge( - Layer.mock(Credential.Service)({ activeAll: () => Effect.succeed(new Map([[connectorID, active]])) }), + Layer.mock(Credential.Service)({ + all: () => Effect.sync(() => [active]), + }), ), ) diff --git a/packages/core/test/connector.test.ts b/packages/core/test/connector.test.ts deleted file mode 100644 index 8d4c854d1ae..00000000000 --- a/packages/core/test/connector.test.ts +++ /dev/null @@ -1,681 +0,0 @@ -import { describe, expect } from "bun:test" -import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer, Scope } from "effect" // kilocode_change -import * as TestClock from "effect/testing/TestClock" -import { Connector } from "@opencode-ai/core/connector" -import { Credential } from "@opencode-ai/core/credential" -import { EventV2 } from "@opencode-ai/core/event" -import { it } from "./lib/effect" - -const layer = Connector.locationLayer.pipe( - Layer.provide(EventV2.defaultLayer), - Layer.provide( - Layer.mock(Credential.Service)({ - create: () => Effect.die("unexpected credential creation"), - }), - ), -) - -function connectionLayer( - created: Array<{ - connectorID: Connector.ID - methodID: Connector.MethodID - label?: string - value: Credential.Value - }>, -) { - return Connector.locationLayer.pipe( - Layer.provide(EventV2.defaultLayer), - Layer.provide( - Layer.mock(Credential.Service)({ - create: (input) => - Effect.sync(() => { - created.push(input) - return new Credential.Info({ id: Credential.ID.create(), ...input, label: input.label ?? "default" }) - }), - }), - ), - ) -} - -describe("Connector", () => { - it.effect("registers connectors through the editor", () => - Effect.gen(function* () { - const connectors = yield* Connector.Service - const scope = yield* Scope.fork(yield* Scope.Scope) - const openai = Connector.ID.make("openai") - - yield* connectors - .update((editor) => editor.update(openai, (connector) => (connector.name = "OpenAI"))) - .pipe(Scope.provide(scope)) - expect(yield* connectors.get(openai)).toEqual(new Connector.Info({ id: openai, name: "OpenAI", methods: [] })) - - yield* Scope.close(scope, Exit.void) - expect(yield* connectors.get(openai)).toBeUndefined() - }).pipe(Effect.provide(layer)), - ) - - it.effect("reveals the previous registration when an override closes", () => - Effect.gen(function* () { - const connectors = yield* Connector.Service - const id = Connector.ID.make("openai") - const first = yield* Scope.fork(yield* Scope.Scope) - const second = yield* Scope.fork(yield* Scope.Scope) - - yield* connectors - .update((editor) => editor.update(id, (connector) => (connector.name = "OpenAI"))) - .pipe(Scope.provide(first)) - yield* connectors - .update((editor) => editor.update(id, (connector) => (connector.name = "OpenAI Override"))) - .pipe(Scope.provide(second)) - expect((yield* connectors.get(id))?.name).toBe("OpenAI Override") - - yield* Scope.close(second, Exit.void) - expect((yield* connectors.get(id))?.name).toBe("OpenAI") - expect((yield* connectors.list()).map((connector) => connector.id)).toEqual([id]) - }).pipe(Effect.provide(layer)), - ) - - it.effect("registers and overrides methods independently", () => - Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("chatgpt") - const first = yield* Scope.fork(yield* Scope.Scope) - const second = yield* Scope.fork(yield* Scope.Scope) - const authorize = () => - Effect.succeed({ - mode: "auto" as const, - url: "https://example.com/authorize", - instructions: "Sign in", - callback: Effect.never, - }) - - yield* connectors - .update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), - authorize, - }), - ) - .pipe(Scope.provide(first)) - yield* connectors - .update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT Override" }), - authorize, - }), - ) - .pipe(Scope.provide(second)) - - expect((yield* connectors.get(connectorID))?.name).toBe("openai") - expect((yield* connectors.get(connectorID))?.methods[0]?.label).toBe("ChatGPT Override") - - yield* Scope.close(second, Exit.void) - expect((yield* connectors.get(connectorID))?.methods[0]?.label).toBe("ChatGPT") - expect((yield* connectors.get(connectorID))?.methods.map((method) => method.id)).toEqual([methodID]) - }).pipe(Effect.provide(layer)), - ) - - it.effect("connects with a key and stores the credential", () => { - const created: Array<{ - connectorID: Connector.ID - methodID: Connector.MethodID - label?: string - value: Credential.Value - }> = [] - return Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("api-key") - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.KeyMethod({ id: methodID, type: "key", label: "API key" }), - authorize: (key, inputs) => - Effect.succeed( - new Credential.Key({ type: "key", key, metadata: { organization: inputs.organization ?? "" } }), - ), - }), - ) - - yield* connectors.connect.key({ - connectorID, - methodID, - key: "secret", - inputs: { organization: "acme" }, - label: "Work", - }) - - expect(created).toEqual([ - { - connectorID, - methodID, - label: "Work", - value: new Credential.Key({ type: "key", key: "secret", metadata: { organization: "acme" } }), - }, - ]) - }).pipe(Effect.provide(connectionLayer(created))) - }) - - it.effect("refreshes OAuth with the originating method", () => { - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("chatgpt") - const credentialID = Credential.ID.create() - const current = new Credential.OAuth({ - type: "oauth", - access: "old-access", - refresh: "old-refresh", - expires: 1, - metadata: { accountID: "account" }, - }) - const updated: Array<{ id: Credential.ID; value: Credential.Value }> = [] - const refreshLayer = Connector.locationLayer.pipe( - Layer.provide(EventV2.defaultLayer), - Layer.provide( - Layer.mock(Credential.Service)({ - get: () => - Effect.succeed( - new Credential.Info({ - id: credentialID, - connectorID, - methodID, - label: "Personal", - value: current, - }), - ), - update: (id, input) => - Effect.sync(() => { - if (input.value) updated.push({ id, value: input.value }) - }), - }), - ), - ) - - return Effect.gen(function* () { - const connectors = yield* Connector.Service - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), - authorize: () => Effect.die("unexpected authorization"), - refresh: (value) => - Effect.succeed( - new Credential.OAuth({ - type: "oauth", - access: "new-access", - refresh: "new-refresh", - expires: 2, - metadata: value.metadata, - }), - ), - }), - ) - - yield* connectors.refresh(credentialID) - expect(updated).toEqual([ - { - id: credentialID, - value: new Credential.OAuth({ - type: "oauth", - access: "new-access", - refresh: "new-refresh", - expires: 2, - metadata: { accountID: "account" }, - }), - }, - ]) - }).pipe(Effect.provide(refreshLayer)) - }) - - it.effect("completes code OAuth once and stores the credential", () => { - const created: Array<{ - connectorID: Connector.ID - methodID: Connector.MethodID - label?: string - value: Credential.Value - }> = [] - return Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("chatgpt") - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), - authorize: () => - Effect.succeed({ - mode: "code" as const, - url: "https://example.com/authorize", - instructions: "Paste the code", - callback: (code: string) => - Effect.succeed( - new Credential.OAuth({ - type: "oauth", - access: "access", - refresh: "refresh", - expires: 1, - metadata: { code }, - }), - ), - }), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {}, label: "Personal" }) - expect(attempt.mode).toBe("code") - yield* connectors.connect.oauth.complete({ attemptID: attempt.attemptID, code: "1234" }) - - expect(created[0]).toEqual({ - connectorID, - methodID, - label: "Personal", - value: new Credential.OAuth({ - type: "oauth", - access: "access", - refresh: "refresh", - expires: 1, - metadata: { code: "1234" }, - }), - }) - }).pipe(Effect.provide(connectionLayer(created))) - }) - - it.effect("keeps code attempts open when the code is missing and closes them on cancel", () => { - const created: Array<{ - connectorID: Connector.ID - methodID: Connector.MethodID - label?: string - value: Credential.Value - }> = [] - return Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("chatgpt") - let closed = false - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), - authorize: () => - Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe( - Effect.as({ - mode: "code" as const, - url: "https://example.com/authorize", - instructions: "Paste the code", - callback: () => Effect.die("unexpected callback"), - }), - ), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} }) - expect( - yield* connectors.connect.oauth.complete({ attemptID: attempt.attemptID }).pipe(Effect.flip), - ).toBeInstanceOf(Connector.CodeRequiredError) - expect(closed).toBe(false) - yield* connectors.connect.oauth.cancel(attempt.attemptID) - expect(closed).toBe(true) - expect(created).toEqual([]) - }).pipe(Effect.provide(connectionLayer(created))) - }) - - it.effect("completes auto OAuth in the background", () => { - const created: Array<{ - connectorID: Connector.ID - methodID: Connector.MethodID - label?: string - value: Credential.Value - }> = [] - return Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("browser") - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }), - authorize: () => - Effect.succeed({ - mode: "auto" as const, - url: "https://example.com/authorize", - instructions: "Sign in", - callback: Effect.succeed( - new Credential.OAuth({ type: "oauth", access: "access", refresh: "refresh", expires: 1 }), - ), - }), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} }) - yield* Effect.yieldNow - expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toEqual({ - status: "complete", - time: attempt.time, - }) - expect(created).toHaveLength(1) - }).pipe(Effect.provide(connectionLayer(created))) - }) - - // kilocode_change start - it.effect("fails auto OAuth when credential persistence fails", () => { - const failed = Connector.locationLayer.pipe( - Layer.provide(EventV2.defaultLayer), - Layer.provide( - Layer.mock(Credential.Service)({ - create: () => Effect.die(new Error("database unavailable")), - }), - ), - ) - return Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("browser") - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }), - authorize: () => - Effect.succeed({ - mode: "auto" as const, - url: "https://example.com/authorize", - instructions: "Sign in", - callback: Effect.succeed( - new Credential.OAuth({ type: "oauth", access: "access", refresh: "refresh", expires: 1 }), - ), - }), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} }) - yield* Effect.yieldNow - expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toMatchObject({ - status: "failed", - message: expect.stringContaining("database unavailable"), - }) - }).pipe(Effect.provide(failed)) - }) - - it.effect("fails code OAuth when credential persistence fails", () => { - const failed = Connector.locationLayer.pipe( - Layer.provide(EventV2.defaultLayer), - Layer.provide( - Layer.mock(Credential.Service)({ - create: () => Effect.die(new Error("database unavailable")), - }), - ), - ) - return Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("chatgpt") - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), - authorize: () => - Effect.succeed({ - mode: "code" as const, - url: "https://example.com/authorize", - instructions: "Paste the code", - callback: () => - Effect.succeed( - new Credential.OAuth({ type: "oauth", access: "access", refresh: "refresh", expires: 1 }), - ), - }), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} }) - const exit = yield* connectors.connect.oauth - .complete({ attemptID: attempt.attemptID, code: "1234" }) - .pipe(Effect.exit) - expect(Exit.isFailure(exit)).toBe(true) - if (Exit.isFailure(exit)) expect(Cause.pretty(exit.cause)).toContain("database unavailable") - expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toMatchObject({ - status: "failed", - message: expect.stringContaining("database unavailable"), - }) - }).pipe(Effect.provide(failed)) - }) - - it.effect("lets OAuth persistence finish after concurrent cancellation", () => - Effect.gen(function* () { - const started = yield* Deferred.make() - const release = yield* Deferred.make() - const created: Credential.Info[] = [] - const delayed = Connector.locationLayer.pipe( - Layer.provide(EventV2.defaultLayer), - Layer.provide( - Layer.mock(Credential.Service)({ - create: (input) => - Effect.gen(function* () { - yield* Deferred.succeed(started, undefined) - yield* Deferred.await(release) - const credential = new Credential.Info({ - id: Credential.ID.create(), - ...input, - label: input.label ?? "default", - }) - created.push(credential) - return credential - }), - }), - ), - ) - - yield* Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("chatgpt") - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), - authorize: () => - Effect.succeed({ - mode: "code" as const, - url: "https://example.com/authorize", - instructions: "Paste the code", - callback: () => - Effect.succeed( - new Credential.OAuth({ type: "oauth", access: "access", refresh: "refresh", expires: 1 }), - ), - }), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} }) - const fiber = yield* connectors.connect.oauth - .complete({ attemptID: attempt.attemptID, code: "1234" }) - .pipe(Effect.forkScoped) - yield* Deferred.await(started) - yield* connectors.connect.oauth.cancel(attempt.attemptID) - yield* Deferred.succeed(release, undefined) - yield* Fiber.join(fiber) - - expect(created).toHaveLength(1) - expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toEqual({ - status: "complete", - time: attempt.time, - }) - }).pipe(Effect.provide(delayed)) - }), - ) - - it.effect("keeps a code OAuth attempt while its callback is completing", () => { - const created: Array<{ - connectorID: Connector.ID - methodID: Connector.MethodID - label?: string - value: Credential.Value - }> = [] - return Effect.gen(function* () { - const started = yield* Deferred.make() - const release = yield* Deferred.make() - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("chatgpt") - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), - authorize: () => - Effect.succeed({ - mode: "code" as const, - url: "https://example.com/authorize", - instructions: "Paste the code", - callback: () => - Deferred.succeed(started, undefined).pipe( - Effect.andThen(Deferred.await(release)), - Effect.as(new Credential.OAuth({ type: "oauth", access: "access", refresh: "refresh", expires: 1 })), - ), - }), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} }) - const fiber = yield* connectors.connect.oauth - .complete({ attemptID: attempt.attemptID, code: "1234" }) - .pipe(Effect.forkScoped) - yield* Deferred.await(started) - yield* connectors.connect.oauth.cancel(attempt.attemptID) - yield* Deferred.succeed(release, undefined) - yield* Fiber.join(fiber) - - expect(created).toHaveLength(1) - expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toMatchObject({ status: "complete" }) - }).pipe(Effect.provide(connectionLayer(created))) - }) - - it.effect("fails and releases code OAuth attempts when the callback times out", () => - Effect.gen(function* () { - const started = yield* Deferred.make() - const state = { closed: false } - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("chatgpt") - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), - authorize: () => - Effect.addFinalizer(() => Effect.sync(() => (state.closed = true))).pipe( - Effect.as({ - mode: "code" as const, - url: "https://example.com/authorize", - instructions: "Paste the code", - callback: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)), - }), - ), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} }) - const fiber = yield* connectors.connect.oauth - .complete({ attemptID: attempt.attemptID, code: "1234" }) - .pipe(Effect.exit, Effect.forkScoped) - yield* Deferred.await(started) - yield* TestClock.adjust(Duration.seconds(30)) - const exit = yield* Fiber.join(fiber) - expect(Exit.isFailure(exit)).toBe(true) - yield* Effect.yieldNow - expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toMatchObject({ status: "failed" }) - expect(state.closed).toBe(true) - }).pipe(Effect.provide(layer)), - ) - - it.effect("fails and releases OAuth attempts when credential persistence times out", () => - Effect.gen(function* () { - const started = yield* Deferred.make() - let closed = false - const stalled = Connector.locationLayer.pipe( - Layer.provide(EventV2.defaultLayer), - Layer.provide( - Layer.mock(Credential.Service)({ - create: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)), - }), - ), - ) - - yield* Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("chatgpt") - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), - authorize: () => - Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe( - Effect.as({ - mode: "code" as const, - url: "https://example.com/authorize", - instructions: "Paste the code", - callback: () => - Effect.succeed( - new Credential.OAuth({ type: "oauth", access: "access", refresh: "refresh", expires: 1 }), - ), - }), - ), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} }) - const fiber = yield* connectors.connect.oauth - .complete({ attemptID: attempt.attemptID, code: "1234" }) - .pipe(Effect.exit, Effect.forkScoped) - yield* Deferred.await(started) - yield* TestClock.adjust(Duration.seconds(30)) - const exit = yield* Fiber.join(fiber) - expect(Exit.isFailure(exit)).toBe(true) - yield* Effect.yieldNow - expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toMatchObject({ status: "failed" }) - expect(closed).toBe(true) - }).pipe(Effect.provide(stalled)) - }), - ) - - // kilocode_change end - it.effect("expires abandoned OAuth attempts", () => { - const created: Array<{ - connectorID: Connector.ID - methodID: Connector.MethodID - label?: string - value: Credential.Value - }> = [] - return Effect.gen(function* () { - const connectors = yield* Connector.Service - const connectorID = Connector.ID.make("openai") - const methodID = Connector.MethodID.make("browser") - let closed = false - yield* connectors.update((editor) => - editor.method.update({ - connectorID, - method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }), - authorize: () => - Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe( - Effect.as({ - mode: "auto" as const, - url: "https://example.com/authorize", - instructions: "Sign in", - callback: Effect.never, - }), - ), - }), - ) - - const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} }) - expect(attempt.time.expires - attempt.time.created).toBe(Duration.toMillis(Duration.minutes(10))) - yield* TestClock.adjust(Duration.minutes(10)) - yield* Effect.yieldNow - expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toEqual({ - status: "expired", - time: attempt.time, - }) - expect(closed).toBe(true) - expect(created).toEqual([]) - }).pipe(Effect.provide(connectionLayer(created))) - }) -}) diff --git a/packages/core/test/credential.test.ts b/packages/core/test/credential.test.ts index 3daa4dab43a..1a987534d3b 100644 --- a/packages/core/test/credential.test.ts +++ b/packages/core/test/credential.test.ts @@ -1,27 +1,57 @@ import path from "path" import { describe, expect } from "bun:test" -import { Effect, Fiber, Layer, Stream } from "effect" +import { Effect, Layer } from "effect" import { Credential } from "@opencode-ai/core/credential" -import { Connector } from "@opencode-ai/core/connector" import { Database } from "@opencode-ai/core/database/database" -import { EventV2 } from "@opencode-ai/core/event" +import { Integration } from "@opencode-ai/core/integration" +// kilocode_change start import { FSUtil } from "@opencode-ai/core/fs-util" import { Global } from "@opencode-ai/core/global" -import { PluginV2 } from "@opencode-ai/core/plugin" +// kilocode_change end import { tmpdir } from "./fixture/tmpdir" -import { testEffect } from "./lib/effect" +import { it } from "./lib/effect" -const it = testEffect(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer))) - -function testLayer(directory: string) { +function layer(directory: string) { return Credential.layer.pipe( - Layer.fresh, + Layer.fresh, // kilocode_change - rebuild so process-local credentials are re-read Layer.provide(Database.layerFromPath(path.join(directory, "credential.db")).pipe(Layer.fresh)), - Layer.provideMerge(EventV2.defaultLayer), ) } describe("Credential", () => { + it.live("stores, updates, lists, and removes credentials", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + Effect.gen(function* () { + const credentials = yield* Credential.Service + const integrationID = Integration.ID.make("openai") + const created = yield* credentials.create({ + integrationID, + label: "Work", + value: new Credential.Key({ type: "key", key: "secret" }), + }) + + expect(yield* credentials.list(integrationID)).toEqual([created]) + yield* credentials.update(created.id, { label: "Personal" }) + expect((yield* credentials.list(integrationID))[0]?.label).toBe("Personal") + + const replacement = yield* credentials.create({ + integrationID, + label: "Replacement", + value: new Credential.Key({ type: "key", key: "replacement" }), + }) + expect(yield* credentials.list(integrationID)).toEqual([replacement]) + + yield* credentials.remove(replacement.id) + expect(yield* credentials.list(integrationID)).toEqual([]) + }).pipe(Effect.provide(layer(tmp.path))), + ), + ), + ) + // kilocode_change start - process-provided credentials remain isolated from durable storage it.live("keeps valid KILO_AUTH_CONTENT credentials and isolated mutations process-local", () => Effect.acquireUseRelease( @@ -51,10 +81,11 @@ describe("Credential", () => { const service = yield* Credential.Service const all = yield* service.all() expect(all).toHaveLength(2) - const initial = yield* service.active(Connector.ID.make("kilocode")) - expect(initial).toMatchObject({ - connectorID: Connector.ID.make("kilocode"), - methodID: Connector.MethodID.make("oauth"), + const kilocode = Integration.ID.make("kilocode") + const listed = yield* service.list(kilocode) + expect(listed).toHaveLength(1) + expect(listed[0]).toMatchObject({ + integrationID: kilocode, label: "Environment", value: { type: "oauth", @@ -64,28 +95,24 @@ describe("Credential", () => { metadata: { accountID: "organization" }, }, }) - expect(initial).toBeDefined() - if (!initial) return const created = yield* service.create({ - connectorID: Connector.ID.make("kilocode"), - methodID: Connector.MethodID.make("api-key"), + integrationID: kilocode, label: "Temporary", value: new Credential.Key({ type: "key", key: "temporary" }), }) + expect(yield* service.list(kilocode)).toEqual([created]) yield* service.update(created.id, { label: "Updated" }) - expect((yield* service.active(Connector.ID.make("kilocode")))?.label).toBe("Updated") - yield* service.activate(initial.id) - expect((yield* service.active(Connector.ID.make("kilocode")))?.id).toBe(initial.id) - yield* service.remove(initial.id) - expect((yield* service.active(Connector.ID.make("kilocode")))?.id).toBe(created.id) + expect((yield* service.list(kilocode))[0]?.label).toBe("Updated") + yield* service.remove(created.id) + expect(yield* service.list(kilocode)).toEqual([]) delete process.env.KILO_AUTH_CONTENT const stored = yield* Effect.gen(function* () { return yield* (yield* Credential.Service).all() - }).pipe(Effect.provide(testLayer(tmp.path)), Effect.scoped) + }).pipe(Effect.provide(layer(tmp.path)), Effect.scoped) expect(stored).toEqual([]) - }).pipe(Effect.provide(testLayer(tmp.path))), + }).pipe(Effect.provide(layer(tmp.path))), ), ), (previous) => @@ -97,7 +124,6 @@ describe("Credential", () => { ) it.live("reconciles supported legacy auth.json credentials on startup", () => - // kilocode_change end Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), @@ -127,11 +153,7 @@ describe("Credential", () => { Layer.provide(FSUtil.defaultLayer), Layer.provide(global), ) - const credentials = Credential.layer.pipe( - Layer.provide(database), - Layer.provide(EventV2.defaultLayer), - Layer.provideMerge(importer), - ) + const credentials = Credential.layer.pipe(Layer.provide(database), Layer.provideMerge(importer)) const result = yield* Effect.gen(function* () { const service = yield* Credential.Service return yield* service.all() @@ -140,11 +162,11 @@ describe("Credential", () => { expect(result).toHaveLength(2) expect(result).toContainEqual( expect.objectContaining({ - connectorID: Connector.ID.make("openai"), - methodID: Connector.MethodID.make("chatgpt-browser"), + integrationID: Integration.ID.make("openai"), label: "Imported", value: expect.objectContaining({ type: "oauth", + methodID: Integration.MethodID.make("chatgpt-browser"), refresh: "refresh", access: "access", expires: 123, @@ -154,22 +176,12 @@ describe("Credential", () => { ) expect(result).toContainEqual( expect.objectContaining({ - connectorID: Connector.ID.make("azure"), - methodID: Connector.MethodID.make("api-key"), + integrationID: Integration.ID.make("azure"), value: expect.objectContaining({ type: "key", key: "key", metadata: { resourceName: "resource" } }), }), ) - // kilocode_change start - update the selected row when a released client changes auth.json. - const selected = yield* Effect.gen(function* () { - return yield* (yield* Credential.Service).create({ - connectorID: Connector.ID.make("azure"), - methodID: Connector.MethodID.make("api-key"), - label: "Selected", - value: new Credential.Key({ type: "key", key: "selected" }), - }) - }).pipe(Effect.provide(credentials), Effect.scoped) - + // a released client can update auth.json after the import; the next startup reconciles the stored value yield* Effect.promise(() => Bun.write( path.join(tmp.path, "auth.json"), @@ -181,25 +193,18 @@ describe("Credential", () => { const service = yield* Credential.Service return { all: yield* service.all(), - active: yield* service.active(Connector.ID.make("azure")), + azure: yield* service.list(Integration.ID.make("azure")), } }).pipe(Effect.provide(credentials), Effect.scoped) - expect(after.all).toHaveLength(3) - expect(after.active).toMatchObject({ - id: selected.id, - value: { type: "key", key: "updated" }, - }) - expect( - after.all.find((item) => item.connectorID === Connector.ID.make("azure") && item.id !== selected.id)?.value, - ).toMatchObject({ type: "key", key: "key" }) - // kilocode_change end + expect(after.all).toHaveLength(2) + expect(after.azure).toHaveLength(1) + expect(after.azure[0]?.value).toMatchObject({ type: "key", key: "updated" }) }), ), ), ) - // kilocode_change start - retain downgrade-readable credential state - it.live("dual-writes active credentials for released auth.json readers", () => + it.live("dual-writes stored credentials for released auth.json readers", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), @@ -209,42 +214,34 @@ describe("Credential", () => { const global = Global.layerWith({ data: tmp.path }) const credentials = Credential.layer.pipe( Layer.provide(database), - Layer.provide(EventV2.defaultLayer), Layer.provide(FSUtil.defaultLayer), Layer.provide(global), ) return Effect.gen(function* () { const service = yield* Credential.Service - const connectorID = Connector.ID.make("legacy-reader") - const created = yield* service.create({ - connectorID, - methodID: Connector.MethodID.make("api-key"), + const integrationID = Integration.ID.make("legacy-reader") + yield* service.create({ + integrationID, value: new Credential.Key({ type: "key", key: "first" }), }) expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "auth.json")).json())).toMatchObject({ "legacy-reader": { type: "api", key: "first" }, }) - const other = yield* service.create({ - connectorID, - methodID: Connector.MethodID.make("api-key"), + const replacement = yield* service.create({ + integrationID, value: new Credential.Key({ type: "key", key: "other" }), }) expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "auth.json")).json())).toMatchObject({ "legacy-reader": { type: "api", key: "other" }, }) - yield* service.activate(created.id) - expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "auth.json")).json())).toMatchObject({ - "legacy-reader": { type: "api", key: "first" }, - }) - yield* service.remove(other.id) - yield* service.update(created.id, { value: new Credential.Key({ type: "key", key: "second" }) }) + yield* service.update(replacement.id, { value: new Credential.Key({ type: "key", key: "second" }) }) expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "auth.json")).json())).toMatchObject({ "legacy-reader": { type: "api", key: "second" }, }) - yield* service.remove(created.id) + yield* service.remove(replacement.id) expect(yield* Effect.promise(() => Bun.file(path.join(tmp.path, "auth.json")).json())).not.toHaveProperty( "legacy-reader", ) @@ -252,8 +249,7 @@ describe("Credential", () => { const file = path.join(tmp.path, "auth.json") yield* Effect.promise(() => Bun.write(file, "{")) yield* service.create({ - connectorID: Connector.ID.make("malformed-reader"), - methodID: Connector.MethodID.make("api-key"), + integrationID: Integration.ID.make("malformed-reader"), value: new Credential.Key({ type: "key", key: "safe" }), }) expect(yield* Effect.promise(() => Bun.file(file).text())).toBe("{") @@ -262,8 +258,7 @@ describe("Credential", () => { yield* Effect.all( ["first-reader", "second-reader"].map((name) => service.create({ - connectorID: Connector.ID.make(name), - methodID: Connector.MethodID.make("api-key"), + integrationID: Integration.ID.make(name), value: new Credential.Key({ type: "key", key: name }), }), ), @@ -278,113 +273,4 @@ describe("Credential", () => { ), ) // kilocode_change end - - it.live("emits credential lifecycle events", () => - Effect.acquireRelease( - Effect.promise(() => tmpdir()), - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ).pipe( - Effect.flatMap((tmp) => - Effect.gen(function* () { - const credentials = yield* Credential.Service - const eventSvc = yield* EventV2.Service - const addedFiber = yield* eventSvc - .subscribe(Credential.Event.Added) - .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped) - const switchedFiber = yield* eventSvc - .subscribe(Credential.Event.Switched) - .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped) - const removedFiber = yield* eventSvc - .subscribe(Credential.Event.Removed) - .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) - - yield* Effect.yieldNow - - const first = yield* credentials.create({ - connectorID: Connector.ID.make("lifecycle"), - methodID: Connector.MethodID.make("key"), - value: new Credential.Key({ type: "key", key: "raw-key" }), - }) - expect(first).toBeDefined() - if (!first) return - expect(first.label).toBe("default") - expect(first.value.type).toBe("key") - if (first.value.type === "key") expect(first.value.key).toBe("raw-key") - - yield* credentials.update(first.id, { label: "keep" }) - const updated = yield* credentials.get(first.id) - expect(updated?.label).toBe("keep") - expect(updated?.value.type).toBe("key") - if (updated?.value.type === "key") expect(updated.value.key).toBe("raw-key") - - const second = yield* credentials.create({ - connectorID: Connector.ID.make("lifecycle"), - methodID: Connector.MethodID.make("key"), - value: new Credential.Key({ type: "key", key: "second-key" }), - }) - expect(second).toBeDefined() - if (!second) return - - yield* credentials.remove(second.id) - const added = Array.from(yield* Fiber.join(addedFiber)) - const switched = Array.from(yield* Fiber.join(switchedFiber)) - const removed = Array.from(yield* Fiber.join(removedFiber)) - expect(added.map((event) => event.data.credential.id)).toEqual([first.id, second.id]) - expect(switched.map((event) => event.data)).toEqual([ - { connectorID: Connector.ID.make("lifecycle"), from: undefined, to: first.id }, - { connectorID: Connector.ID.make("lifecycle"), from: first.id, to: second.id }, - { connectorID: Connector.ID.make("lifecycle"), from: second.id, to: first.id }, - ]) - expect(removed[0]?.data.credential.id).toBe(second.id) - }).pipe(Effect.provide(testLayer(tmp.path))), - ), - ), - ) - - it.live("always switches to newly created credentials", () => - Effect.acquireRelease( - Effect.promise(() => tmpdir()), - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), - ).pipe( - Effect.flatMap((tmp) => - Effect.gen(function* () { - const credentials = yield* Credential.Service - const eventSvc = yield* EventV2.Service - const switchedFiber = yield* eventSvc - .subscribe(Credential.Event.Switched) - .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped) - - yield* Effect.yieldNow - - const first = yield* credentials.create({ - connectorID: Connector.ID.make("switch"), - methodID: Connector.MethodID.make("key"), - value: new Credential.Key({ type: "key", key: "first-key" }), - }) - const second = yield* credentials.create({ - connectorID: Connector.ID.make("switch"), - methodID: Connector.MethodID.make("key"), - value: new Credential.Key({ type: "key", key: "second-key" }), - }) - const third = yield* credentials.create({ - connectorID: Connector.ID.make("switch"), - methodID: Connector.MethodID.make("key"), - value: new Credential.Key({ type: "key", key: "third-key" }), - }) - - expect(first).toBeDefined() - expect(second).toBeDefined() - expect(third).toBeDefined() - if (!first || !second || !third) return - - expect((yield* credentials.active(Connector.ID.make("switch")))?.id).toBe(third.id) - expect(Array.from(yield* Fiber.join(switchedFiber)).map((event) => event.data)).toEqual([ - { connectorID: Connector.ID.make("switch"), from: undefined, to: first.id }, - { connectorID: Connector.ID.make("switch"), from: first.id, to: second.id }, - { connectorID: Connector.ID.make("switch"), from: second.id, to: third.id }, - ]) - }).pipe(Effect.provide(testLayer(tmp.path))), - ), - ), - ) }) diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index 7daff4e5b26..d7126f76e26 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -13,6 +13,7 @@ import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order" import eventSourcedSessionInputMigration from "@opencode-ai/core/database/migration/20260604172448_event_sourced_session_input" import contextEpochAgentMigration from "@opencode-ai/core/database/migration/20260605042240_add_context_epoch_agent" +import simplifyIntegrationCredentialsMigration from "@opencode-ai/core/database/migration/20260611192811_lush_chimera" import { ProjectV2 } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" import { AbsolutePath } from "@opencode-ai/core/schema" @@ -92,6 +93,18 @@ describe("DatabaseMigration", () => { ) }) + test("rejects a non-empty database without a session table", async () => { + await expect( + run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run(sql`CREATE TABLE unrelated (id text PRIMARY KEY)`) + yield* DatabaseMigration.apply(db) + }), + ), + ).rejects.toThrow("Database is not empty and has no session table") + }) + test("backfills existing Context Epoch rows to the build agent", async () => { await run( Effect.gen(function* () { @@ -112,6 +125,31 @@ describe("DatabaseMigration", () => { ) }) + test("keeps legacy credential fields nullable", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run( + sql`CREATE TABLE credential (id text PRIMARY KEY, connector_id text NOT NULL, method_id text NOT NULL, label text NOT NULL, value text NOT NULL, active integer DEFAULT false NOT NULL, time_created integer NOT NULL, time_updated integer NOT NULL)`, + ) + yield* db.run( + sql`CREATE UNIQUE INDEX credential_connector_active_idx ON credential (connector_id) WHERE active = 1`, + ) + yield* DatabaseMigration.applyOnly(db, [simplifyIntegrationCredentialsMigration]) + + yield* db.run( + sql`INSERT INTO credential (id, connector_id, method_id, label, value, active, time_created, time_updated) VALUES ('legacy', 'openai', 'oauth', 'Legacy', '{}', 1, 1, 1)`, + ) + yield* db.run( + sql`INSERT INTO credential (id, integration_id, label, value, time_created, time_updated) VALUES ('current', 'anthropic', 'Current', '{}', 2, 2)`, + ) + expect(yield* db.get(sql`SELECT connector_id, method_id, active FROM credential WHERE id = 'current'`)).toEqual( + { connector_id: null, method_id: null, active: null }, + ) + }), + ) + }) + test("resets beta history and rebuilds event-sourced Session input storage", async () => { await run( Effect.gen(function* () { diff --git a/packages/core/test/integration.test.ts b/packages/core/test/integration.test.ts new file mode 100644 index 00000000000..fa05e23d95e --- /dev/null +++ b/packages/core/test/integration.test.ts @@ -0,0 +1,401 @@ +import { describe, expect } from "bun:test" +import { Duration, Effect, Exit, Layer, Scope } from "effect" +import * as TestClock from "effect/testing/TestClock" +import { Integration } from "@opencode-ai/core/integration" +import { IntegrationConnection } from "@opencode-ai/core/integration/connection" +import { Credential } from "@opencode-ai/core/credential" +import { EventV2 } from "@opencode-ai/core/event" +import { it } from "./lib/effect" + +const layer = Integration.locationLayer.pipe( + Layer.provide(EventV2.defaultLayer), + Layer.provide( + Layer.mock(Credential.Service)({ + create: () => Effect.die("unexpected credential creation"), + list: () => Effect.succeed([]), + }), + ), +) + +function connectionLayer( + created: Array<{ + integrationID: Integration.ID + label?: string + value: Credential.Info + }>, +) { + return Integration.locationLayer.pipe( + Layer.provide(EventV2.defaultLayer), + Layer.provide( + Layer.mock(Credential.Service)({ + create: (input) => + Effect.sync(() => { + created.push(input) + return new Credential.Stored({ + id: Credential.ID.create(), + integrationID: input.integrationID, + label: input.label ?? "default", + value: input.value, + }) + }), + list: () => Effect.succeed([]), + }), + ), + ) +} + +describe("Integration", () => { + it.effect("registers integrations through the editor", () => + Effect.gen(function* () { + const integrations = yield* Integration.Service + const scope = yield* Scope.fork(yield* Scope.Scope) + const openai = Integration.ID.make("openai") + + yield* integrations + .update((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI"))) + .pipe(Scope.provide(scope)) + expect(yield* integrations.get(openai)).toEqual( + new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }), + ) + + yield* Scope.close(scope, Exit.void) + expect(yield* integrations.get(openai)).toBeUndefined() + }).pipe(Effect.provide(layer)), + ) + + it.effect("reveals the previous registration when an override closes", () => + Effect.gen(function* () { + const integrations = yield* Integration.Service + const id = Integration.ID.make("openai") + const first = yield* Scope.fork(yield* Scope.Scope) + const second = yield* Scope.fork(yield* Scope.Scope) + + yield* integrations + .update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI"))) + .pipe(Scope.provide(first)) + yield* integrations + .update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override"))) + .pipe(Scope.provide(second)) + expect((yield* integrations.get(id))?.name).toBe("OpenAI Override") + + yield* Scope.close(second, Exit.void) + expect((yield* integrations.get(id))?.name).toBe("OpenAI") + expect((yield* integrations.list()).map((integration) => integration.id)).toEqual([id]) + }).pipe(Effect.provide(layer)), + ) + + it.effect("registers and overrides methods independently", () => + Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("chatgpt") + const first = yield* Scope.fork(yield* Scope.Scope) + const second = yield* Scope.fork(yield* Scope.Scope) + const authorize = () => + Effect.succeed({ + mode: "auto" as const, + url: "https://example.com/authorize", + instructions: "Sign in", + callback: Effect.never, + }) + + yield* integrations + .update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), + authorize, + }), + ) + .pipe(Scope.provide(first)) + yield* integrations + .update((editor) => { + expect(editor.get(integrationID)).toEqual({ id: integrationID, name: "openai" }) + expect(editor.list()).toEqual([{ id: integrationID, name: "openai" }]) + expect(editor.method.list(integrationID)).toEqual([ + expect.objectContaining({ id: methodID, label: "ChatGPT" }), + ]) + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT Override" }), + authorize, + }) + }) + .pipe(Scope.provide(second)) + + expect((yield* integrations.get(integrationID))?.name).toBe("openai") + expect((yield* integrations.get(integrationID))?.methods[0]).toMatchObject({ label: "ChatGPT Override" }) + + yield* Scope.close(second, Exit.void) + expect((yield* integrations.get(integrationID))?.methods[0]).toMatchObject({ label: "ChatGPT" }) + expect((yield* integrations.get(integrationID))?.methods).toEqual([expect.objectContaining({ id: methodID })]) + }).pipe(Effect.provide(layer)), + ) + + it.effect("connects with a key and stores the credential", () => { + const created: Array<{ + integrationID: Integration.ID + label?: string + value: Credential.Info + }> = [] + return Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.KeyMethod({ type: "key", label: "API key" }), + }), + ) + + yield* integrations.connect.key({ + integrationID, + key: "secret", + label: "Work", + }) + + expect(created).toEqual([ + { + integrationID, + label: "Work", + value: new Credential.Key({ type: "key", key: "secret" }), + }, + ]) + }).pipe(Effect.provide(connectionLayer(created))) + }) + + it.effect("completes code OAuth once and stores the credential", () => { + const created: Array<{ + integrationID: Integration.ID + label?: string + value: Credential.Info + }> = [] + return Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("chatgpt") + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), + authorize: () => + Effect.succeed({ + mode: "code" as const, + url: "https://example.com/authorize", + instructions: "Paste the code", + callback: (code: string) => + Effect.succeed( + new Credential.OAuth({ + type: "oauth", + methodID, + access: "access", + refresh: "refresh", + expires: 1, + metadata: { code }, + }), + ), + }), + }), + ) + + const attempt = yield* integrations.connect.oauth({ + integrationID, + methodID, + inputs: {}, + label: "Personal", + }) + expect(attempt.mode).toBe("code") + yield* integrations.attempt.complete({ attemptID: attempt.attemptID, code: "1234" }) + + expect(created[0]).toEqual({ + integrationID, + label: "Personal", + value: new Credential.OAuth({ + type: "oauth", + methodID, + access: "access", + refresh: "refresh", + expires: 1, + metadata: { code: "1234" }, + }), + }) + }).pipe(Effect.provide(connectionLayer(created))) + }) + + it.effect("keeps code attempts open when the code is missing and closes them on cancel", () => { + const created: Array<{ + integrationID: Integration.ID + label?: string + value: Credential.Info + }> = [] + return Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("chatgpt") + let closed = false + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), + authorize: () => + Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe( + Effect.as({ + mode: "code" as const, + url: "https://example.com/authorize", + instructions: "Paste the code", + callback: () => Effect.die("unexpected callback"), + }), + ), + }), + ) + + const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} }) + expect(yield* integrations.attempt.complete({ attemptID: attempt.attemptID }).pipe(Effect.flip)).toBeInstanceOf( + Integration.CodeRequiredError, + ) + expect(closed).toBe(false) + yield* integrations.attempt.cancel(attempt.attemptID) + expect(closed).toBe(true) + expect(created).toEqual([]) + }).pipe(Effect.provide(connectionLayer(created))) + }) + + it.effect("completes auto OAuth in the background", () => { + const created: Array<{ + integrationID: Integration.ID + label?: string + value: Credential.Info + }> = [] + return Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("browser") + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }), + authorize: () => + Effect.succeed({ + mode: "auto" as const, + url: "https://example.com/authorize", + instructions: "Sign in", + callback: Effect.succeed( + new Credential.OAuth({ type: "oauth", methodID, access: "access", refresh: "refresh", expires: 1 }), + ), + }), + }), + ) + + const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} }) + yield* Effect.yieldNow + expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({ + status: "complete", + time: attempt.time, + }) + expect(created).toHaveLength(1) + }).pipe(Effect.provide(connectionLayer(created))) + }) + + it.effect("expires abandoned OAuth attempts", () => { + const created: Array<{ + integrationID: Integration.ID + label?: string + value: Credential.Info + }> = [] + return Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("browser") + let closed = false + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }), + authorize: () => + Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe( + Effect.as({ + mode: "auto" as const, + url: "https://example.com/authorize", + instructions: "Sign in", + callback: Effect.never, + }), + ), + }), + ) + + const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} }) + expect(attempt.time.expires - attempt.time.created).toBe(Duration.toMillis(Duration.minutes(10))) + yield* TestClock.adjust(Duration.minutes(10)) + yield* Effect.yieldNow + expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({ + status: "expired", + time: attempt.time, + }) + expect(closed).toBe(true) + expect(created).toEqual([]) + }).pipe(Effect.provide(connectionLayer(created))) + }) + + it.effect("projects credential and env connections", () => { + const integrationID = Integration.ID.make("acme") + const rows = [ + { + id: Credential.ID.create(), + integrationID, + label: "Work", + value: new Credential.Key({ type: "key", key: "a" }), + }, + { + id: Credential.ID.create(), + integrationID, + label: "Personal", + value: new Credential.Key({ type: "key", key: "b" }), + }, + ] + const projectionLayer = Integration.locationLayer.pipe( + Layer.provide(EventV2.defaultLayer), + Layer.provide( + Layer.mock(Credential.Service)({ + list: () => Effect.succeed(rows.map((row) => new Credential.Stored(row))), + }), + ), + ) + return Effect.acquireUseRelease( + Effect.sync(() => { + const previous = process.env.INTEGRATION_TEST_ACME_KEY + process.env.INTEGRATION_TEST_ACME_KEY = "secret" + delete process.env.INTEGRATION_TEST_ACME_MISSING + return previous + }), + () => + Effect.gen(function* () { + const integrations = yield* Integration.Service + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.EnvMethod({ + type: "env", + names: ["INTEGRATION_TEST_ACME_KEY", "INTEGRATION_TEST_ACME_MISSING"], + }), + }), + ) + + // Stored credentials and detected env vars appear as connections. + expect((yield* integrations.get(integrationID))?.connections).toEqual([ + new IntegrationConnection.CredentialInfo({ type: "credential", id: rows[0]!.id, label: "Work" }), + new IntegrationConnection.CredentialInfo({ + type: "credential", + id: rows[1]!.id, + label: "Personal", + }), + new IntegrationConnection.EnvInfo({ type: "env", name: "INTEGRATION_TEST_ACME_KEY" }), + ]) + }).pipe(Effect.provide(projectionLayer)), + (previous) => + Effect.sync(() => { + if (previous === undefined) delete process.env.INTEGRATION_TEST_ACME_KEY + else process.env.INTEGRATION_TEST_ACME_KEY = previous + }), + ) + }) +}) diff --git a/packages/core/test/kilocode/account-auth-v2-migration.test.ts b/packages/core/test/kilocode/account-auth-v2-migration.test.ts index 9620d612a37..cabfb758c5c 100644 --- a/packages/core/test/kilocode/account-auth-v2-migration.test.ts +++ b/packages/core/test/kilocode/account-auth-v2-migration.test.ts @@ -1,12 +1,15 @@ import path from "path" import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" -import { Connector } from "@opencode-ai/core/connector" +import { eq } from "drizzle-orm" +import { IntegrationSchema } from "@opencode-ai/core/integration/schema" import { Credential } from "@opencode-ai/core/credential" import { Database } from "@opencode-ai/core/database/database" import { EventV2 } from "@opencode-ai/core/event" import { FSUtil } from "@opencode-ai/core/fs-util" import { Global } from "@opencode-ai/core/global" +import { DataMigrationTable } from "@opencode-ai/core/data-migration.sql" +import { CredentialTable } from "@opencode-ai/core/credential/sql" import { tmpdir } from "../fixture/tmpdir" import { it } from "../lib/effect" @@ -38,7 +41,7 @@ const auth = Effect.acquireRelease( ) describe("Credential auth-v2 migration", () => { - it.live("preserves multiple accounts, active selection, and Kilo organization", () => + it.live("imports every account with the active account ordered last", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), @@ -83,16 +86,17 @@ describe("Credential auth-v2 migration", () => { const credentials = yield* Credential.Service return { all: yield* credentials.all(), - active: yield* credentials.active(Connector.ID.make("kilo")), + list: yield* credentials.list(IntegrationSchema.ID.make("kilo")), } }).pipe(Effect.provide(layer(tmp.path))) expect(result.all.map((item) => item.label)).toEqual(["first", "second"]) - expect(result.active?.label).toBe("second") - expect(result.active?.value.type).toBe("oauth") - if (result.active?.value.type === "oauth") { - expect(result.active.value.access).toBe("access-second") - expect(result.active.value.metadata?.accountID).toBe("org-second") + expect(result.list.length).toBe(2) + const active = result.list.at(-1) + expect(active?.value.type).toBe("oauth") + if (active?.value.type === "oauth") { + expect(active.value.access).toBe("access-second") + expect(active.value.metadata?.accountID).toBe("org-second") } }), ), @@ -100,4 +104,96 @@ describe("Credential auth-v2 migration", () => { ), ), ) + + it.live("repairs an active-only v2 import without duplicating the active account", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((tmp) => + auth.pipe( + Effect.flatMap(() => + Effect.gen(function* () { + const integration = IntegrationSchema.ID.make("kilo") + const active = new Credential.OAuth({ + type: "oauth", + methodID: IntegrationSchema.MethodID.make("oauth"), + refresh: "refresh-second", + access: "access-second", + expires: 2, + metadata: { accountID: "org-second" }, + }) + const database = Database.layerFromPath(path.join(tmp.path, "credential.db")).pipe(Layer.fresh) + yield* Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db.insert(CredentialTable).values({ + id: Credential.ID.create(), + integration_id: integration, + label: "second", + value: active, + }) + yield* db.insert(DataMigrationTable).values({ + name: "credential.kilo-account-json-v2", + time_completed: Date.now(), + }) + }).pipe(Effect.provide(database)) + + yield* Effect.promise(() => + Bun.write( + path.join(tmp.path, "auth-v2.json"), + JSON.stringify({ + version: 2, + accounts: { + acc_first: { + id: "acc_first", + serviceID: "kilo", + description: "first", + credential: { + type: "oauth", + refresh: "refresh-first", + access: "access-first", + expires: 1, + accountId: "org-first", + }, + }, + acc_second: { + id: "acc_second", + serviceID: "kilo", + description: "second", + credential: { + type: "oauth", + refresh: "refresh-second", + access: "access-second", + expires: 2, + accountId: "org-second", + }, + }, + }, + active: { kilo: "acc_second" }, + }), + ), + ) + + const result = yield* Effect.gen(function* () { + const credentials = yield* Credential.Service + return yield* credentials.list(integration) + }).pipe(Effect.provide(layer(tmp.path))) + const repaired = yield* Effect.gen(function* () { + const { db } = yield* Database.Service + return yield* db + .select() + .from(DataMigrationTable) + .where(eq(DataMigrationTable.name, "credential.kilo-account-json-v3")) + .get() + }).pipe(Effect.provide(database)) + + expect(result.map((item) => item.label)).toEqual(["first", "second"]) + expect(result.filter((item) => item.label === "second")).toHaveLength(1) + expect(repaired).toBeDefined() + }), + ), + ), + ), + ), + ) }) diff --git a/packages/core/test/kilocode/db-preflight.test.ts b/packages/core/test/kilocode/db-preflight.test.ts new file mode 100644 index 00000000000..3069111aee8 --- /dev/null +++ b/packages/core/test/kilocode/db-preflight.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, test } from "bun:test" +import { Database } from "bun:sqlite" +import { accessSync, chmodSync, constants } from "fs" +import path from "path" +import { DbPreflight } from "@opencode-ai/core/kilocode/db-preflight" +import { Database as KiloDatabase } from "@opencode-ai/core/database/database" +import { tmpdir } from "../fixture/tmpdir" + +const writable = (file: string) => { + try { + accessSync(file, constants.W_OK) + return true + } catch { + return false + } +} + +// Windows: chmod is a no-op, so non-writable files cannot be staged; root ignores permission bits +const skip = process.platform === "win32" || process.getuid?.() === 0 + +function createWalDb(file: string) { + const db = new Database(file) + db.run("PRAGMA journal_mode = WAL") + db.run("CREATE TABLE t (x)") + db.run("INSERT INTO t VALUES (1)") + db.close() +} + +// leaves committed-but-uncheckpointed frames in the WAL by SIGKILLing the writer, +// reproducing the state a crashed kilo process leaves behind +async function createWalDbWithPendingFrames(file: string) { + const script = [ + `const { Database } = require("bun:sqlite")`, + `const db = new Database(${JSON.stringify(file)})`, + `db.run("PRAGMA journal_mode = WAL")`, + `db.run("PRAGMA wal_autocheckpoint = 0")`, + `db.run("CREATE TABLE t (x)")`, + `db.run("INSERT INTO t VALUES (1)")`, + `console.log("ready")`, + `setInterval(() => {}, 1000)`, + ].join("\n") + const child = Bun.spawn([process.execPath, "-e", script], { stdout: "pipe" }) + const reader = child.stdout.getReader() + await reader.read() + child.kill("SIGKILL") + await child.exited +} + +describe("DbPreflight", () => { + test("skips in-memory databases", () => { + expect(() => DbPreflight.assertWritable(":memory:")).not.toThrow() + }) + + test("accepts a writable database", async () => { + await using tmp = await tmpdir() + const file = path.join(tmp.path, "kilo.db") + createWalDb(file) + expect(() => DbPreflight.assertWritable(file)).not.toThrow() + }) + + test("names the offending file for a read-only sidecar outside the kilo data dir", async () => { + if (skip) return + await using tmp = await tmpdir() + const file = path.join(tmp.path, "kilo.db") + // a clean close deletes the sidecars on some platforms; a killed writer always leaves them + await createWalDbWithPendingFrames(file) + chmodSync(`${file}-wal`, 0o444) + expect(() => DbPreflight.assertWritable(file)).toThrow(`Database file is not writable: ${file}-wal`) + chmodSync(`${file}-wal`, 0o644) + }) + + test("repairs read-only files inside the trusted dir", async () => { + if (skip) return + await using tmp = await tmpdir() + const file = path.join(tmp.path, "kilo.db") + await createWalDbWithPendingFrames(file) + chmodSync(file, 0o444) + chmodSync(`${file}-wal`, 0o444) + expect(() => DbPreflight.assertWritable(file, tmp.path)).not.toThrow() + expect(writable(file)).toBe(true) + expect(writable(`${file}-wal`)).toBe(true) + }) + + test("reports a missing directory as missing, not as read-only", async () => { + await using tmp = await tmpdir() + const dir = path.join(tmp.path, "absent") + const file = path.join(dir, "kilo.db") + expect(() => DbPreflight.assertWritable(file)).toThrow(`Database directory does not exist: ${dir}`) + }) + + test("rejects a read-only directory when WAL files must be created", async () => { + if (skip) return + await using tmp = await tmpdir() + const dir = path.join(tmp.path, "locked") + const file = path.join(dir, "kilo.db") + await Bun.write(path.join(dir, ".keep"), "") + chmodSync(dir, 0o555) + try { + expect(() => DbPreflight.assertWritable(file)).toThrow(`Database directory is not writable: ${dir}`) + } finally { + chmodSync(dir, 0o755) + } + }) + + test("pending WAL frames with a read-only sidecar fail with the actionable error, and repair recovers the data", async () => { + if (skip) return + await using tmp = await tmpdir() + const file = path.join(tmp.path, "kilo.db") + await createWalDbWithPendingFrames(file) + chmodSync(`${file}-wal`, 0o444) + + // without repair (untrusted dir) the wiring in layerFromPath surfaces the clear error + expect(() => KiloDatabase.layerFromPath(file)).toThrow(`Database file is not writable: ${file}-wal`) + + // with repair the startup pragma sequence succeeds and the committed row survives + DbPreflight.assertWritable(file, tmp.path) + const db = new Database(file, { readwrite: true, create: true }) + db.run("PRAGMA journal_mode = WAL") + db.run("PRAGMA wal_checkpoint(PASSIVE)") + expect(db.query("SELECT x FROM t").all()).toEqual([{ x: 1 }]) + db.close() + }) +}) diff --git a/packages/core/test/kilocode/integration-settlement.test.ts b/packages/core/test/kilocode/integration-settlement.test.ts new file mode 100644 index 00000000000..0eae2b2aad9 --- /dev/null +++ b/packages/core/test/kilocode/integration-settlement.test.ts @@ -0,0 +1,334 @@ +import { describe, expect } from "bun:test" +import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect" +import * as TestClock from "effect/testing/TestClock" +import { Integration } from "@opencode-ai/core/integration" +import { Credential } from "@opencode-ai/core/credential" +import { EventV2 } from "@opencode-ai/core/event" +import { it } from "../lib/effect" + +// Regression coverage for Kilo's OAuth attempt settlement guards: persistence +// happens before completion is exposed, and settlement is atomic with +// cancellation, expiry, and timeouts. + +const layer = Integration.locationLayer.pipe( + Layer.provide(EventV2.defaultLayer), + Layer.provide( + Layer.mock(Credential.Service)({ + create: () => Effect.die("unexpected credential creation"), + list: () => Effect.succeed([]), + }), + ), +) + +function connectionLayer( + created: Array<{ + integrationID: Integration.ID + label?: string + value: Credential.Info + }>, +) { + return Integration.locationLayer.pipe( + Layer.provide(EventV2.defaultLayer), + Layer.provide( + Layer.mock(Credential.Service)({ + create: (input) => + Effect.sync(() => { + created.push(input) + return new Credential.Stored({ + id: Credential.ID.create(), + integrationID: input.integrationID, + label: input.label ?? "default", + value: input.value, + }) + }), + list: () => Effect.succeed([]), + }), + ), + ) +} + +describe("Integration settlement guards", () => { + it.effect("fails auto OAuth when credential persistence fails", () => { + const failed = Integration.locationLayer.pipe( + Layer.provide(EventV2.defaultLayer), + Layer.provide( + Layer.mock(Credential.Service)({ + create: () => Effect.die(new Error("database unavailable")), + list: () => Effect.succeed([]), + }), + ), + ) + return Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("browser") + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }), + authorize: () => + Effect.succeed({ + mode: "auto" as const, + url: "https://example.com/authorize", + instructions: "Sign in", + callback: Effect.succeed( + new Credential.OAuth({ type: "oauth", methodID, access: "access", refresh: "refresh", expires: 1 }), + ), + }), + }), + ) + + const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} }) + yield* Effect.yieldNow + expect(yield* integrations.attempt.status(attempt.attemptID)).toMatchObject({ + status: "failed", + message: expect.stringContaining("database unavailable"), + }) + }).pipe(Effect.provide(failed)) + }) + + it.effect("fails code OAuth when credential persistence fails", () => { + const failed = Integration.locationLayer.pipe( + Layer.provide(EventV2.defaultLayer), + Layer.provide( + Layer.mock(Credential.Service)({ + create: () => Effect.die(new Error("database unavailable")), + list: () => Effect.succeed([]), + }), + ), + ) + return Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("chatgpt") + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), + authorize: () => + Effect.succeed({ + mode: "code" as const, + url: "https://example.com/authorize", + instructions: "Paste the code", + callback: () => + Effect.succeed( + new Credential.OAuth({ type: "oauth", methodID, access: "access", refresh: "refresh", expires: 1 }), + ), + }), + }), + ) + + const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} }) + const exit = yield* integrations.attempt.complete({ attemptID: attempt.attemptID, code: "1234" }).pipe( + Effect.exit, + ) + expect(Exit.isFailure(exit)).toBe(true) + if (Exit.isFailure(exit)) expect(Cause.pretty(exit.cause)).toContain("database unavailable") + expect(yield* integrations.attempt.status(attempt.attemptID)).toMatchObject({ + status: "failed", + message: expect.stringContaining("database unavailable"), + }) + }).pipe(Effect.provide(failed)) + }) + + it.effect("lets OAuth persistence finish after concurrent cancellation", () => + Effect.gen(function* () { + const started = yield* Deferred.make() + const release = yield* Deferred.make() + const created: Credential.Stored[] = [] + const delayed = Integration.locationLayer.pipe( + Layer.provide(EventV2.defaultLayer), + Layer.provide( + Layer.mock(Credential.Service)({ + create: (input) => + Effect.gen(function* () { + yield* Deferred.succeed(started, undefined) + yield* Deferred.await(release) + const credential = new Credential.Stored({ + id: Credential.ID.create(), + integrationID: input.integrationID, + label: input.label ?? "default", + value: input.value, + }) + created.push(credential) + return credential + }), + list: () => Effect.succeed([]), + }), + ), + ) + + yield* Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("chatgpt") + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), + authorize: () => + Effect.succeed({ + mode: "code" as const, + url: "https://example.com/authorize", + instructions: "Paste the code", + callback: () => + Effect.succeed( + new Credential.OAuth({ type: "oauth", methodID, access: "access", refresh: "refresh", expires: 1 }), + ), + }), + }), + ) + + const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} }) + const fiber = yield* integrations.attempt + .complete({ attemptID: attempt.attemptID, code: "1234" }) + .pipe(Effect.forkScoped) + yield* Deferred.await(started) + yield* integrations.attempt.cancel(attempt.attemptID) + yield* Deferred.succeed(release, undefined) + yield* Fiber.join(fiber) + + expect(created).toHaveLength(1) + expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({ + status: "complete", + time: attempt.time, + }) + }).pipe(Effect.provide(delayed)) + }), + ) + + it.effect("keeps a code OAuth attempt while its callback is completing", () => { + const created: Array<{ + integrationID: Integration.ID + label?: string + value: Credential.Info + }> = [] + return Effect.gen(function* () { + const started = yield* Deferred.make() + const release = yield* Deferred.make() + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("chatgpt") + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), + authorize: () => + Effect.succeed({ + mode: "code" as const, + url: "https://example.com/authorize", + instructions: "Paste the code", + callback: () => + Deferred.succeed(started, undefined).pipe( + Effect.andThen(Deferred.await(release)), + Effect.as( + new Credential.OAuth({ type: "oauth", methodID, access: "access", refresh: "refresh", expires: 1 }), + ), + ), + }), + }), + ) + + const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} }) + const fiber = yield* integrations.attempt + .complete({ attemptID: attempt.attemptID, code: "1234" }) + .pipe(Effect.forkScoped) + yield* Deferred.await(started) + yield* integrations.attempt.cancel(attempt.attemptID) + yield* Deferred.succeed(release, undefined) + yield* Fiber.join(fiber) + + expect(created).toHaveLength(1) + expect(yield* integrations.attempt.status(attempt.attemptID)).toMatchObject({ status: "complete" }) + }).pipe(Effect.provide(connectionLayer(created))) + }) + + it.effect("fails and releases code OAuth attempts when the callback times out", () => + Effect.gen(function* () { + const started = yield* Deferred.make() + const state = { closed: false } + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("chatgpt") + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), + authorize: () => + Effect.addFinalizer(() => Effect.sync(() => (state.closed = true))).pipe( + Effect.as({ + mode: "code" as const, + url: "https://example.com/authorize", + instructions: "Paste the code", + callback: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)), + }), + ), + }), + ) + + const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} }) + const fiber = yield* integrations.attempt + .complete({ attemptID: attempt.attemptID, code: "1234" }) + .pipe(Effect.exit, Effect.forkScoped) + yield* Deferred.await(started) + yield* TestClock.adjust(Duration.seconds(30)) + const exit = yield* Fiber.join(fiber) + expect(Exit.isFailure(exit)).toBe(true) + yield* Effect.yieldNow + expect(yield* integrations.attempt.status(attempt.attemptID)).toMatchObject({ status: "failed" }) + expect(state.closed).toBe(true) + }).pipe(Effect.provide(layer)), + ) + + it.effect("fails and releases OAuth attempts when credential persistence times out", () => + Effect.gen(function* () { + const started = yield* Deferred.make() + let closed = false + const stalled = Integration.locationLayer.pipe( + Layer.provide(EventV2.defaultLayer), + Layer.provide( + Layer.mock(Credential.Service)({ + create: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Effect.never)), + list: () => Effect.succeed([]), + }), + ), + ) + + yield* Effect.gen(function* () { + const integrations = yield* Integration.Service + const integrationID = Integration.ID.make("openai") + const methodID = Integration.MethodID.make("chatgpt") + yield* integrations.update((editor) => + editor.method.update({ + integrationID, + method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }), + authorize: () => + Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe( + Effect.as({ + mode: "code" as const, + url: "https://example.com/authorize", + instructions: "Paste the code", + callback: () => + Effect.succeed( + new Credential.OAuth({ type: "oauth", methodID, access: "access", refresh: "refresh", expires: 1 }), + ), + }), + ), + }), + ) + + const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} }) + const fiber = yield* integrations.attempt + .complete({ attemptID: attempt.attemptID, code: "1234" }) + .pipe(Effect.exit, Effect.forkScoped) + yield* Deferred.await(started) + yield* TestClock.adjust(Duration.seconds(30)) + const exit = yield* Fiber.join(fiber) + expect(Exit.isFailure(exit)).toBe(true) + yield* Effect.yieldNow + expect(yield* integrations.attempt.status(attempt.attemptID)).toMatchObject({ status: "failed" }) + expect(closed).toBe(true) + }).pipe(Effect.provide(stalled)) + }), + ) +}) diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index 0bd3a2497a9..69dba2ae0a5 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -27,17 +27,15 @@ import { ApplicationTools } from "../src/tool/application-tools" const applicationTools = ApplicationTools.layer const it = testEffect( Layer.merge( - applicationTools, + Layer.mergeAll(applicationTools, Database.defaultLayer, EventV2.defaultLayer), LocationServiceMap.layer.pipe( Layer.provide(applicationTools), Layer.provide( Layer.mergeAll( Project.defaultLayer, EventV2.defaultLayer, - Credential.layer.pipe( - Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)), - Layer.provide(EventV2.defaultLayer), - ), + Credential.defaultLayer, + Credential.layer.pipe(Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh))), Npm.defaultLayer, ModelsDev.defaultLayer, FSUtil.defaultLayer, diff --git a/packages/core/test/move-session.test.ts b/packages/core/test/move-session.test.ts index 0af8da1b9f6..5f7fbb16d3a 100644 --- a/packages/core/test/move-session.test.ts +++ b/packages/core/test/move-session.test.ts @@ -11,6 +11,7 @@ import { Git } from "@opencode-ai/core/git" import { EventV2 } from "@opencode-ai/core/event" import { Project } from "@opencode-ai/core/project" import { ProjectTable } from "@opencode-ai/core/project/sql" +import { ProjectDirectories } from "@opencode-ai/core/project/directories" import { AbsolutePath } from "@opencode-ai/core/schema" import { SessionV2 } from "@opencode-ai/core/session" import { SessionExecution } from "@opencode-ai/core/session/execution" @@ -22,11 +23,13 @@ import { testEffect } from "./lib/effect" const database = Database.layerFromPath(":memory:") const events = EventV2.layer.pipe(Layer.provide(database)) +const directories = ProjectDirectories.layer.pipe(Layer.provide(database), Layer.provide(events)) const projector = SessionProjector.layer.pipe(Layer.provide(database), Layer.provide(events)) const project = Project.layer.pipe( Layer.provide(database), Layer.provide(FSUtil.defaultLayer), Layer.provide(Git.defaultLayer), + Layer.provide(directories), ) const store = SessionStore.layer.pipe(Layer.provide(database)) const sessions = SessionV2.layer.pipe( @@ -45,7 +48,7 @@ const layer = MoveSession.layer.pipe( Layer.provide(sessions), ) const it = testEffect( - Layer.mergeAll(layer, database, events, project, projector, store, SessionExecution.noopLayer, sessions), + Layer.mergeAll(layer, database, events, directories, project, projector, store, SessionExecution.noopLayer, sessions), ) function abs(input: string) { diff --git a/packages/core/test/plugin/models-dev.test.ts b/packages/core/test/plugin/models-dev.test.ts index 3abc39464f9..8cdfc066cd2 100644 --- a/packages/core/test/plugin/models-dev.test.ts +++ b/packages/core/test/plugin/models-dev.test.ts @@ -2,7 +2,7 @@ import path from "path" import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { Connector } from "@opencode-ai/core/connector" +import { Integration } from "@opencode-ai/core/integration" import { Credential } from "@opencode-ai/core/credential" import { Database } from "@opencode-ai/core/database/database" import { EventV2 } from "@opencode-ai/core/event" @@ -23,14 +23,25 @@ const locationLayer = Layer.succeed( ) const plugins = PluginV2.layer.pipe(Layer.provide(events)) const policy = Policy.layer.pipe(Layer.provide(locationLayer)) -const credentials = Credential.layer.pipe(Layer.provide(Database.layerFromPath(":memory:")), Layer.provide(events)) -const catalog = Catalog.layer.pipe(Layer.provide(Layer.mergeAll(events, locationLayer, plugins, policy, credentials))) -const connectors = Connector.locationLayer.pipe(Layer.provide(credentials), Layer.provide(events)) -const layer = Layer.mergeAll(catalog, connectors, credentials, events, locationLayer, plugins) +const connections = Credential.layer.pipe( + Layer.fresh, + Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)), + Layer.provide(events), +) +const catalog = Catalog.layer.pipe(Layer.provide(Layer.mergeAll(events, locationLayer, plugins, policy, connections))) +const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections)) +const layer = Layer.mergeAll( + catalog.pipe(Layer.provide(connections)), + integrations, + connections, + events, + locationLayer, + plugins, +) const it = testEffect(layer) describe("ModelsDevPlugin", () => { - it.effect("registers key connectors for providers with environment variables", () => + it.effect("registers key methods for providers with environment variables", () => Effect.acquireUseRelease( Effect.sync(() => { const previous = { @@ -44,14 +55,19 @@ describe("ModelsDevPlugin", () => { () => Effect.gen(function* () { yield* ModelsDevPlugin.effect - const connectors = yield* Connector.Service - expect(yield* connectors.list()).toEqual([ - new Connector.Info({ - id: Connector.ID.make("acme"), + const integrations = yield* Integration.Service + expect(yield* integrations.list()).toEqual([ + new Integration.Info({ + id: Integration.ID.make("acme"), name: "Acme", methods: [ - new Connector.KeyMethod({ id: Connector.MethodID.make("api-key"), type: "key", label: "API Key" }), + new Integration.KeyMethod({ type: "key" }), + new Integration.EnvMethod({ + type: "env", + names: ["ACME_API_KEY"], + }), ], + connections: [], }), ]) }).pipe(Effect.provide(ModelsDev.defaultLayer)), diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index d5fe1df0275..c4bdd806c9d 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Credential } from "@opencode-ai/core/credential" -import { Connector } from "@opencode-ai/core/connector" +import { Integration } from "@opencode-ai/core/integration" import { Database } from "@opencode-ai/core/database/database" import { Catalog } from "@opencode-ai/core/catalog" import { EventV2 } from "@opencode-ai/core/event" @@ -14,14 +14,15 @@ import { location } from "../fixture/location" import { testEffect } from "../lib/effect" import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper" +const database = Database.layerFromPath(":memory:").pipe(Layer.fresh) +const preferences = Credential.layer.pipe(Layer.provide(database)) +const accounts = Layer.merge( + Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)), + preferences, +) const itWithAccount = testEffect( Catalog.locationLayer.pipe( - Layer.provideMerge( - Credential.layer.pipe( - Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)), - Layer.provide(EventV2.defaultLayer), - ), - ), + Layer.provideMerge(accounts), Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge( Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))), @@ -83,8 +84,7 @@ describe("AzurePlugin", () => { const credentials = yield* Credential.Service const catalog = yield* Catalog.Service yield* credentials.create({ - connectorID: Connector.ID.make("azure"), - methodID: Connector.MethodID.make("api-key"), + integrationID: Integration.ID.make("azure"), value: new Credential.Key({ type: "key", key: "key", diff --git a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts index 235f76d039d..208ab8710d3 100644 --- a/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts +++ b/packages/core/test/plugin/provider-cloudflare-workers-ai.test.ts @@ -1,7 +1,7 @@ import { describe, expect } from "bun:test" import { Effect, Layer } from "effect" import { Credential } from "@opencode-ai/core/credential" -import { Connector } from "@opencode-ai/core/connector" +import { Integration } from "@opencode-ai/core/integration" import { Database } from "@opencode-ai/core/database/database" import { Catalog } from "@opencode-ai/core/catalog" import { Location } from "@opencode-ai/core/location" @@ -15,14 +15,15 @@ import { location } from "../fixture/location" import { testEffect } from "../lib/effect" import { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper" +const database = Database.layerFromPath(":memory:").pipe(Layer.fresh) +const preferences = Credential.layer.pipe(Layer.provide(database)) +const accounts = Layer.merge( + Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)), + preferences, +) const itWithAccount = testEffect( Catalog.locationLayer.pipe( - Layer.provideMerge( - Credential.layer.pipe( - Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)), - Layer.provide(EventV2.defaultLayer), - ), - ), + Layer.provideMerge(accounts), Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge( Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))), @@ -137,8 +138,7 @@ describe("CloudflareWorkersAIPlugin", () => { const credentials = yield* Credential.Service const catalog = yield* Catalog.Service yield* credentials.create({ - connectorID: Connector.ID.make("cloudflare-workers-ai"), - methodID: Connector.MethodID.make("api-key"), + integrationID: Integration.ID.make("cloudflare-workers-ai"), value: new Credential.Key({ type: "key", key: "account-key", diff --git a/packages/core/test/plugin/provider-gitlab.test.ts b/packages/core/test/plugin/provider-gitlab.test.ts index 5b82cda959d..dab52a1f7fa 100644 --- a/packages/core/test/plugin/provider-gitlab.test.ts +++ b/packages/core/test/plugin/provider-gitlab.test.ts @@ -1,7 +1,7 @@ import { describe, expect, mock } from "bun:test" import { Effect, Layer } from "effect" import { Credential } from "@opencode-ai/core/credential" -import { Connector } from "@opencode-ai/core/connector" +import { Integration } from "@opencode-ai/core/integration" import { Database } from "@opencode-ai/core/database/database" import { Catalog } from "@opencode-ai/core/catalog" import { EventV2 } from "@opencode-ai/core/event" @@ -15,6 +15,12 @@ import { testEffect } from "../lib/effect" import { it, model, npmLayer, withEnv } from "./provider-helper" const gitlabSDKOptions: Record[] = [] +const database = Database.layerFromPath(":memory:").pipe(Layer.fresh) +const preferences = Credential.layer.pipe(Layer.provide(database)) +const accounts = Layer.merge( + Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)), + preferences, +) void mock.module("gitlab-ai-provider", () => ({ VERSION: "test-version", @@ -31,12 +37,7 @@ void mock.module("gitlab-ai-provider", () => ({ const itWithAccount = testEffect( Catalog.locationLayer.pipe( - Layer.provideMerge( - Credential.layer.pipe( - Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)), - Layer.provide(EventV2.defaultLayer), - ), - ), + Layer.provideMerge(accounts), Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge( Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/") }))), @@ -174,8 +175,7 @@ describe("GitLabPlugin", () => { const credentials = yield* Credential.Service const catalog = yield* Catalog.Service yield* credentials.create({ - connectorID: Connector.ID.make("gitlab"), - methodID: Connector.MethodID.make("api-key"), + integrationID: Integration.ID.make("gitlab"), value: new Credential.Key({ type: "key", key: "account-token" }), }) yield* plugin.add(GitLabPlugin) @@ -208,10 +208,10 @@ describe("GitLabPlugin", () => { const credentials = yield* Credential.Service const catalog = yield* Catalog.Service yield* credentials.create({ - connectorID: Connector.ID.make("gitlab"), - methodID: Connector.MethodID.make("oauth"), + integrationID: Integration.ID.make("gitlab"), value: new Credential.OAuth({ type: "oauth", + methodID: Integration.MethodID.make("oauth"), refresh: "refresh-token", access: "account-oauth-token", expires: 9999999999999, diff --git a/packages/core/test/plugin/provider-helper.ts b/packages/core/test/plugin/provider-helper.ts index cc8ebbf396c..f9334480d1f 100644 --- a/packages/core/test/plugin/provider-helper.ts +++ b/packages/core/test/plugin/provider-helper.ts @@ -3,7 +3,7 @@ import type { LanguageModelV3 } from "@ai-sdk/provider" import { expect } from "bun:test" import { Effect, Layer, Option } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { Connector } from "@opencode-ai/core/connector" +import { Integration } from "@opencode-ai/core/integration" import { Credential } from "@opencode-ai/core/credential" import { EventV2 } from "@opencode-ai/core/event" import { Location } from "@opencode-ai/core/location" @@ -48,15 +48,24 @@ export const catalogLayer = Layer.succeed( }), ) -const connectors = Connector.locationLayer.pipe( +const integrations = Integration.locationLayer.pipe( Layer.provide(EventV2.defaultLayer), - Layer.provide(Layer.mock(Credential.Service)({ create: () => Effect.die("unexpected credential creation") })), + Layer.provide( + Layer.mock(Credential.Service)({ + create: () => Effect.die("unexpected credential creation"), + list: () => Effect.succeed([]), + }), + ), ) export const it = testEffect( Catalog.locationLayer.pipe( - Layer.provideMerge(connectors), - Layer.provideMerge(Layer.mock(Credential.Service)({ activeAll: () => Effect.succeed(new Map()) })), + Layer.provideMerge(integrations), + Layer.provideMerge( + Layer.mock(Credential.Service)({ + all: () => Effect.succeed([]), + }), + ), Layer.provideMerge(EventV2.defaultLayer), Layer.provideMerge(locationLayer), Layer.provideMerge(npmLayer), diff --git a/packages/core/test/plugin/provider-openai.test.ts b/packages/core/test/plugin/provider-openai.test.ts index f67a4e94495..a317ba4bf7d 100644 --- a/packages/core/test/plugin/provider-openai.test.ts +++ b/packages/core/test/plugin/provider-openai.test.ts @@ -1,17 +1,17 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" -import { Connector } from "@opencode-ai/core/connector" +import { Integration } from "@opencode-ai/core/integration" import { ModelV2 } from "@opencode-ai/core/model" import { PluginV2 } from "@opencode-ai/core/plugin" import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai" import { ProviderV2 } from "@opencode-ai/core/provider" import { fakeSelectorSdk, it, model, provider } from "./provider-helper" -function add(plugin: PluginV2.Interface, connectors: Connector.Interface) { +function add(plugin: PluginV2.Interface, integrations: Integration.Interface) { return plugin.add({ ...OpenAIPlugin, - effect: OpenAIPlugin.effect.pipe(Effect.provideService(Connector.Service, connectors)), + effect: OpenAIPlugin.effect.pipe(Effect.provideService(Integration.Service, integrations)), }) } @@ -19,15 +19,15 @@ describe("OpenAIPlugin", () => { it.effect("registers browser and headless ChatGPT OAuth methods", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service - yield* add(plugin, yield* Connector.Service) - expect((yield* (yield* Connector.Service).get(Connector.ID.make("openai")))?.methods).toEqual([ - new Connector.OAuthMethod({ - id: Connector.MethodID.make("chatgpt-browser"), + yield* add(plugin, yield* Integration.Service) + expect((yield* (yield* Integration.Service).get(Integration.ID.make("openai")))?.methods).toEqual([ + new Integration.OAuthMethod({ + id: Integration.MethodID.make("chatgpt-browser"), type: "oauth", label: "ChatGPT Pro/Plus (browser)", }), - new Connector.OAuthMethod({ - id: Connector.MethodID.make("chatgpt-headless"), + new Integration.OAuthMethod({ + id: Integration.MethodID.make("chatgpt-headless"), type: "oauth", label: "ChatGPT Pro/Plus (headless)", }), @@ -38,7 +38,7 @@ describe("OpenAIPlugin", () => { it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service - yield* add(plugin, yield* Connector.Service) + yield* add(plugin, yield* Integration.Service) const result = yield* plugin.trigger( "aisdk.sdk", { @@ -55,7 +55,7 @@ describe("OpenAIPlugin", () => { it.effect("ignores non-OpenAI SDK packages", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service - yield* add(plugin, yield* Connector.Service) + yield* add(plugin, yield* Integration.Service) const result = yield* plugin.trigger( "aisdk.sdk", { model: model("openai", "gpt-5"), package: "@ai-sdk/openai-compatible", options: { name: "openai" } }, @@ -69,7 +69,7 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const plugin = yield* PluginV2.Service const calls: string[] = [] - yield* add(plugin, yield* Connector.Service) + yield* add(plugin, yield* Integration.Service) const result = yield* plugin.trigger( "aisdk.language", { @@ -90,7 +90,7 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const plugin = yield* PluginV2.Service const calls: string[] = [] - yield* add(plugin, yield* Connector.Service) + yield* add(plugin, yield* Integration.Service) const result = yield* plugin.trigger( "aisdk.language", { model: model("anthropic", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} }, @@ -105,7 +105,7 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const plugin = yield* PluginV2.Service const catalog = yield* Catalog.Service - yield* add(plugin, yield* Connector.Service) + yield* add(plugin, yield* Integration.Service) const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("openai", { api: { type: "aisdk", package: "@ai-sdk/openai" } }) @@ -124,7 +124,7 @@ describe("OpenAIPlugin", () => { Effect.gen(function* () { const plugin = yield* PluginV2.Service const catalog = yield* Catalog.Service - yield* add(plugin, yield* Connector.Service) + yield* add(plugin, yield* Integration.Service) const transform = yield* catalog.transform() yield* transform((catalog) => { const item = provider("custom-openai") diff --git a/packages/core/test/plugin/provider-snowflake-cortex.test.ts b/packages/core/test/plugin/provider-snowflake-cortex.test.ts index f7029459784..ff5ec4ba451 100644 --- a/packages/core/test/plugin/provider-snowflake-cortex.test.ts +++ b/packages/core/test/plugin/provider-snowflake-cortex.test.ts @@ -72,6 +72,48 @@ describe("SnowflakeCortexPlugin", () => { ), ) + it.effect("uses SNOWFLAKE_CORTEX_TOKEN env var", () => + withEnv({ SNOWFLAKE_CORTEX_TOKEN: "oauth-token", SNOWFLAKE_CORTEX_PAT: undefined }, () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(SnowflakeCortexPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("snowflake-cortex", "claude-sonnet-4-6"), + package: "@ai-sdk/openai-compatible", + options: { name: "snowflake-cortex", baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1" }, + }, + {}, + ) + expect(result.sdk).toBeDefined() + }), + ), + ) + + it.effect("falls back to options.token when no Snowflake env token is set", () => + withEnv({ SNOWFLAKE_CORTEX_TOKEN: undefined, SNOWFLAKE_CORTEX_PAT: undefined }, () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(SnowflakeCortexPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("snowflake-cortex", "claude-sonnet-4-6"), + package: "@ai-sdk/openai-compatible", + options: { + name: "snowflake-cortex", + baseURL: "https://test.snowflakecomputing.com/api/v2/cortex/v1", + token: "options-token", + }, + }, + {}, + ) + expect(result.sdk).toBeDefined() + }), + ), + ) + it.effect("sets includeUsage on the SDK options", () => withEnv({ SNOWFLAKE_CORTEX_PAT: "test-pat" }, () => Effect.gen(function* () { diff --git a/packages/core/test/project-copy.test.ts b/packages/core/test/project-copy.test.ts index f1ba10fb091..823d8d842ec 100644 --- a/packages/core/test/project-copy.test.ts +++ b/packages/core/test/project-copy.test.ts @@ -12,23 +12,28 @@ import { EventV2 } from "@opencode-ai/core/event" import { Project } from "@opencode-ai/core/project" import { ProjectDirectoryTable, ProjectTable } from "@opencode-ai/core/project/sql" import { ProjectCopy } from "@opencode-ai/core/project/copy" +import { ProjectDirectories } from "@opencode-ai/core/project/directories" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" const databaseLayer = Database.layerFromPath(":memory:") const eventLayer = EventV2.layer.pipe(Layer.provide(databaseLayer)) +const directoriesLayer = ProjectDirectories.layer.pipe(Layer.provide(databaseLayer)) const copyLayer = ProjectCopy.layer.pipe( Layer.provide(databaseLayer), + Layer.provide(directoriesLayer), Layer.provide(eventLayer), Layer.provide(FSUtil.defaultLayer), Layer.provide(Git.defaultLayer), ) -const it = testEffect(Layer.mergeAll(copyLayer, databaseLayer, eventLayer)) +const it = testEffect(Layer.mergeAll(copyLayer, databaseLayer, eventLayer, directoriesLayer)) function abs(input: string) { return AbsolutePath.make(input) } +const gitWorktree = ProjectCopy.StrategyID.make("git_worktree") + async function initRepo(directory: string) { await $`git init`.cwd(directory).quiet() await $`git config core.fsmonitor false`.cwd(directory).quiet() @@ -55,7 +60,7 @@ function setup() { .pipe(Effect.orDie) yield* db .insert(ProjectDirectoryTable) - .values({ project_id: projectID, directory: sourceDirectory, type: "main" }) + .values({ project_id: projectID, directory: sourceDirectory }) .run() .pipe(Effect.orDie) return { root, sourceDirectory, projectID, db } @@ -65,7 +70,7 @@ function setup() { function stored(projectID: Project.ID) { return Database.Service.use(({ db }) => db - .select({ directory: ProjectDirectoryTable.directory, type: ProjectDirectoryTable.type }) + .select({ directory: ProjectDirectoryTable.directory, strategy: ProjectDirectoryTable.strategy }) .from(ProjectDirectoryTable) .where(eq(ProjectDirectoryTable.project_id, projectID)) .all() @@ -77,18 +82,40 @@ function stored(projectID: Project.ID) { } describe("ProjectCopy", () => { - it.live("detects linked git worktrees but not root checkouts", () => + it.effect("accepts arbitrary non-empty strategy ids", () => + Effect.sync(() => { + expect(String(ProjectCopy.StrategyID.make("acme/snapshot"))).toBe("acme/snapshot") + expect(() => ProjectCopy.StrategyID.make(" acme/snapshot ")).toThrow() + expect(() => ProjectCopy.StrategyID.make(" ")).toThrow() + }), + ) + + it.effect("rejects duplicate strategies and reports unavailable ids", () => Effect.gen(function* () { const input = yield* setup() const copy = yield* ProjectCopy.Service - const target = abs(`${input.root.path}-copy-detected`) - yield* Effect.addFinalizer(() => - Effect.promise(() => fs.rm(target, { recursive: true, force: true })).pipe(Effect.ignore), - ) - yield* Effect.promise(() => $`git worktree add --detach ${target} HEAD`.cwd(input.root.path).quiet()) + const strategy: ProjectCopy.Strategy = { + id: ProjectCopy.StrategyID.make("test/duplicate"), + create: () => Effect.die("unused"), + remove: () => Effect.die("unused"), + list: () => Effect.succeed([]), + } - expect(yield* copy.detect({ directory: input.sourceDirectory })).toBeUndefined() - expect(yield* copy.detect({ directory: target })).toBe("git_worktree") + yield* copy.register(strategy) + expect(yield* copy.register(strategy).pipe(Effect.flip)).toBeInstanceOf(ProjectCopy.DuplicateStrategyError) + + const unavailable = ProjectCopy.StrategyID.make("acme/missing") + const error = yield* copy + .create({ + projectID: input.projectID, + strategy: unavailable, + sourceDirectory: input.sourceDirectory, + directory: abs(`${input.root.path}-missing-strategy`), + name: "copy", + }) + .pipe(Effect.flip) + expect(error).toBeInstanceOf(ProjectCopy.StrategyUnavailableError) + if (error instanceof ProjectCopy.StrategyUnavailableError) expect(error.strategy).toBe(unavailable) }), ) @@ -110,7 +137,7 @@ describe("ProjectCopy", () => { const created = yield* copy.create({ projectID: input.projectID, - strategy: "git_worktree", + strategy: gitWorktree, sourceDirectory: input.sourceDirectory, directory: parent, name: "copy", @@ -118,15 +145,15 @@ describe("ProjectCopy", () => { expect(created.directory).toBe(target) expect(yield* stored(input.projectID)).toEqual( [ - { directory: input.sourceDirectory, type: "main" as const }, - { directory: created.directory, type: "git_worktree" as const }, + { directory: input.sourceDirectory, strategy: null }, + { directory: created.directory, strategy: "git_worktree" }, ].toSorted((a, b) => a.directory.localeCompare(b.directory)), ) expect(Array.from(yield* Fiber.join(fiber))[0]?.data).toEqual({ projectID: input.projectID }) yield* copy.remove({ projectID: input.projectID, directory: created.directory, force: false }) - expect(yield* stored(input.projectID)).toEqual([{ directory: input.sourceDirectory, type: "main" as const }]) + expect(yield* stored(input.projectID)).toEqual([{ directory: input.sourceDirectory, strategy: null }]) expect(yield* Effect.promise(() => Bun.file(target).exists())).toBe(false) }), ) @@ -142,7 +169,7 @@ describe("ProjectCopy", () => { ) const created = yield* copy.create({ projectID: input.projectID, - strategy: "git_worktree", + strategy: gitWorktree, sourceDirectory: input.sourceDirectory, directory: parent, name: "copy", @@ -158,7 +185,7 @@ describe("ProjectCopy", () => { expect(error.operation).toBe("remove") expect(error.forceRequired).toBe(true) } - expect(yield* stored(input.projectID)).toContainEqual({ directory: created.directory, type: "git_worktree" }) + expect(yield* stored(input.projectID)).toContainEqual({ directory: created.directory, strategy: "git_worktree" }) expect(yield* Effect.promise(() => Bun.file(path.join(created.directory, "dirty.txt")).exists())).toBe(true) yield* copy.remove({ projectID: input.projectID, directory: created.directory, force: true }) @@ -166,6 +193,28 @@ describe("ProjectCopy", () => { }), ) + it.live("preserves copies whose stored strategy is unavailable", () => + Effect.gen(function* () { + const input = yield* setup() + const copy = yield* ProjectCopy.Service + const unavailable = abs(`${input.root.path}-copy-unavailable`) + yield* Effect.promise(() => fs.mkdir(unavailable)) + yield* Effect.addFinalizer(() => Effect.promise(() => fs.rm(unavailable, { recursive: true, force: true }))) + yield* input.db + .insert(ProjectDirectoryTable) + .values({ project_id: input.projectID, directory: unavailable, strategy: "acme/missing" }) + .run() + .pipe(Effect.orDie) + + const error = yield* copy + .remove({ projectID: input.projectID, directory: unavailable, force: false }) + .pipe(Effect.flip) + + expect(error).toBeInstanceOf(ProjectCopy.StrategyUnavailableError) + expect(yield* stored(input.projectID)).toContainEqual({ directory: unavailable, strategy: "acme/missing" }) + }), + ) + it.live("adds a numeric suffix when a copy directory already exists", () => Effect.gen(function* () { const input = yield* setup() @@ -181,7 +230,7 @@ describe("ProjectCopy", () => { const created = yield* copy.create({ projectID: input.projectID, - strategy: "git_worktree", + strategy: gitWorktree, sourceDirectory: input.sourceDirectory, directory: parent, name: "copy", @@ -219,7 +268,7 @@ describe("ProjectCopy", () => { const error = yield* copy .create({ projectID: input.projectID, - strategy: "git_worktree", + strategy: gitWorktree, sourceDirectory: input.sourceDirectory, directory: parent, name: "copy", @@ -227,7 +276,8 @@ describe("ProjectCopy", () => { .pipe(Effect.flip) expect(error).toBeInstanceOf(ProjectCopy.DestinationExistsError) - expect(error.directory).toBe(abs(path.join(parent, "copy-10"))) + if (error instanceof ProjectCopy.DestinationExistsError) + expect(error.directory).toBe(abs(path.join(parent, "copy-10"))) }), ) @@ -263,25 +313,30 @@ describe("ProjectCopy", () => { Effect.promise(() => fs.rm(target, { recursive: true, force: true })).pipe(Effect.ignore), ) yield* Effect.promise(() => $`git worktree add --detach ${target} HEAD`.cwd(input.root.path).quiet()) + yield* input.db + .insert(ProjectDirectoryTable) + .values({ project_id: input.projectID, directory: target }) + .run() + .pipe(Effect.orDie) const fiber = yield* events .subscribe(ProjectCopy.Event.Updated) .pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) yield* Effect.yieldNow - yield* copy.refresh({ projectID: input.projectID }) - const discovered = abs(yield* Effect.promise(() => fs.realpath(target))) + expect(yield* copy.refresh({ projectID: input.projectID })).toEqual({ updated: [discovered], removed: [] }) + expect(yield* stored(input.projectID)).toEqual( [ - { directory: input.sourceDirectory, type: "main" as const }, - { directory: discovered, type: "git_worktree" as const }, + { directory: input.sourceDirectory, strategy: null }, + { directory: discovered, strategy: "git_worktree" }, ].toSorted((a, b) => a.directory.localeCompare(b.directory)), ) expect(Array.from(yield* Fiber.join(fiber))[0]?.data).toEqual({ projectID: input.projectID }) yield* Effect.promise(() => $`git worktree remove --force ${target}`.cwd(input.root.path).quiet()) - yield* copy.refresh({ projectID: input.projectID }) - expect(yield* stored(input.projectID)).toEqual([{ directory: input.sourceDirectory, type: "main" as const }]) + expect(yield* copy.refresh({ projectID: input.projectID })).toEqual({ updated: [], removed: [discovered] }) + expect(yield* stored(input.projectID)).toEqual([{ directory: input.sourceDirectory, strategy: null }]) }), ) @@ -303,8 +358,8 @@ describe("ProjectCopy", () => { const discovered = abs(yield* Effect.promise(() => fs.realpath(target))) expect(yield* stored(input.projectID)).toEqual( [ - { directory: input.sourceDirectory, type: "main" as const }, - { directory: discovered, type: "git_worktree" as const }, + { directory: input.sourceDirectory, strategy: null }, + { directory: discovered, strategy: "git_worktree" }, ].toSorted((a, b) => a.directory.localeCompare(b.directory)), ) }), @@ -314,7 +369,27 @@ describe("ProjectCopy", () => { Effect.gen(function* () { const copy = yield* ProjectCopy.Service - yield* copy.refresh({ projectID: Project.ID.make("missing-project") }) + expect(yield* copy.refresh({ projectID: Project.ID.make("missing-project") })).toEqual({ + updated: [], + removed: [], + }) + }), + ) + + it.live("refresh removes missing ordinary checkouts", () => + Effect.gen(function* () { + const input = yield* setup() + const missing = abs(`${input.root.path}-missing-checkout`) + yield* input.db + .insert(ProjectDirectoryTable) + .values({ project_id: input.projectID, directory: missing }) + .run() + .pipe(Effect.orDie) + const copy = yield* ProjectCopy.Service + + expect(yield* copy.refresh({ projectID: input.projectID })).toEqual({ updated: [], removed: [missing] }) + + expect(yield* stored(input.projectID)).not.toContainEqual({ directory: missing, strategy: null }) }), ) }) diff --git a/packages/core/test/project-directories.test.ts b/packages/core/test/project-directories.test.ts new file mode 100644 index 00000000000..c1d2d8801f9 --- /dev/null +++ b/packages/core/test/project-directories.test.ts @@ -0,0 +1,63 @@ +import { describe, expect } from "bun:test" +import { Effect, Layer, Schema } from "effect" +import { Database } from "@opencode-ai/core/database/database" +import { EventV2 } from "@opencode-ai/core/event" +import { Project } from "@opencode-ai/core/project" +import { ProjectDirectories } from "@opencode-ai/core/project/directories" +import { ProjectTable } from "@opencode-ai/core/project/sql" +import { AbsolutePath } from "@opencode-ai/core/schema" +import { testEffect } from "./lib/effect" + +const database = Database.layerFromPath(":memory:") +const events = EventV2.layer.pipe(Layer.provide(database)) +const directories = ProjectDirectories.layer.pipe(Layer.provide(database), Layer.provide(events)) +const it = testEffect(Layer.mergeAll(database, events, directories)) + +const projectID = Project.ID.make("project-directories") +const directory = AbsolutePath.make("/tmp/project-directories") + +function setup() { + return Database.Service.use(({ db }) => + db + .insert(ProjectTable) + .values({ id: projectID, worktree: directory, sandboxes: [], time_created: 1, time_updated: 1 }) + .onConflictDoNothing() + .run() + .pipe(Effect.orDie), + ) +} + +describe("ProjectDirectories", () => { + it.effect("decodes directory schemas", () => + Effect.sync(() => { + expect(Schema.decodeUnknownSync(ProjectDirectories.ListInput)({ projectID })).toEqual({ projectID }) + expect(Schema.decodeUnknownSync(ProjectDirectories.ListOutput)([{ directory }])).toEqual([{ directory }]) + }), + ) + + it.effect("creates once and ignores conflicts", () => + Effect.gen(function* () { + yield* setup() + const service = yield* ProjectDirectories.Service + + expect(yield* service.create({ projectID, directory })).toBe(true) + expect(yield* service.create({ projectID, directory, strategy: "git_worktree" })).toBe(false) + expect(yield* service.list(projectID)).toEqual([{ directory, strategy: undefined }]) + }), + ) + + it.effect("replaces the strategy when requested", () => + Effect.gen(function* () { + yield* setup() + const service = yield* ProjectDirectories.Service + yield* service.create({ projectID, directory, strategy: "old/strategy" }) + + expect(yield* service.create({ projectID, directory, strategy: "new/strategy", behavior: "replace" })).toBe(true) + expect(yield* service.create({ projectID, directory, strategy: "new/strategy", behavior: "replace" })).toBe(false) + expect(yield* service.create({ projectID, directory, behavior: "replace" })).toBe(true) + expect(yield* service.create({ projectID, directory, behavior: "replace" })).toBe(false) + expect(yield* service.create({ projectID, directory, strategy: "new/strategy", behavior: "replace" })).toBe(true) + expect(yield* service.list(projectID)).toEqual([{ directory, strategy: "new/strategy" }]) + }), + ) +}) diff --git a/packages/core/test/project.test.ts b/packages/core/test/project.test.ts index 6dd194a1761..4938656d544 100644 --- a/packages/core/test/project.test.ts +++ b/packages/core/test/project.test.ts @@ -4,24 +4,27 @@ import fs from "fs/promises" import path from "path" import { Effect, Layer, Schema } from "effect" import { ProjectV2 } from "@opencode-ai/core/project" -import { ProjectDirectoryTable, ProjectTable } from "@opencode-ai/core/project/sql" import { Database } from "@opencode-ai/core/database/database" import { FSUtil } from "@opencode-ai/core/fs-util" import { Git } from "@opencode-ai/core/git" import { AbsolutePath } from "@opencode-ai/core/schema" import { Hash } from "@opencode-ai/core/util/hash" +import { ProjectDirectories } from "@opencode-ai/core/project/directories" import { tmpdir } from "./fixture/tmpdir" import { testEffect } from "./lib/effect" const databaseLayer = Database.layerFromPath(":memory:") +const directoriesLayer = ProjectDirectories.layer.pipe(Layer.provide(databaseLayer)) const it = testEffect( Layer.mergeAll( ProjectV2.layer.pipe( - Layer.provide(databaseLayer), Layer.provide(FSUtil.defaultLayer), Layer.provide(Git.defaultLayer), + Layer.provide(directoriesLayer), + Layer.provide(databaseLayer), ), databaseLayer, + directoriesLayer, ), ) @@ -51,54 +54,6 @@ async function rootCommit(dir: string) { return (await $`git rev-list --max-parents=0 HEAD`.cwd(dir).text()).trim() } -describe("Project directories schemas", () => { - it.effect("decodes project directory input and inline directory results", () => - Effect.sync(() => { - expect(Schema.decodeUnknownSync(ProjectV2.DirectoriesInput)({ projectID: ProjectV2.ID.make("project") })).toEqual( - { - projectID: ProjectV2.ID.make("project"), - }, - ) - expect( - Schema.decodeUnknownSync(ProjectV2.Directories)([ - { directory: AbsolutePath.make("/tmp/project"), type: "main" }, - ]), - ).toEqual([{ directory: AbsolutePath.make("/tmp/project"), type: "main" }]) - }), - ) - - it.effect("lists stored project directories newest first for the requested project", () => - Effect.gen(function* () { - const project = yield* ProjectV2.Service - const { db } = yield* Database.Service - const projectID = ProjectV2.ID.make("directories-project") - const otherID = ProjectV2.ID.make("directories-other") - yield* db - .insert(ProjectTable) - .values([ - { id: projectID, worktree: AbsolutePath.make("/repo"), sandboxes: [], time_created: 1, time_updated: 1 }, - { id: otherID, worktree: AbsolutePath.make("/other"), sandboxes: [], time_created: 1, time_updated: 1 }, - ]) - .run() - .pipe(Effect.orDie) - yield* db - .insert(ProjectDirectoryTable) - .values([ - { project_id: projectID, directory: AbsolutePath.make("/repo/z"), type: "root", time_created: 2 }, - { project_id: projectID, directory: AbsolutePath.make("/repo/a"), type: "main", time_created: 1 }, - { project_id: otherID, directory: AbsolutePath.make("/other"), type: "main", time_created: 3 }, - ]) - .run() - .pipe(Effect.orDie) - - expect(yield* project.directories({ projectID })).toEqual([ - { directory: AbsolutePath.make("/repo/z"), type: "root" }, - { directory: AbsolutePath.make("/repo/a"), type: "main" }, - ]) - }), - ) -}) - describe("ProjectV2.resolve", () => { it.live("returns global for non-git directory", () => Effect.gen(function* () { diff --git a/packages/effect-drizzle-sqlite/package.json b/packages/effect-drizzle-sqlite/package.json index f86239c9a70..021b5b4e906 100644 --- a/packages/effect-drizzle-sqlite/package.json +++ b/packages/effect-drizzle-sqlite/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.13", + "version": "7.4.16", "name": "@opencode-ai/effect-drizzle-sqlite", "type": "module", "license": "MIT", diff --git a/packages/effect-sqlite-node/package.json b/packages/effect-sqlite-node/package.json index feeceb9b703..66a22748230 100644 --- a/packages/effect-sqlite-node/package.json +++ b/packages/effect-sqlite-node/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.13", + "version": "7.4.16", "name": "@opencode-ai/effect-sqlite-node", "type": "module", "license": "MIT", diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 60064f526e5..d31c75a3d81 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "kilo" name = "Kilo" description = "The open source coding agent." -version = "7.4.13" +version = "7.4.16" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/Kilo-Org/kilocode" @@ -11,26 +11,26 @@ name = "Kilo" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.13/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.4.16/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/http-recorder/package.json b/packages/http-recorder/package.json index 40dd767c7af..6eadd755345 100644 --- a/packages/http-recorder/package.json +++ b/packages/http-recorder/package.json @@ -1,11 +1,10 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "7.4.13", + "version": "7.4.16", "name": "@opencode-ai/http-recorder", "description": "Record and replay Effect HTTP client traffic with deterministic cassettes", "type": "module", "license": "MIT", - "private": true, "repository": { "type": "git", "url": "git+https://github.com/Kilo-Org/kilocode.git", @@ -24,6 +23,9 @@ "engines": { "node": ">=22" }, + "publishConfig": { + "access": "public" + }, "scripts": { "test": "bun test --timeout 30000 --only-failures", "test:ci": "mkdir -p .artifacts/unit && bun test --timeout 30000 --reporter=junit --reporter-outfile=.artifacts/unit/junit.xml", diff --git a/packages/kilo-console/package.json b/packages/kilo-console/package.json index 8b1acff3743..82ed878f473 100755 --- a/packages/kilo-console/package.json +++ b/packages/kilo-console/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-console", - "version": "7.4.13", + "version": "7.4.16", "private": true, "type": "module", "scripts": { diff --git a/packages/kilo-docs/lychee.toml b/packages/kilo-docs/lychee.toml index 4b6d5113d2c..73dc8a11fbf 100644 --- a/packages/kilo-docs/lychee.toml +++ b/packages/kilo-docs/lychee.toml @@ -50,6 +50,7 @@ exclude = [ '^https?://vercel\.link/', # API base URL, returns 404 when fetched directly '^https?://api\.apertis\.ai/v1/?$', + '^https?://cloud-agent-next\.kilosessions\.ai/?$', # Redirects to authenticated Google Cloud console '^https?://console\.cloud\.google\.com', # Google AI Studio API keys page redirects to Google sign-in diff --git a/packages/kilo-docs/markdoc/partials/cli-commands-table.md b/packages/kilo-docs/markdoc/partials/cli-commands-table.md index 65bfbedcf2c..e8e42e3921c 100644 --- a/packages/kilo-docs/markdoc/partials/cli-commands-table.md +++ b/packages/kilo-docs/markdoc/partials/cli-commands-table.md @@ -26,6 +26,7 @@ | `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 cloud` | run Cloud Agent tasks | | `kilo db` | database tools | | `kilo config` | configuration tools | | `kilo plugin ` | install plugin and update config | diff --git a/packages/kilo-docs/package.json b/packages/kilo-docs/package.json index 22005b34360..cff09854e6e 100644 --- a/packages/kilo-docs/package.json +++ b/packages/kilo-docs/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-docs", - "version": "7.4.13", + "version": "7.4.16", "private": true, "scripts": { "dev": "next dev --webpack --port 3002", diff --git a/packages/kilo-docs/pages/ai-providers/fireworks.md b/packages/kilo-docs/pages/ai-providers/fireworks.md index fcc1f27df02..8c463a02b7a 100644 --- a/packages/kilo-docs/pages/ai-providers/fireworks.md +++ b/packages/kilo-docs/pages/ai-providers/fireworks.md @@ -62,5 +62,5 @@ Then set your default model: ## Tips and Notes - **Performance:** Fireworks AI is optimized for speed and offers excellent performance for both chat and completion tasks. -- **Pricing:** Refer to the [Fireworks AI Pricing](https://fireworks.ai/pricing) page for current pricing information. +- **Pricing:** Refer to the [Fireworks AI Pricing](https://docs.fireworks.ai/serverless/pricing) page for current pricing information. - **Rate Limits:** Fireworks AI has usage-based rate limits. Monitor your usage in the dashboard and consider upgrading your plan if needed. diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md index 8a2d4178c27..144f545ca74 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/cli-reference.md @@ -968,6 +968,76 @@ Options: --json print daemon details as JSON [boolean] ``` +## kilo cloud + +``` +run Cloud Agent tasks + +Commands: + kilo cloud start start a Cloud Agent task + kilo cloud send send a follow-up prompt to a Cloud Agent task + kilo cloud status show Cloud Agent task status + kilo cloud result show a Cloud Agent task result + +Options: + --help Show help [boolean] + --version Show version number [boolean] +``` + +### kilo cloud start + +``` +start a Cloud Agent task + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --prompt prompt for the Cloud Agent [string] [required] + --repo repository shorthand or URL [string] + --repo-type repository provider type [string] [choices: "github", "gitlab", "git"] + --branch repository branch [string] + --model Cloud Agent model [string] + --mode Cloud Agent mode [string] + --org-id Kilo organization ID [string] + --stream connect to the WebSocket stream and print events as JSONL [boolean] +``` + +### kilo cloud send + +``` +send a follow-up prompt to a Cloud Agent task + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --session-id Cloud Agent session ID [string] [required] + --prompt follow-up prompt for the Cloud Agent [string] [required] +``` + +### kilo cloud status + +``` +show Cloud Agent task status + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --session-id Cloud Agent session ID [string] [required] + --message-id Cloud Agent message ID [string] [required] +``` + +### kilo cloud result + +``` +show a Cloud Agent task result + +Options: + --help Show help [boolean] + --version Show version number [boolean] + --session-id Cloud Agent session ID [string] [required] + --message-id Cloud Agent message ID [string] [required] +``` + ## kilo db ``` diff --git a/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md b/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md index c49a56e0031..096b2d1b3d4 100644 --- a/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md +++ b/packages/kilo-docs/pages/getting-started/settings/auto-approving-actions.md @@ -11,6 +11,10 @@ description: "Configure automatic approval settings for Kilo Code operations" Auto-approve settings speed up your workflow by eliminating repetitive confirmation prompts, but they significantly increase security risks. The VS Code extension and CLI share the same permission model; choose the tab that matches how you configure Kilo Code. +{% callout type="note" %} +**Editing project config while a session is running:** Kilo caches project-level `kilo.jsonc` / `kilo.json` (in `.kilo/`) when it first loads a workspace, and does not re-read it on every prompt. If you add, change, or remove a project permission rule while the backend is already running, reload the VS Code window (or start a fresh CLI session) for the change to take effect. Until then, Kilo keeps using the previously loaded rules — so an auto-approved call may still cite a project rule you just edited. Global config (`~/.config/kilo/`) is reloaded automatically. +{% /callout %} + {% tabs %} {% tab label="VSCode" %} diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-200-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-200-chromium-linux.png deleted file mode 100644 index f7283e81eb3..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-200-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a89d1a9a31f7de1780d772b7caf71d96f0b4b895df954c77dcd508a7febe7c9 -size 1950 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-chromium-linux.png deleted file mode 100644 index ca5db9f6486..00000000000 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/task-header-with-memory-chromium-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:431a0c12cb844b5d4e535cc4c7bf5546fd9e66a178b8023b00978d07c291a531 -size 3919 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/worktree-sources-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/worktree-sources-chromium-linux.png new file mode 100644 index 00000000000..531d84967ff --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/history-sessionlist/worktree-sources-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9ccf10c46b0e7bb882eee890c27396821b16b4ffef995da648b9bc47768360cb +size 22759 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/session-tabs/switcher-open-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/session-tabs/switcher-open-chromium-linux.png new file mode 100644 index 00000000000..4bb775859c9 --- /dev/null +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/session-tabs/switcher-open-chromium-linux.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:273c76976df0e83fa404ae2af2d561f8c38458e3ef1b32f4f5b1990a5906a195 +size 14239 diff --git a/packages/kilo-docs/source-links.md b/packages/kilo-docs/source-links.md index 74e3dcec068..e9b3199d5e8 100644 --- a/packages/kilo-docs/source-links.md +++ b/packages/kilo-docs/source-links.md @@ -50,6 +50,8 @@ - +- + - - @@ -106,6 +108,7 @@ - + - diff --git a/packages/kilo-gateway/package.json b/packages/kilo-gateway/package.json index 9dabb917b0f..37461419a97 100644 --- a/packages/kilo-gateway/package.json +++ b/packages/kilo-gateway/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-gateway", - "version": "7.4.13", + "version": "7.4.16", "type": "module", "license": "MIT", "description": "Unified Kilo Gateway package for OpenCode - authentication, provider, and API integration", diff --git a/packages/kilo-gateway/src/api/kilo-pass.ts b/packages/kilo-gateway/src/api/kilo-pass.ts index fd75f512cd1..014cb594d75 100644 --- a/packages/kilo-gateway/src/api/kilo-pass.ts +++ b/packages/kilo-gateway/src/api/kilo-pass.ts @@ -32,13 +32,9 @@ export async function fetchKiloPassState(token: string): Promise = {} for (const model of raw.data) { - // Skip models that don't support tools — Kilo requires tool calling - if (!model.supported_parameters?.includes("tools")) { + // Skip models that explicitly don't support tools — Kilo requires tool calling + // Optimistically assume models with a missing supported_parameters array support tools + if (model.supported_parameters && !model.supported_parameters.includes("tools")) { continue } @@ -222,7 +223,7 @@ function transformToModelDevFormat(model: OpenRouterModel): any { // Determine capabilities const supportsImages = inputModalities.includes("image") - const supportsTools = supportedParameters.includes("tools") + const supportsTools = !model.supported_parameters || supportedParameters.includes("tools") const supportsReasoning = supportedParameters.includes("reasoning") const supportsTemperature = supportedParameters.includes("temperature") diff --git a/packages/kilo-gateway/test/api/kilo-pass.test.ts b/packages/kilo-gateway/test/api/kilo-pass.test.ts index a179eeecbb2..49cf45c9601 100644 --- a/packages/kilo-gateway/test/api/kilo-pass.test.ts +++ b/packages/kilo-gateway/test/api/kilo-pass.test.ts @@ -1,5 +1,5 @@ -import { describe, expect, test } from "bun:test" -import { parseKiloPassState } from "../../src/api/kilo-pass" +import { describe, expect, mock, spyOn, test } from "bun:test" +import { fetchKiloPassState, parseKiloPassState } from "../../src/api/kilo-pass" describe("parseKiloPassState", () => { test("parses batched tRPC subscription data", () => { @@ -60,4 +60,32 @@ describe("parseKiloPassState", () => { test("returns null without period amounts", () => { expect(parseKiloPassState({ status: "none" })).toBeNull() }) + + test("silently ignores transport failures", async () => { + const prev = global.fetch + const warn = spyOn(console, "warn").mockImplementation(() => undefined) + global.fetch = mock(() => Promise.reject(new DOMException("The operation timed out.", "TimeoutError"))) + + try { + await expect(fetchKiloPassState("token")).resolves.toBeNull() + expect(warn).not.toHaveBeenCalled() + } finally { + warn.mockRestore() + global.fetch = prev + } + }) + + test("silently ignores unsuccessful responses", async () => { + const prev = global.fetch + const warn = spyOn(console, "warn").mockImplementation(() => undefined) + global.fetch = mock(() => Promise.resolve(new Response(null, { status: 503 }))) + + try { + await expect(fetchKiloPassState("token")).resolves.toBeNull() + expect(warn).not.toHaveBeenCalled() + } finally { + warn.mockRestore() + global.fetch = prev + } + }) }) diff --git a/packages/kilo-i18n/package.json b/packages/kilo-i18n/package.json index 1e948a37f18..f690fe2f5ff 100644 --- a/packages/kilo-i18n/package.json +++ b/packages/kilo-i18n/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-i18n", - "version": "7.4.13", + "version": "7.4.16", "type": "module", "license": "MIT", "description": "Kilo-specific i18n translations and overrides", diff --git a/packages/kilo-indexing/package.json b/packages/kilo-indexing/package.json index fc5d092bb13..eac32eee09f 100644 --- a/packages/kilo-indexing/package.json +++ b/packages/kilo-indexing/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-indexing", - "version": "7.4.13", + "version": "7.4.16", "type": "module", "license": "MIT", "description": "Standalone indexing engine and host helpers for Kilo Code", diff --git a/packages/kilo-jetbrains/AGENTS.md b/packages/kilo-jetbrains/AGENTS.md index 714f92feb08..05a80bf50cb 100644 --- a/packages/kilo-jetbrains/AGENTS.md +++ b/packages/kilo-jetbrains/AGENTS.md @@ -157,6 +157,7 @@ For blocking I/O in coroutines, move the dispatcher switch inside the callee usi - CLI process spawning, download, extraction, and lifecycle belong in `backend`. - By default, the plugin does not bundle CLI binaries. At connect time the backend downloads the GitHub Release asset for the version pinned in `packages/kilo-jetbrains/package.json`; `backend` resources include `kilo.properties` with `cli.version` and `cli.pinned` for split-mode RPC and runtime use. +- Bundled release builds pass `-Pkilo.cli.bundled=true` while keeping `kilo.cli.pinned=true`. This build-only flag stages all pinned CLI release assets into `kilo-cli.zip`; runtime detects that resource and extracts only the current platform instead of downloading. Do not add a `cli.bundled` key to `kilo.properties` or repurpose `kilo.cli.pinned=false` for public bundled releases. - For release questions, use the `release-jetbrains` skill and reference `.kilo/skills/release-jetbrains/SKILL.md`; it verifies the CLI pin before creating immutable `jetbrains/v*` tags. - For OS and environment checks, prefer IntelliJ Platform classes over raw JVM APIs such as `System.getProperty(...)` or `System.getenv(...)`. - Detect architecture with `com.intellij.util.system.CpuArch.CURRENT`, not `System.getProperty("os.arch")`. diff --git a/packages/kilo-jetbrains/CHANGELOG.md b/packages/kilo-jetbrains/CHANGELOG.md index 404f18168b2..d8619ecfc1b 100644 --- a/packages/kilo-jetbrains/CHANGELOG.md +++ b/packages/kilo-jetbrains/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## 7.4.16 + +### Patch Changes + +- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`2b13e7d`](https://github.com/Kilo-Org/kilocode/commit/2b13e7da2a6a776baeb2d797cd5aaeb07a526c0b) - Improve JetBrains diff previews by hiding hunk headers and adding full-path tooltips to clickable file links. + +- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`5c526f1`](https://github.com/Kilo-Org/kilocode/commit/5c526f140b78b13608ad3855532f5215c0b29675) - Render edit tool results with a clickable file target and a highlighted, simplified diff view. + +- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`73942c3`](https://github.com/Kilo-Org/kilocode/commit/73942c3f262dda53030d748e6c08f84db2384253) - Open edit tool file links directly when multiple files share the same name. + +- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`dd31044`](https://github.com/Kilo-Org/kilocode/commit/dd3104400840e1b4641097bf892e25dfccfd592d) - Render multi-file apply_patch edits as a "Patch" with a file-count tag and one section per file, each showing a clickable filename link and its own changes badge aligned with the diff. + +- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`79e606e`](https://github.com/Kilo-Org/kilocode/commit/79e606ebcbb15d20b5fde29d614f07270b1c0b3d) - Smooth out chat scrolling in large JetBrains sessions by only refreshing hover state for the message under the pointer. + +- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`95ae0e0`](https://github.com/Kilo-Org/kilocode/commit/95ae0e0b3b066ec5ab60c36b7bcffb973a942872) - Improve chat scrolling performance in large JetBrains sessions. + +- [#12491](https://github.com/Kilo-Org/kilocode/pull/12491) [`b2a3a8d`](https://github.com/Kilo-Org/kilocode/commit/b2a3a8dc10d5f579396e1bd76e16a0eef696bede) - Size edit and shell preview popovers to their content with a wider maximum width. + +## 7.5.0 + +### Minor Changes + +- [#12437](https://github.com/Kilo-Org/kilocode/pull/12437) [`af33ede`](https://github.com/Kilo-Org/kilocode/commit/af33eded9e4ac1988d218e911b5ff0d4e1b9d8b1) - Add Rules settings for instruction files and Claude Code compatibility. Fix cloud session history import failing with an HTTP 400 error. + +- [#12416](https://github.com/Kilo-Org/kilocode/pull/12416) [`a9a9b78`](https://github.com/Kilo-Org/kilocode/commit/a9a9b78b97290e855cda3dd7118a429503802396) - Support viewing, opening, editing, deleting, and configuring JetBrains skill sources. + +### Patch Changes + +- [#12291](https://github.com/Kilo-Org/kilocode/pull/12291) [`0672375`](https://github.com/Kilo-Org/kilocode/commit/067237564a170e84bc60f42b50bcba99ba9fe0c3) - Improve the JetBrains permission dialog with clearer auto-approve rule actions, hints, and command styling. + +- [#12291](https://github.com/Kilo-Org/kilocode/pull/12291) [`e9d0af5`](https://github.com/Kilo-Org/kilocode/commit/e9d0af577359e27728d4b47442d861ac2e5c6e1e) - Honor saved JetBrains bash permission rules when running with isolated dev storage. + ## 7.4.12 ### Patch Changes @@ -70,6 +102,50 @@ ## [Unreleased] +## [7.0.10] - 2026-07-24 + +### Added + +- Render edit, write, and apply-patch tool results as expandable diff previews with clickable file links, change counts, syntax-highlighted diffs, and clearer multi-file patch sections. + +### Fixed + +- Improve session performance for large transcripts. +- Fix Kilo Core failures caused by strict OpenAI-compatible compaction requests, unexpected provider finish reasons, read-only database files at startup, AWS profile credentials, and config files being rewritten just by reading them. + +### Changed + +- Update the JetBrains CLI pin from Kilo Core 7.4.13 to 7.4.15. + +## [7.0.9] - 2026-07-21 + +### Added + +- Add a Rules settings page under Agent Behavior for managing instruction files and Claude Code compatibility. + +### Fixed + +- Restore importing cloud-only session history by updating the JetBrains CLI pin to Kilo Core 7.4.13. + +### Changed + +- Improve xAI prompt cache usage in Kilo Core for better cache hit rates. + +## [7.0.8] - 2026-07-21 + +### Added + +- Add settings for context controls, including context mentions and ignore patterns. +- Add settings for skills, including editing local skills and viewing remote skills as read-only. +- Add auto-approve settings for permission rules, with filters and wildcard labels. +- Use Kilo Core for JetBrains file mention search so @-mentions match CLI indexing behavior. + +### Fixed + +### Changed + +- Update the JetBrains CLI pin from Kilo Core 7.4.5 to 7.4.11. + ## [7.0.7] - 2026-07-15 ### Added diff --git a/packages/kilo-jetbrains/README.md b/packages/kilo-jetbrains/README.md index e5452e7ddd0..754202c3c09 100644 --- a/packages/kilo-jetbrains/README.md +++ b/packages/kilo-jetbrains/README.md @@ -78,7 +78,7 @@ The built plugin archive is at `build/distributions/kilo.jetbrains-.zip ## Releasing -See [RELEASING.md](RELEASING.md) for the full release process, including how to tag and push an RC, where to watch workflow progress, and how to install RC builds via the custom plugin repository. +See [RELEASING.md](RELEASING.md) for the full release process, including how to tag and push an RC, where to watch workflow progress, how to install RC builds, and how the signed bundled CLI build is published to the GitHub-hosted stable plugin repository. --- diff --git a/packages/kilo-jetbrains/RELEASE_TODO.md b/packages/kilo-jetbrains/RELEASE_TODO.md index b74103894b7..1500e75128b 100644 --- a/packages/kilo-jetbrains/RELEASE_TODO.md +++ b/packages/kilo-jetbrains/RELEASE_TODO.md @@ -14,6 +14,8 @@ - Create a JetBrains Marketplace permanent token from Marketplace `My Tokens`. - Add `JETBRAINS_MARKETPLACE_TOKEN` to GitHub Actions secrets or the protected environment. - Confirm `GITHUB_TOKEN` has `contents: write` permission for creating and updating GitHub Releases for `jetbrains/v*` tags. +- Confirm `GITHUB_TOKEN` has `actions: write`, `pages: write`, and `id-token: write` permission for dispatching bundled releases and publishing the stable GitHub Pages plugin repository. +- Configure GitHub Pages for this repository with source set to GitHub Actions. - Confirm `KILO_MAINTAINER_APP_ID` and `KILO_MAINTAINER_APP_SECRET` are available to create release PRs and immediate release tags. - Optionally create a protected `jetbrains-marketplace` GitHub Environment with required reviewers. - If using an environment, move the Marketplace and signing secrets there and set the workflow job environment. @@ -35,6 +37,7 @@ - Review and edit `packages/kilo-jetbrains/CHANGELOG.md` in the generated release PR. - Merge the release PR to trigger publish from `jetbrains/vx.y.z-rc.n`, for example `jetbrains/v7.0.1-rc.1`. - Watch the `publish-jetbrains` workflow. +- Confirm the follow-up `publish-jetbrains-bundled` workflow completes and attaches `kilo-code-x.y.z-rc.n-bundled.zip` to the prerelease. - Download and retain the workflow artifact if needed. - Confirm the update appears on the JetBrains Marketplace `eap` channel. - Confirm the GitHub Release for the `jetbrains/vx.y.z-rc.n` tag exists and contains the JetBrains plugin ZIP asset. @@ -48,5 +51,6 @@ - Review and edit `packages/kilo-jetbrains/CHANGELOG.md` in the generated release PR. - Merge the release PR to trigger publish from `jetbrains/vx.y.z`. - Watch the `publish-jetbrains` workflow. +- Confirm the follow-up `publish-jetbrains-bundled` workflow completes, attaches `kilo-code-x.y.z-bundled.zip`, and updates `https://kilo-org.github.io/kilocode/jetbrains/updatePlugins.xml`. - Confirm the update appears on the default JetBrains Marketplace channel. - Confirm the GitHub Release for the `jetbrains/vx.y.z` tag exists and contains the JetBrains plugin ZIP asset. diff --git a/packages/kilo-jetbrains/RELEASING.md b/packages/kilo-jetbrains/RELEASING.md index 606065a27d4..8f763037019 100644 --- a/packages/kilo-jetbrains/RELEASING.md +++ b/packages/kilo-jetbrains/RELEASING.md @@ -132,6 +132,16 @@ Publishing behavior: The workflow checks out `jetbrains/v` for verification, signing, and Marketplace publishing. It overlays the reviewed `packages/kilo-jetbrains/gradle.properties` and `packages/kilo-jetbrains/CHANGELOG.md` from the merged PR before rendering release notes and before `publishPlugin`, so the Marketplace plugin version, Marketplace notes, and GitHub Release use the reviewed metadata. +After Marketplace publishing succeeds, `publish-jetbrains` dispatches `publish-jetbrains-bundled`. The bundled workflow rebuilds the same `jetbrains/v` tag with `-Pkilo.cli.bundled=true`, signs and verifies the all-platform plugin ZIP, then uploads `kilo-code--bundled.zip` to the same GitHub Release. Bundled builds keep `kilo.cli.pinned=true`; the build flag only embeds the pinned CLI release assets so runtime extracts the bundled current-platform CLI instead of downloading it. + +Stable bundled releases also publish the GitHub Pages custom plugin repository XML: + +```text +https://kilo-org.github.io/kilocode/jetbrains/updatePlugins.xml +``` + +RC bundled ZIPs are attached to prereleases for install-from-disk testing, but they do not update the stable custom repository XML. + ## Installing RC Builds RC builds are published to the `eap` channel. To get them in IntelliJ IDEA: diff --git a/packages/kilo-jetbrains/backend/build.gradle.kts b/packages/kilo-jetbrains/backend/build.gradle.kts index 374aa08a85b..77d014030d1 100644 --- a/packages/kilo-jetbrains/backend/build.gradle.kts +++ b/packages/kilo-jetbrains/backend/build.gradle.kts @@ -22,6 +22,7 @@ val generatedProps = layout.buildDirectory.dir("generated/kilo-props") val generatedCli = layout.buildDirectory.dir("generated/kilo-cli-res") val pinned = providers.gradleProperty("kilo.cli.pinned").map { it.trim().toBoolean() }.orElse(true) val repoCli = pinned.map { !it } +val bundled = providers.gradleProperty("kilo.cli.bundled").map { it.trim().toBoolean() }.orElse(false) val repoRootDir = rootProject.layout.projectDirectory.dir("../opencode") val pinnedCliVersion = providers.fileContents(rootProject.layout.projectDirectory.file("package.json")).asText.map { text -> @@ -32,11 +33,15 @@ val pinnedCliVersion = providers.fileContents(rootProject.layout.projectDirector sourceSets { main { resources.srcDir(generatedProps) - if (repoCli.get()) resources.srcDir(generatedCli) + if (repoCli.get() || bundled.get()) resources.srcDir(generatedCli) kotlin.srcDir(generatedApi) } } +if (repoCli.get() && bundled.get()) { + error("kilo.cli.bundled=true requires kilo.cli.pinned=true; do not combine release CLI bundling with local repo CLI mode.") +} + val writeKiloProperties by tasks.registering(WriteProperties::class) { description = "Write pinned Kilo CLI properties" val out = generatedProps.map { it.file("kilo.properties") } @@ -88,6 +93,17 @@ val stageRepoCli by tasks.registering(StageRepoCliTask::class) { outputs.upToDateWhen { false } } +val stageBundledCli by tasks.registering(StageBundledCliTask::class) { + description = "Stage all pinned Kilo CLI release assets into backend resources" + cliVersion.set(pinnedCliVersion) + token.set( + providers.environmentVariable("GH_TOKEN") + .orElse(providers.environmentVariable("GITHUB_TOKEN")) + ) + cacheDir.set(layout.buildDirectory.dir("cli-cache")) + archive.set(generatedCli.map { it.file("kilo-cli.zip") }) +} + val normalizeOpenApiSpec by tasks.registering(NormalizeOpenApiSpecTask::class) { description = "Normalize upstream CLI OpenAPI metadata before Kotlin client generation" dependsOn(generateOpenApiSpec) @@ -143,12 +159,14 @@ val fixGeneratedApi by tasks.registering(FixGeneratedApiTask::class) { tasks.named("compileKotlin") { dependsOn(fixGeneratedApi, writeKiloProperties) if (repoCli.get()) dependsOn(stageRepoCli) + if (bundled.get()) dependsOn(stageBundledCli) inputs.dir(generatedApi) } tasks.named("processResources") { dependsOn(writeKiloProperties) if (repoCli.get()) dependsOn(stageRepoCli) + if (bundled.get()) dependsOn(stageBundledCli) } tasks.named("compileTestKotlin") { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 5bbfae2186d..0318e49d11a 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -820,11 +820,13 @@ class KiloBackendAppService private constructor( } } "global.disposed" -> { + logSessionDisposalRisk("global.disposed") log.info("SSE global.disposed — triggering full application reload") val current = _appState.value if (current is KiloAppState.Ready) load() } "server.instance.disposed" -> { + logSessionDisposalRisk("server.instance.disposed") log.info("SSE server.instance.disposed — triggering full application reload") val current = _appState.value if (current is KiloAppState.Ready) load() @@ -835,6 +837,12 @@ class KiloBackendAppService private constructor( } } + private fun logSessionDisposalRisk(event: String) { + val active = sessions.statuses.value.filterValues { it.type != "idle" } + if (active.isEmpty()) return + log.warn("SSE $event while sessions are active; sessions may be cancelled count=${active.size} statuses=${active.values.map { it.type }.distinct()}") + } + private suspend fun clear() { synchronized(loadLock) { val jobs = listOfNotNull(loader, eventWatcher) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt index bce451fc1e1..e79249b0634 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt @@ -243,7 +243,11 @@ class KiloConnectionService( // doesn't fire against a stale timestamp from the old connection. lastEvent.set(System.currentTimeMillis()) val src = factory.newEventSource(request, listener) - source.set(src) + source.compareAndSet(null, src) + if (source.get() !== src) { + src.cancel() + return + } log.info("SSE: connecting to port $port") timeoutJob?.cancel() timeoutJob = cs.launch { @@ -258,6 +262,7 @@ class KiloConnectionService( private val listener = object : EventSourceListener() { override fun onOpen(src: EventSource, response: Response) { + source.compareAndSet(null, src) if (source.get() !== src) return if (response.request.url.port != port) return timeoutJob?.cancel() diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt index f5b9fc4f71a..f48dbf90387 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt @@ -86,6 +86,10 @@ class KiloBackendSessionManager( } fun stop() { + val active = _statuses.value.filterValues { it.type != "idle" } + if (active.isNotEmpty()) { + log.warn("Session manager stopping with active sessions count=${active.size} statuses=${active.values.map { it.type }.distinct()}") + } watcher?.cancel() watcher = null client = null diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt index fb545a75091..9425f0779f7 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt @@ -126,8 +126,8 @@ class KiloBackendCliManager( private suspend fun resolveCli(onProgress: (CliDownload) -> Unit): File { val force = forceExtract forceExtract = false - if (!KiloProps.pinned()) { - if (force) log.info("Force re-extracting local repo CLI ${KiloProps.cliVersion()}") + if (KiloRepoCli.available()) { + if (force) log.info("Force re-extracting bundled CLI ${KiloProps.cliVersion()}") val cli = KiloRepoCli.extract(force) onProgress(CliDownload(100, KiloProps.cliVersion(), KiloCliPlatform.current())) return cli @@ -581,7 +581,7 @@ internal suspend fun awaitReady( } } -private const val DEFAULT_CONFIG = """{"permission":{"edit":"ask","bash":"ask"}}""" +private const val DEFAULT_CONFIG = """{"permission":{"edit":"ask"}}""" // Must be called from a background thread — devStorageEnv() performs blocking I/O (mkdirs). internal fun buildKiloCliEnv( diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index a3f265ad3d9..a87fd32c6aa 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -57,6 +57,7 @@ import ai.kilocode.rpc.dto.ProviderMetadataDto import ai.kilocode.rpc.dto.ProviderSettingsProviderDto import ai.kilocode.rpc.dto.PartTimeDto import ai.kilocode.rpc.dto.PermissionRuleDto +import ai.kilocode.rpc.dto.PermissionRuleDecisionDto import ai.kilocode.rpc.dto.PromptDto import ai.kilocode.rpc.dto.PromptPartDto import ai.kilocode.rpc.dto.QuestionInfoDto @@ -531,6 +532,7 @@ object KiloCliDataParser { skills = parseSkillsConfig(obj["skills"].obj()), mcp = parseMcpConfig(obj["mcp"].obj()), agent = parseAgentConfig(obj["agent"].obj()), + permission = parsePermissionConfig(obj["permission"].obj()), ) }.getOrDefault(ConfigDto()) @@ -647,7 +649,12 @@ object KiloCliDataParser { val obj = item.obj() ?: return@mapNotNull null val name = obj.str("name") ?: return@mapNotNull null val location = obj.str("location") ?: return@mapNotNull null - SkillDto(name = name, description = obj.str("description"), location = location) + SkillDto( + name = name, + description = obj.str("description"), + location = location, + content = obj.str("content"), + ) } fun parseAgentBehaviorCommands(raw: String): List = @@ -914,6 +921,9 @@ object KiloCliDataParser { }) } + val permission = patch.permission + if (permission != null) put("permission", buildPermission(permission)) + if (patch.agents.isNotEmpty()) { put("agent", buildJsonObject { for ((name, agent) in patch.agents) put(name, buildJsonObject { @@ -1205,6 +1215,8 @@ object KiloCliDataParser { }?.toMap() ?: emptyMap() val path = metaObj.path() val diffs = metaObj.permissionDiffs(path) + val rawRules = metaObj.ruleDecisions() + val rules = rawRules.map { it.pattern } return PermissionRequestDto( id = id, sessionID = sid, @@ -1215,7 +1227,8 @@ object KiloCliDataParser { tool = toolRef(obj), message = obj.str("message") ?: metaObj?.str("message"), command = metaObj?.str("command") ?: obj.str("command"), - rules = metaObj.rules(), + rules = rules, + ruleDecisions = rawRules.ifEmpty { always.map { PermissionRuleDecisionDto(it) } }, filePath = path, fileDiffs = diffs, ) @@ -1633,20 +1646,43 @@ private fun JsonObject?.path(): String? { return str("filepath") ?: str("filePath") ?: str("file") ?: str("path") } -private fun JsonObject?.rules(): List { +private fun JsonObject?.ruleDecisions(): List { if (this == null) return emptyList() val raw = this["rules"] ?: return emptyList() val arr = raw.arr() if (arr != null) { - return arr.mapNotNull { it.jsonPrimitive.contentOrNull } + return arr.mapNotNull { elem -> + val obj = elem.obj() + if (obj != null) { + val pattern = obj.str("pattern") ?: obj.str("rule") ?: obj.str("text") ?: return@mapNotNull null + val next = obj.decision() + return@mapNotNull PermissionRuleDecisionDto(pattern, next, obj.defaultDecision() ?: next) + } + val pattern = runCatching { elem.jsonPrimitive.contentOrNull }.getOrNull() ?: return@mapNotNull null + PermissionRuleDecisionDto(pattern) + } } val text = runCatching { raw.jsonPrimitive.contentOrNull }.getOrNull() ?: return emptyList() - if (text.startsWith("[")) { - return runCatching { - KiloCliDataParser.parseRulesJson(text) - }.getOrElse { listOf(text) } + if (text.startsWith("[")) return KiloCliDataParser.parseRulesJson(text).map { PermissionRuleDecisionDto(it) } + return listOf(PermissionRuleDecisionDto(text)) +} + +private fun JsonObject.decision(): String { + val value = str("decision") ?: str("state") ?: str("action") ?: return "pending" + return permissionDecision(value) +} + +private fun JsonObject.defaultDecision(): String? { + val value = str("defaultDecision") ?: str("default") ?: str("defaultAction") ?: str("fallback") ?: return null + return permissionDecision(value) +} + +private fun permissionDecision(value: String): String { + return when (value.lowercase()) { + "approved", "allow" -> "approved" + "denied", "deny" -> "denied" + else -> "pending" } - return listOf(text) } private fun JsonObject?.permissionDiffs(path: String?): List { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloRepoCli.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloRepoCli.kt index a46ea79506f..8138ae4daed 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloRepoCli.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloRepoCli.kt @@ -1,5 +1,6 @@ package ai.kilocode.backend.cli +import ai.kilocode.log.KiloLog import com.intellij.openapi.application.PathManager import com.intellij.openapi.util.SystemInfo import kotlinx.coroutines.Dispatchers @@ -10,20 +11,33 @@ import java.io.OutputStream import java.util.zip.ZipInputStream object KiloRepoCli { + private const val ARCHIVE = "kilo-cli.zip" + private val log = KiloLog.create(KiloRepoCli::class.java) + + fun available(): Boolean = KiloRepoCli::class.java.classLoader.getResource(ARCHIVE) != null + suspend fun extract(force: Boolean): File = extract( force = force, - root = File(PathManager.getSystemPath(), "kilo/repo-cli"), + root = File(PathManager.getSystemPath(), "kilo/repo-cli/${KiloProps.cliVersion()}"), + cleanup = true, source = { - KiloRepoCli::class.java.classLoader.getResourceAsStream("kilo-cli.zip") - ?: throw IllegalStateException("kilo-cli.zip resource not found; rebuild with kilo.cli.pinned=false") + KiloRepoCli::class.java.classLoader.getResourceAsStream(ARCHIVE) + ?: throw IllegalStateException("kilo-cli.zip resource not found; rebuild with bundled CLI resources") }, ) - internal suspend fun extract(force: Boolean, root: File, source: () -> InputStream): File = withContext(Dispatchers.IO) { - val exe = File(root, "bin/${KiloCliPlatform.exe()}") + internal suspend fun extract( + force: Boolean, + root: File, + cleanup: Boolean = false, + source: () -> InputStream, + ): File = withContext(Dispatchers.IO) { + val platform = KiloCliPlatform.current() + val exe = File(root, "$platform/bin/${KiloCliPlatform.exe()}") val done = File(root, ".complete") if (!force && done.isFile && exe.isFile) { if (!SystemInfo.isWindows) exe.setExecutable(true) + if (cleanup) prune(root) return@withContext exe } @@ -38,21 +52,55 @@ object KiloRepoCli { ZipInputStream(input.buffered()).use { zip -> while (true) { val entry = zip.nextEntry ?: break - write(root, entry.name, entry.isDirectory) { out -> zip.copyTo(out) } + val path = select(root, entry.name, platform) + if (path != null) write(root, path, entry.isDirectory) { out -> zip.copyTo(out) } zip.closeEntry() } } } - if (!exe.isFile) throw IllegalStateException("Local repo CLI archive did not contain bin/${KiloCliPlatform.exe()}") + if (!exe.isFile) throw IllegalStateException("Bundled CLI archive did not contain $platform/bin/${KiloCliPlatform.exe()}") if (!SystemInfo.isWindows) exe.setExecutable(true) done.writeText("ok\n") + if (cleanup) prune(root) return@withContext exe } + private fun prune(root: File) { + val parent = root.parentFile ?: return + val entries = parent.listFiles() ?: return + for (entry in entries) { + if (!entry.isDirectory || entry.name == root.name || entry.name.startsWith(".")) continue + log.info("Removing stale bundled Kilo CLI version ${entry.absolutePath}") + if (!entry.deleteRecursively()) { + log.warn("Failed to remove stale bundled Kilo CLI version ${entry.absolutePath}") + } + } + } + + private fun select(dir: File, name: String, platform: String): String? { + check(dir, name) + val path = name.replace('\\', '/') + val prefix = "$platform/" + if (path.startsWith(prefix)) return path + if (path.startsWith("bin/")) return "$platform/$path" + return null + } + + private fun check(dir: File, name: String) { + val raw = name.replace('\\', '/') + if (raw.startsWith("/")) throw IllegalStateException("Archive entry escapes target directory: $name") + val parts = raw.split('/').filter { it.isNotEmpty() } + if (parts.any { it == ".." }) throw IllegalStateException("Archive entry escapes target directory: $name") + val target = File(dir, name).canonicalFile + val base = dir.canonicalFile + if (target != base && !target.path.startsWith(base.path + File.separator)) { + throw IllegalStateException("Archive entry escapes target directory: $name") + } + } + private fun write(dir: File, name: String, directory: Boolean, copy: (OutputStream) -> Unit) { - val path = if (name.startsWith("bin/")) name else "bin/$name" - val target = File(dir, path).canonicalFile + val target = File(dir, name).canonicalFile val base = dir.canonicalFile if (target != base && !target.path.startsWith(base.path + File.separator)) { throw IllegalStateException("Archive entry escapes target directory: $name") diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt index d728a6b6aa3..95185f2f204 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImpl.kt @@ -14,6 +14,7 @@ import ai.kilocode.rpc.dto.ConfigPatchDto import ai.kilocode.rpc.dto.McpConfigDto import ai.kilocode.rpc.dto.McpServerConfigDto import ai.kilocode.rpc.dto.PermissionRuleItemDto +import ai.kilocode.rpc.dto.SkillDto import com.intellij.openapi.components.service import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -22,7 +23,11 @@ import kotlinx.serialization.json.JsonPrimitive import okhttp3.MediaType.Companion.toMediaType import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import com.intellij.openapi.util.SystemInfo import java.net.URLEncoder +import java.nio.file.Files +import java.nio.file.InvalidPathException +import java.nio.file.Path import java.nio.charset.StandardCharsets import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger @@ -33,6 +38,7 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = private val JSON = "application/json".toMediaType() private val saved = ConcurrentHashMap() private val port = AtomicInteger(-1) + private val extensions = setOf("md", "markdown", "txt", "text", "html", "htm") } private val app: KiloBackendAppService get() = backend ?: service() @@ -56,11 +62,59 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = } } - override suspend fun skills(directory: String) = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null)) + override suspend fun skills(directory: String): List { + val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null)) + return items.map { item -> + val editable = editable(item) + item.copy(content = skillContent(item) ?: item.content, editable = editable) + } + } override suspend fun removeSkill(directory: String, location: String): Boolean = post(directory, "/kilocode/skill/remove", JsonObject(mapOf("location" to JsonPrimitive(location)))) + override suspend fun reloadSkills(directory: String): Boolean { + LOG.info("Skills reload requested dir=$directory") + if (hasActiveSession(directory)) { + LOG.warn("Skills reload blocked by active session dir=$directory") + return false + } + runCatching { post(directory, "/instance/reload") }.onFailure { err -> + LOG.warn("Skills reload failed dir=$directory", err) + }.getOrThrow() + LOG.info("Skills reload succeeded dir=$directory") + return true + } + + override suspend fun saveSkill(directory: String, location: String, content: String): Boolean { + LOG.info("Skill save requested dir=$directory location=$location") + app.requireReady() + val paths = knownSkills(directory) + val path = writablePath(directory, location, paths) ?: return false + withContext(Dispatchers.IO) { + Files.writeString(path, content, StandardCharsets.UTF_8) + } + LOG.info("Skill file saved dir=$directory path=$path bytes=${content.toByteArray(StandardCharsets.UTF_8).size}") + LOG.info("Skill save reload deferred dir=$directory path=$path") + return true + } + + override suspend fun saveSkills(directory: String, edits: Map): Boolean { + LOG.info("Skills save requested dir=$directory count=${edits.size}") + app.requireReady() + val known = knownSkills(directory) + val paths = edits.mapNotNull { (location, content) -> + val path = writablePath(directory, location, known) ?: return false + path to content + } + withContext(Dispatchers.IO) { + for ((path, content) in paths) Files.writeString(path, content, StandardCharsets.UTF_8) + } + LOG.info("Skill files saved dir=$directory count=${paths.size}") + LOG.info("Skills save reload deferred dir=$directory count=${paths.size}") + return true + } + override suspend fun removeAgent(directory: String, name: String): Boolean = post(directory, "/kilocode/agent/remove", JsonObject(mapOf("name" to JsonPrimitive(name)))) @@ -139,6 +193,104 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = return true } + private fun hasActiveSession(directory: String): Boolean { + val active = app.sessions.statuses.value.filterValues { it.type != "idle" } + if (active.isNotEmpty()) { + LOG.info("Skills reload active statuses dir=$directory count=${active.size} types=${active.values.map { it.type }.distinct()}") + return true + } + val permissions = runCatching { app.chat.pendingPermissions(directory) }.onFailure { err -> + LOG.warn("Skills reload pending permission check failed dir=$directory", err) + }.getOrDefault(emptyList()) + if (permissions.isNotEmpty()) { + LOG.info("Skills reload pending permissions dir=$directory count=${permissions.size}") + return true + } + val questions = runCatching { app.chat.pendingQuestions(directory) }.onFailure { err -> + LOG.warn("Skills reload pending question check failed dir=$directory", err) + }.getOrDefault(emptyList()) + if (questions.isNotEmpty()) { + LOG.info("Skills reload pending questions dir=$directory count=${questions.size}") + return true + } + return false + } + + private suspend fun skillContent(skill: SkillDto): String? { + val path = resolveSkillPath(skill.location) ?: return null + return runCatching { + withContext(Dispatchers.IO) { + if (!Files.isRegularFile(path)) null else Files.readString(path, StandardCharsets.UTF_8) + } + }.onFailure { err -> + LOG.warn("Skill content read failed: $path", err) + }.getOrNull() + } + + private fun editable(skill: SkillDto): Boolean { + val path = resolveSkillPath(skill.location) ?: return false + if (urlCached(path)) return false + return true + } + + private suspend fun knownSkills(directory: String): Set { + val items = KiloCliDataParser.parseAgentBehaviorSkills(request(directory, "/skill", null)) + return items.mapNotNull { item -> resolveEditablePath(item) }.toSet() + } + + private fun writablePath(directory: String, location: String, known: Set): Path? { + val path = resolveSkillPath(location) + if (path == null) { + LOG.warn("Skill save rejected: invalid location dir=$directory location=$location") + return null + } + if (path !in known) { + LOG.warn("Skill save rejected: unknown skill dir=$directory path=$path") + return null + } + return path + } + + private fun resolveEditablePath(skill: SkillDto): Path? { + val path = resolveSkillPath(skill.location) ?: return null + if (urlCached(path)) return null + return path + } + + private fun resolveSkillPath(location: String): Path? { + val raw = normalizeWorkspacePath(location) ?: return null + val path = try { + Path.of(raw).normalize() + } catch (_: InvalidPathException) { + return null + } + if (!path.isAbsolute || !isSkillFile(path)) return null + return path + } + + private fun urlCached(path: Path): Boolean { + return cacheRoots().any { root -> path.startsWith(root.resolve("kilo").resolve("skills").normalize()) } + } + + private fun cacheRoots(): Set = buildSet { + val home = System.getProperty("user.home") + add(Path.of(cacheRoot()).normalize()) + add(Path.of(home, ".cache").normalize()) + add(Path.of(home, "Library", "Caches").normalize()) + System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() }?.let { add(Path.of(it).normalize()) } + add(Path.of(home, "AppData", "Local").normalize()) + } + + private fun cacheRoot(): String { + val xdg = System.getenv("XDG_CACHE_HOME")?.takeIf { it.isNotBlank() } + if (xdg != null) return xdg + val home = System.getProperty("user.home") + if (SystemInfo.isMac) return Path.of(home, "Library", "Caches").toString() + if (SystemInfo.isWindows) return System.getenv("LOCALAPPDATA")?.takeIf { it.isNotBlank() } + ?: Path.of(home, "AppData", "Local").toString() + return Path.of(home, ".cache").toString() + } + private suspend fun patchConfig(path: String, body: String): Unit = withContext(Dispatchers.IO) { val http = app.http ?: throw IllegalStateException("Kilo HTTP client is unavailable") val url = "http://127.0.0.1:${app.port}$path" @@ -234,6 +386,12 @@ class KiloAgentBehaviorRpcApiImpl(private val backend: KiloBackendAppService? = private fun encodePath(value: String): String = encode(value).replace("+", "%20") + private fun isSkillFile(path: Path): Boolean { + val name = path.fileName?.toString() ?: return false + if (name == "SKILL.md") return true + return name.substringAfterLast('.', "").lowercase() in extensions + } + private data class SavedMcp( val directory: String, val name: String, diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt index 8c61e1e4dbe..981ea1ad28b 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceDtoMapper.kt @@ -64,6 +64,8 @@ internal object KiloWorkspaceDtoMapper { name = s.name, description = s.description, location = s.location, + content = s.content, + editable = false, ) private fun provider(p: ProviderInfo) = ProviderDto( diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt index 29f644efc8a..af986d0769c 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloWorkspaceRpcApiImpl.kt @@ -268,6 +268,13 @@ class KiloWorkspaceRpcApiImpl internal constructor( target(globalConfig()) } + override suspend fun refreshConfigFiles(directory: String) { + val files = withContext(Dispatchers.IO) { + listOf(localConfig(directory), globalConfig()).map { it.toFile() } + } + LocalFileSystem.getInstance().refreshIoFiles(files, true, true, null) + } + override suspend fun openLocalConfig(directory: String): Boolean = openConfig(withContext(Dispatchers.IO) { localConfig(directory) }) @@ -341,7 +348,7 @@ class KiloWorkspaceRpcApiImpl internal constructor( } descriptor.navigate(true) if (cont.isActive) cont.resume(Unit) - }, ModalityState.any()) + }, ModalityState.nonModal()) } private fun project(path: Path): Project? { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt index b948b4822da..27e19d1f064 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloBackendWorkspace.kt @@ -254,6 +254,7 @@ class KiloBackendWorkspace( name = s.name, description = s.description, location = s.location, + content = s.content, ) }) } catch (e: CancellationException) { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt index 0308089c277..37c58d853d6 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/workspace/KiloWorkspaceState.kt @@ -143,4 +143,5 @@ data class SkillInfo( val name: String, val description: String?, val location: String, + val content: String?, ) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloConnectionServiceTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloConnectionServiceTest.kt index 853b77f25b5..254417db094 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloConnectionServiceTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloConnectionServiceTest.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.withTimeoutOrNull import okhttp3.Request import okhttp3.sse.EventSource import okhttp3.sse.EventSourceListener @@ -36,6 +37,10 @@ import kotlin.test.assertTrue class KiloConnectionServiceTest { + private companion object { + const val WAIT_MS = 15_000L + } + private val mock = MockCliServer() private val fake = FakeCliServer(mock) private val log = TestLog() @@ -101,20 +106,23 @@ class KiloConnectionServiceTest { val svc = KiloConnectionService(scope, server, {}, log) val job = scope.launch { svc.connect() } - val downloading = withTimeout(5_000) { + val downloading = withTimeout(WAIT_MS) { svc.state.first { it is ConnectionState.Downloading } } assertEquals(ConnectionState.Downloading(42, "1.2.3", "darwin-arm64"), downloading) resolved.complete(Unit) - withTimeout(5_000) { + withTimeout(WAIT_MS) { svc.state.first { it == ConnectionState.Connecting } } ready.complete(Unit) - withTimeout(5_000) { + val connected = withTimeoutOrNull(WAIT_MS) { svc.state.first { it is ConnectionState.Connected } } + if (connected == null) { + error("Timed out waiting for Connected after CLI ready; state=${svc.state.value}; logs=${log.messages.joinToString("\n")}") + } job.join() } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliManagerEnvTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliManagerEnvTest.kt index 9024ce35614..96d3b5faa03 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliManagerEnvTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliManagerEnvTest.kt @@ -64,10 +64,11 @@ class KiloBackendCliManagerEnvTest { } @Test - fun `isolation disabled - default CLI config asks for edit and bash permissions`() { + fun `isolation disabled - default CLI config asks for edit permissions without forcing bash`() { val env = manager.buildEnv("pwd123", emptyMap()) - assertEquals("""{"permission":{"edit":"ask","bash":"ask"}}""", env["KILO_CONFIG_CONTENT"]) + assertEquals("""{"permission":{"edit":"ask"}}""", env["KILO_CONFIG_CONTENT"]) + assertFalse(env["KILO_CONFIG_CONTENT"]?.contains("bash") == true) } @Test diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 59b64671401..8e71d6ce373 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -1235,6 +1235,23 @@ class KiloCliDataParserTest { assertNull(webfetch.value) } + @Test + fun `parseConfig - top-level permission map`() { + val cfg = KiloCliDataParser.parseConfig( + """{"permission":{"bash":"ask","read":{"*":"allow","*.env":"deny"},"webfetch":null}}""" + ) + val bash = cfg.permission?.get("bash") + val read = cfg.permission?.get("read") + val webfetch = cfg.permission?.get("webfetch") + + assertIs(bash) + assertEquals("ask", bash.value) + assertIs(read) + assertEquals(mapOf("*" to "allow", "*.env" to "deny"), read.map) + assertIs(webfetch) + assertNull(webfetch.value) + } + @Test fun `parseConfig - empty and missing blocks`() { val cfg = KiloCliDataParser.parseConfig("{}") @@ -2222,6 +2239,21 @@ class KiloCliDataParserTest { ) } + @Test + fun `buildConfigPatch - full top-level permission object with null deletes`() { + val patch = ConfigPatchDto( + permission = linkedMapOf( + "bash" to PermissionRuleDto.Patterns(linkedMapOf("*" to "ask", "npm test" to "allow")), + "read" to PermissionRuleDto.Level(null), + ), + ) + + assertEquals( + "{\"permission\":{\"bash\":{\"*\":\"ask\",\"npm test\":\"allow\"},\"read\":null}}", + KiloCliDataParser.buildConfigPatch(patch), + ) + } + @Test fun `buildConfigPatch - empty patch`() { assertEquals("{}", KiloCliDataParser.buildConfigPatch(ConfigPatchDto())) @@ -2390,6 +2422,86 @@ class KiloCliDataParserTest { assertEquals("git status --short", asked.request.metadata["command"]) } + @Test + fun `parsePermissionRequest - parses rule decisions`() { + val data = globalEvent(""" + "type": "permission.asked", + "properties": { + "id": "perm_rules", + "sessionID": "ses_1", + "permission": "bash", + "patterns": ["git add ."], + "always": ["git *", "git add *", "git add ."], + "metadata": { + "rules": [ + {"pattern": "git *", "decision": "approved", "defaultAction": "ask"}, + {"pattern": "git add *", "action": "deny", "defaultDecision": "allow"}, + "git add ." + ] + } + } + """) + + val result = KiloCliDataParser.parseChatEvent("permission.asked", data) + assertNotNull(result) + val asked = result as? ChatEventDto.PermissionAsked ?: error("Expected PermissionAsked") + assertEquals(listOf("git *", "git add *", "git add ."), asked.request.rules) + assertEquals(asked.request.rules, asked.request.ruleDecisions.map { it.pattern }) + assertEquals("git *", asked.request.ruleDecisions[0].pattern) + assertEquals("approved", asked.request.ruleDecisions[0].decision) + assertEquals("pending", asked.request.ruleDecisions[0].defaultDecision) + assertEquals("git add *", asked.request.ruleDecisions[1].pattern) + assertEquals("denied", asked.request.ruleDecisions[1].decision) + assertEquals("approved", asked.request.ruleDecisions[1].defaultDecision) + assertEquals("git add .", asked.request.ruleDecisions[2].pattern) + assertEquals("pending", asked.request.ruleDecisions[2].decision) + assertEquals("pending", asked.request.ruleDecisions[2].defaultDecision) + } + + @Test + fun `parsePermissionRequest - uses always when metadata rules are absent`() { + val data = globalEvent(""" + "type": "permission.asked", + "properties": { + "id": "perm_always", + "sessionID": "ses_1", + "permission": "bash", + "patterns": ["git add ."], + "always": ["git add *"], + "metadata": {} + } + """) + + val result = KiloCliDataParser.parseChatEvent("permission.asked", data) + assertNotNull(result) + val asked = result as? ChatEventDto.PermissionAsked ?: error("Expected PermissionAsked") + assertEquals(emptyList(), asked.request.rules) + assertEquals(listOf("git add *"), asked.request.ruleDecisions.map { it.pattern }) + assertEquals(listOf("pending"), asked.request.ruleDecisions.map { it.decision }) + } + + @Test + fun `parsePermissionRequest - uses always when metadata rules are empty`() { + val data = globalEvent(""" + "type": "permission.asked", + "properties": { + "id": "perm_empty_rules", + "sessionID": "ses_1", + "permission": "bash", + "patterns": ["git add ."], + "always": ["git add *"], + "metadata": {"rules": []} + } + """) + + val result = KiloCliDataParser.parseChatEvent("permission.asked", data) + assertNotNull(result) + val asked = result as? ChatEventDto.PermissionAsked ?: error("Expected PermissionAsked") + assertEquals(emptyList(), asked.request.rules) + assertEquals(listOf("git add *"), asked.request.ruleDecisions.map { it.pattern }) + assertEquals(listOf("pending"), asked.request.ruleDecisions.map { it.decision }) + } + @Test fun `parsePermissionRequest - diff and filepath fallback`() { val data = globalEvent(""" diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloRepoCliTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloRepoCliTest.kt index 4c2f1400532..b5a2adf93f8 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloRepoCliTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloRepoCliTest.kt @@ -12,6 +12,7 @@ import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertFalse import kotlin.test.assertTrue class KiloRepoCliTest { @@ -38,6 +39,38 @@ class KiloRepoCliTest { assertEquals("#!/bin/new\n", forced.readText()) } + @Test + fun `extracts only current platform from multi platform archive`() = runBlocking { + val platform = KiloCliPlatform.current() + val other = if (platform == "windows-x64") "darwin-arm64" else "windows-x64" + val cli = KiloRepoCli.extract(false, dir) { + ByteArrayInputStream(multi(platform, other)) + } + + assertTrue(cli.isFile) + assertEquals("current", cli.readText()) + assertFalse(File(dir, "$other/bin/kilo.exe").exists()) + assertFalse(File(dir, "$other/bin/kilo").exists()) + } + + @Test + fun `prunes stale bundled cli versions after resolve`() = runBlocking { + val root = File(dir, "7.4.11") + val stale = File(dir, "7.4.10") + File(stale, "old").apply { + parentFile.mkdirs() + writeText("old") + } + + val cli = KiloRepoCli.extract(false, root, cleanup = true) { + ByteArrayInputStream(archive("current")) + } + + assertTrue(cli.isFile) + assertFalse(stale.exists()) + assertTrue(root.isDirectory) + } + @Test fun `rejects archive entries that escape root`() = runBlocking { val ex = assertFailsWith { @@ -66,4 +99,17 @@ class KiloRepoCliTest { } return out.toByteArray() } + + private fun multi(platform: String, other: String): ByteArray { + val out = ByteArrayOutputStream() + ZipOutputStream(out).use { zip -> + zip.putNextEntry(ZipEntry("$platform/bin/${KiloCliPlatform.exe()}")) + zip.write("current".toByteArray()) + zip.closeEntry() + zip.putNextEntry(ZipEntry("$other/bin/kilo.exe")) + zip.write("other".toByteArray()) + zip.closeEntry() + } + return out.toByteArray() + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt index 4050343723a..880e6cba4cd 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/rpc/KiloAgentBehaviorRpcApiImplTest.kt @@ -14,6 +14,8 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withTimeout +import java.nio.file.Files +import java.nio.file.Path import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertContains @@ -78,6 +80,156 @@ class KiloAgentBehaviorRpcApiImplTest { assertContains(err.message.orEmpty(), "HTTP 400") } + @Test + fun `skills and remove skill call CLI endpoints`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val file = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md") + val content = """--- + |name: plan + |description: Plan work + |--- + | + |# Fresh Plan + """.trimMargin() + Files.writeString(file, content) + mock.skills = """[ + {"name":"plan","description":"Plan work","location":"$file","content":"# Stale Plan"}, + {"name":"builtin","location":"builtin"} + ]""".trimIndent() + val rpc = rpc() + + val skills = rpc.skills("/test project") + assertEquals(listOf("plan", "builtin"), skills.map { it.name }) + assertEquals("Plan work", skills.single { it.name == "plan" }.description) + assertEquals(content, skills.single { it.name == "plan" }.content) + assertEquals(true, skills.single { it.name == "plan" }.editable) + assertEquals(false, skills.single { it.name == "builtin" }.editable) + + assertTrue(rpc.removeSkill("/test project", file.toString())) + assertEquals("{\"location\":\"$file\"}", mock.lastSkillRemoveBody) + assertEquals(1, mock.requestCount("/kilocode/skill/remove")) + + mock.skillRemoveStatus = 400 + val err = assertFailsWith { + rpc.removeSkill("/test", "/tmp/missing/SKILL.md") + } + assertContains(err.message.orEmpty(), "HTTP 400") + + assertTrue(rpc.reloadSkills("/test project")) + assertEquals(1, mock.requestCount("/instance/reload")) + } + + @Test + fun `url cached skills are read only`() = runBlocking { + val cache = Path.of(System.getProperty("user.home"), ".cache", "kilo", "skills", "remote") + val file = Files.createDirectories(cache).resolve("SKILL.md") + Files.writeString(file, "# Remote") + mock.skills = """[ + {"name":"remote","description":"Remote","location":"$file","content":"# Remote"} + ]""".trimIndent() + + val skill = rpc().skills("/test project").single() + + assertEquals(false, skill.editable) + assertEquals("# Remote", skill.content) + } + + @Test + fun `custom skills under non cache paths remain editable`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val file = Files.createDirectories(dir.resolve("cache/kilo/skills/custom")).resolve("SKILL.md") + Files.writeString(file, "# Custom") + mock.skills = """[ + {"name":"custom","description":"Custom","location":"$file","content":"# Custom"} + ]""".trimIndent() + + val skill = rpc().skills("/test project").single() + + assertEquals(true, skill.editable) + } + + @Test + fun `save skill supports configured markdown text and html files without reload`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val file = dir.resolve("test.md") + Files.writeString(file, "old") + mock.skills = """[ + {"name":"test","description":"Test","location":"$file","content":"old"} + ]""".trimIndent() + val rpc = rpc() + + assertTrue(rpc.saveSkill("/test project", file.toString(), "new content")) + assertEquals("new content", Files.readString(file)) + assertEquals(0, mock.requestCount("/instance/reload")) + } + + @Test + fun `save skill writes content without reloading instance`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val file = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md") + Files.writeString(file, "old") + mock.skills = """[ + {"name":"plan","description":"Plan work","location":"$file","content":"old"} + ]""".trimIndent() + val rpc = rpc() + + assertTrue(rpc.saveSkill("/test project", file.toString(), "new content")) + + assertEquals("new content", Files.readString(file)) + assertEquals(0, mock.requestCount("/instance/reload")) + assertFalse(rpc.saveSkill("/test project", "builtin", "nope")) + } + + @Test + fun `save skills validates known paths once for multiple edits`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val plan = Files.createDirectories(dir.resolve("plan")).resolve("SKILL.md") + val review = Files.createDirectories(dir.resolve("review")).resolve("SKILL.md") + Files.writeString(plan, "old plan") + Files.writeString(review, "old review") + mock.skills = """[ + {"name":"plan","description":"Plan work","location":"$plan","content":"old plan"}, + {"name":"review","description":"Review work","location":"$review","content":"old review"} + ]""".trimIndent() + val rpc = rpc() + mock.resetCounts() + + assertTrue(rpc.saveSkills("/test project", mapOf(plan.toString() to "new plan", review.toString() to "new review"))) + + assertEquals("new plan", Files.readString(plan)) + assertEquals("new review", Files.readString(review)) + assertEquals(1, mock.requestCount("/skill")) + } + + @Test + fun `save skill rejects unknown absolute skill files`() = runBlocking { + val dir = Files.createTempDirectory("kilo-skill-test") + val known = Files.createDirectories(dir.resolve("known")).resolve("SKILL.md") + val other = Files.createDirectories(dir.resolve("other")).resolve("SKILL.md") + Files.writeString(known, "known") + Files.writeString(other, "old") + mock.skills = """[ + {"name":"known","description":"Known","location":"$known","content":"known"} + ]""".trimIndent() + + assertFalse(rpc().saveSkill("/test project", other.toString(), "new content")) + + assertEquals("old", Files.readString(other)) + } + + @Test + fun `reload skills is blocked by pending permissions`() = runBlocking { + mock.pendingPermissions = """[ + {"id":"per_test","sessionID":"ses_test","permission":"bash","patterns":["*"],"metadata":{}} + ]""".trimIndent() + val rpc = rpc() + + assertFalse(rpc.reloadSkills("/test project")) + + assertEquals(1, mock.requestCount("/permission")) + assertEquals(0, mock.requestCount("/instance/reload")) + } + @Test fun `mcp config writes global and workspace patches`() = runBlocking { mock.config = """{"mcp":{"global":{"type":"local","command":["node","g.js"]}}}""" diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt index 2ec50ae0ede..5e05d255733 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt @@ -68,9 +68,11 @@ class MockCliServer : AutoCloseable { @Volatile var mcpStatus = 200 @Volatile var mcpActionStatus = 200 @Volatile var agentRemoveStatus = 200 + @Volatile var skillRemoveStatus = 200 @Volatile var agentBuilderStatus = 200 @Volatile var lastMcpActionPath: String? = null @Volatile var lastAgentRemoveBody: String? = null + @Volatile var lastSkillRemoveBody: String? = null @Volatile var lastAgentBuilderPath: String? = null @Volatile var lastAgentBuilderBody: String? = null @Volatile var lastAgentBuilderMethod: String? = null @@ -135,6 +137,8 @@ class MockCliServer : AutoCloseable { @Volatile var lastSessionRenamePath: String? = null @Volatile var lastSessionRenameBody: String? = null @Volatile var lastSessionRenameMethod: String? = null + @Volatile var pendingPermissions = "[]" + @Volatile var pendingQuestions = "[]" /** Configurable delay for all endpoint responses (ms). 0 = no delay. */ @Volatile var responseDelay: Long = 0 @@ -375,6 +379,11 @@ class MockCliServer : AutoCloseable { lastAgentRemoveBody = body respond(output, agentRemoveStatus, if (agentRemoveStatus == 200) "true" else """{"error":"Agent not found"}""") } + bare == "/kilocode/skill/remove" && method == "POST" -> { + lastSkillRemoveBody = body + respond(output, skillRemoveStatus, if (skillRemoveStatus == 200) "true" else """{"error":"Skill not found"}""") + } + bare == "/instance/reload" && method == "POST" -> respond(output, 200, "true") bare == "/command" -> respond(output, commandsStatus, commands) bare == "/skill" -> respond(output, skillsStatus, skills) bare == "/find/file" -> { @@ -405,6 +414,8 @@ class MockCliServer : AutoCloseable { respond(output, cloudSessionImportStatus, cloudSessionImport) } bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses) + bare == "/permission" && method == "GET" -> respond(output, 200, pendingPermissions) + bare == "/question" && method == "GET" -> respond(output, 200, pendingQuestions) bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions) bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate) bare.matches(Regex("/session/ses_[^/]+")) && method == "GET" -> diff --git a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/StageBundledCliTask.kt b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/StageBundledCliTask.kt new file mode 100644 index 00000000000..62ae78a2317 --- /dev/null +++ b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/StageBundledCliTask.kt @@ -0,0 +1,225 @@ +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.net.HttpURLConnection +import java.net.URI +import java.security.MessageDigest +import java.time.Instant +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +abstract class StageBundledCliTask : DefaultTask() { + companion object { + private val DIGEST = Regex("^sha256:[a-f0-9]{64}$") + private val JSON = Json { ignoreUnknownKeys = true } + private const val API = "https://api.github.com/repos/Kilo-Org/kilocode/releases/tags" + private val PLATFORMS = listOf( + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "windows-arm64", + "windows-x64", + ) + } + + @get:Input + abstract val cliVersion: Property + + @get:Internal + abstract val token: Property + + @get:Internal + abstract val cacheDir: DirectoryProperty + + @get:OutputFile + abstract val archive: RegularFileProperty + + @TaskAction + fun run() { + val ver = cliVersion.get() + val assets = assets(ver) + val files = PLATFORMS.associateWith { platform -> + val ext = ext(platform) + val name = "kilo-$platform.$ext" + val digest = assets[name] ?: throw GradleException("Kilo CLI release $ver did not include $name") + val file = cacheDir.dir(ver).map { it.dir(platform).file(name) }.get().asFile + fetch(ver, platform, name, digest, file) + file + } + + val out = archive.get().asFile + out.parentFile.mkdirs() + ZipOutputStream(out.outputStream().buffered()).use { zip -> + for ((platform, file) in files) { + if (file.name.endsWith(".zip")) { + zip(platform, file, zip) + continue + } + tar(platform, file, zip) + } + } + } + + private fun assets(ver: String): Map { + val url = "$API/v$ver" + logger.lifecycle("Fetching pinned Kilo CLI release metadata from $url") + val conn = connect(url) + try { + val code = conn.responseCode + if (code !in 200..299) fail(conn, code, "Failed to fetch pinned Kilo CLI release metadata") + val body = conn.inputStream.bufferedReader().use { it.readText() } + return JSON.parseToJsonElement(body).jsonObject["assets"]?.jsonArray + ?.associate { item -> + val obj = item.jsonObject + val name = obj["name"]?.jsonPrimitive?.contentOrNull + val digest = obj["digest"]?.jsonPrimitive?.contentOrNull + if (name.isNullOrBlank() || digest.isNullOrBlank()) return@associate "" to "" + name to digest + } + ?.filter { it.key.isNotEmpty() } + ?.mapValues { item -> + val digest = item.value + if (!digest.matches(DIGEST)) { + throw GradleException("Pinned Kilo CLI release $ver asset ${item.key} has invalid digest") + } + digest + } + ?: emptyMap() + } finally { + conn.disconnect() + } + } + + private fun fetch(ver: String, platform: String, name: String, digest: String, file: File) { + if (file.isFile && sum(file) == digest) return + file.parentFile.mkdirs() + val url = "https://github.com/Kilo-Org/kilocode/releases/download/v$ver/$name" + logger.lifecycle("Downloading pinned Kilo CLI $platform from $url") + val conn = connect(url) + try { + val code = conn.responseCode + if (code !in 200..299) fail(conn, code, "Failed to download pinned Kilo CLI $platform") + conn.inputStream.use { input -> + file.outputStream().use { output -> input.copyTo(output) } + } + } finally { + conn.disconnect() + } + verify(file, digest) + } + + private fun zip(platform: String, file: File, out: ZipOutputStream) { + ZipInputStream(file.inputStream().buffered()).use { zip -> + while (true) { + val entry = zip.nextEntry ?: break + if (!entry.isDirectory) write(out, platform, entry.name) { zip.copyTo(out) } + zip.closeEntry() + } + } + } + + private fun tar(platform: String, file: File, out: ZipOutputStream) { + TarArchiveInputStream(GzipCompressorInputStream(file.inputStream().buffered())).use { tar -> + while (true) { + val entry = tar.nextEntry ?: break + if (entry.isDirectory) continue + if (entry.isSymbolicLink || !entry.isFile) { + throw GradleException("Unsupported CLI tar entry type in ${file.name}: ${entry.name}") + } + write(out, platform, entry.name) { tar.copyTo(out) } + } + } + } + + private fun write(out: ZipOutputStream, platform: String, name: String, copy: () -> Unit) { + out.putNextEntry(ZipEntry(path(platform, name))) + copy() + out.closeEntry() + } + + private fun path(platform: String, name: String): String { + val raw = name.replace('\\', '/') + if (raw.startsWith("/")) throw GradleException("Archive entry escapes target directory: $name") + val parts = raw.split('/').filter { it.isNotEmpty() && it != "." } + if (parts.isEmpty()) throw GradleException("Archive entry is empty: $name") + if (parts.any { it == ".." }) throw GradleException("Archive entry escapes target directory: $name") + val path = if (parts.first() == "bin") parts else listOf("bin") + parts + return "$platform/${path.joinToString("/")}" + } + + private fun verify(file: File, digest: String) { + val actual = sum(file) + if (actual == digest) return + if (file.exists() && !file.delete()) logger.warn("Failed to delete invalid pinned Kilo CLI archive ${file.absolutePath}") + throw GradleException("Pinned Kilo CLI archive digest mismatch for ${file.name}: expected $digest, got $actual") + } + + private fun sum(file: File) = "sha256:${sha256(file)}" + + private fun sha256(file: File): String { + val md = MessageDigest.getInstance("SHA-256") + file.inputStream().buffered().use { input -> + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + while (true) { + val n = input.read(buffer) + if (n < 0) break + md.update(buffer, 0, n) + } + } + return md.digest().joinToString("") { "%02x".format(it.toInt() and 0xff) } + } + + private fun connect(url: String): HttpURLConnection { + val conn = URI(url).toURL().openConnection() as HttpURLConnection + conn.connectTimeout = 30_000 + conn.readTimeout = 120_000 + conn.instanceFollowRedirects = true + conn.setRequestProperty("Accept", "application/vnd.github+json") + token.getOrNull() + ?.trim() + ?.takeIf { it.isNotEmpty() } + ?.let { conn.setRequestProperty("Authorization", "Bearer $it") } + return conn + } + + private fun fail(conn: HttpURLConnection, code: Int, msg: String): Nothing { + val info = rate(conn) + val body = runCatching { conn.errorStream?.bufferedReader()?.use { it.readText() } } + .getOrNull() + ?.take(500) + val detail = if (body.isNullOrBlank()) "" else ": $body" + if (limited(conn, code)) { + throw GradleException("GitHub API rate limit exceeded while staging bundled Kilo CLI ($info)$detail") + } + throw GradleException("$msg: HTTP $code from ${conn.url} ($info)$detail") + } + + private fun rate(conn: HttpURLConnection): String { + val reset = conn.getHeaderField("X-RateLimit-Reset") + ?.toLongOrNull() + ?.let { Instant.ofEpochSecond(it).toString() } + return "limit=${conn.getHeaderField("X-RateLimit-Limit")} remaining=${conn.getHeaderField("X-RateLimit-Remaining")} " + + "used=${conn.getHeaderField("X-RateLimit-Used")} reset=$reset retryAfter=${conn.getHeaderField("Retry-After")}" + } + + private fun limited(conn: HttpURLConnection, code: Int) = + code == 429 || (code == 403 && conn.getHeaderField("X-RateLimit-Remaining") == "0") + + private fun ext(platform: String) = if (platform.startsWith("linux-")) "tar.gz" else "zip" +} diff --git a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/StageRepoCliTask.kt b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/StageRepoCliTask.kt index f5a77cd5321..1f58091ee03 100644 --- a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/StageRepoCliTask.kt +++ b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/StageRepoCliTask.kt @@ -28,12 +28,13 @@ abstract class StageRepoCliTask : DefaultTask() { } val out = archive.get().asFile + val platform = platform() out.parentFile.mkdirs() ZipOutputStream(out.outputStream().buffered()).use { zip -> dir.walkTopDown() .filter { it.isFile } .forEach { file -> - val name = "bin/${file.relativeTo(dir).invariantSeparatorsPath}" + val name = "$platform/bin/${file.relativeTo(dir).invariantSeparatorsPath}" zip.putNextEntry(ZipEntry(name)) file.inputStream().use { it.copyTo(zip) } zip.closeEntry() @@ -42,4 +43,20 @@ abstract class StageRepoCliTask : DefaultTask() { } private fun exe() = if (System.getProperty("os.name").lowercase().contains("windows")) "kilo.exe" else "kilo" + + private fun platform(): String { + val os = System.getProperty("os.name").lowercase() + val name = when { + os.contains("mac") || os.contains("darwin") -> "darwin" + os.contains("linux") -> "linux" + os.contains("windows") -> "windows" + else -> throw GradleException("Unsupported OS: ${System.getProperty("os.name")}") + } + val arch = when (System.getProperty("os.arch").lowercase()) { + "aarch64", "arm64" -> "arm64" + "x86_64", "amd64" -> "x64" + else -> throw GradleException("Unsupported architecture: ${System.getProperty("os.arch")}") + } + return "$name-$arch" + } } diff --git a/packages/kilo-jetbrains/docs/bundled-release-plan.md b/packages/kilo-jetbrains/docs/bundled-release-plan.md new file mode 100644 index 00000000000..7a45f698e2e --- /dev/null +++ b/packages/kilo-jetbrains/docs/bundled-release-plan.md @@ -0,0 +1,64 @@ +# JetBrains Bundled-CLI Release Plan + +Ship a signed, all-platform, CLI-bundled build of the Kilo JetBrains plugin to a GitHub-hosted custom plugin repository, as an alternative to the JetBrains Marketplace, which caps plugin ZIPs at 400 MB. The Marketplace build stays lean and downloads the CLI at runtime; the bundled build embeds every platform's CLI so it works offline or on restricted networks. + +## Decisions + +1. Host `updatePlugins.xml` via GitHub Pages deployed by Actions. +2. Maintain a single stable custom repo: one `updatePlugins.xml`, updated on stable releases only. +3. Auto-trigger the bundled workflow after `publish-jetbrains` succeeds. +4. Decide runtime delivery by presence of the bundled `kilo-cli.zip` resource. Do not add a `kilo.properties` flag, and do not edit committed files for a bundled build. + +## Core Principle + +- A bundled build uses the same `jetbrains/v` tag, the same source, and `kilo.cli.pinned=true`. +- The only build difference is the override `-Pkilo.cli.bundled=true`. +- `kilo.properties` stays byte-identical between Marketplace and bundled builds. The only build-output difference is whether `kilo-cli.zip` is embedded in the backend jar. +- `kilo.cli.pinned` keeps its existing meaning: which CLI version / OpenAPI source / release guard. It does not control runtime delivery. + +## Phase 1: Backend Delivery + +- Add `KiloRepoCli.available()` to detect `kilo-cli.zip` on the classpath. +- Change `KiloBackendCliManager.resolveCli()` to extract when `KiloRepoCli.available()` is true; otherwise download the pinned release asset. +- Store bundled archives as `/bin/kilo[.exe]` for all six platforms: `darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `windows-arm64`, `windows-x64`. +- Extract only the current platform's subtree to disk so users do not store all six binaries locally. +- Keep path traversal checks for every archive entry. +- Update repo CLI dev staging to use the same layout. + +## Phase 2: Gradle Bundling + +- Add a build-only property `kilo.cli.bundled`, defaulting to false. +- Keep `kilo.cli.pinned=true` for bundled production builds. +- Add a task that downloads all six pinned CLI release assets from GitHub, verifies their `sha256` digests from release metadata, and assembles `kilo-cli.zip` as a backend resource. +- Wire that generated resource only when `-Pkilo.cli.bundled=true` or local repo CLI mode is active. +- Leave the production guard against `kilo.cli.pinned=false` intact. + +Bundled build command: + +```bash +./gradlew clean buildPlugin signPlugin verifyPluginSignature verifyPlugin \ + -Pproduction=true -Pkilo.version= -Pkilo.channel=default \ + -Pkilo.cli.bundled=true +``` + +## Phase 3: Bundle Workflow + +- Add `.github/workflows/publish-jetbrains-bundled.yml`. +- Add a final success step to `publish-jetbrains.yml` that dispatches the bundle workflow with the merged release PR and merge commit. +- The bundle workflow checks out the merged release PR for validation, then checks out the immutable `jetbrains/v` tag, restores reviewed release metadata, builds the bundled variant, signs it, verifies it, and uploads `kilo-code--bundled.zip` to the same GitHub Release. +- Bundle ZIPs are produced for RC and stable releases. Only stable releases update the custom plugin repository XML. + +## Phase 4: GitHub Pages Repository + +- Generate `jetbrains/updatePlugins.xml` from the signed bundled ZIP metadata on stable releases. +- Point the plugin URL at the uploaded GitHub Release asset. +- Deploy the XML with GitHub Pages Actions to `https://kilo-org.github.io/kilocode/jetbrains/updatePlugins.xml`. +- Users add that URL in JetBrains IDEs under Settings -> Plugins -> Manage Plugin Repositories. + +## Acceptance Criteria + +- Marketplace builds remain unchanged and download the CLI at runtime. +- Bundled builds use the same tag and source, keep `kilo.cli.pinned=true`, and differ only by `-Pkilo.cli.bundled=true`. +- Bundled ZIPs are signed and attached to the `jetbrains/v` release. +- Runtime extracts the bundled current-platform CLI and never downloads when `kilo-cli.zip` is present. +- Stable releases update the GitHub Pages `updatePlugins.xml` with the latest bundled signed ZIP URL. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloNotifications.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloNotifications.kt index d2bc692d61a..570c73363fe 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloNotifications.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloNotifications.kt @@ -21,4 +21,13 @@ object KiloNotifications { ?: Notification(GROUP, title, content ?: "", NotificationType.ERROR) notification.notify(project) } + + fun info(title: String, content: String? = null) { + val project = ProjectManager.getInstance().openProjects.firstOrNull { !it.isDefault } + val notification = NotificationGroupManager.getInstance() + .getNotificationGroup(GROUP) + ?.createNotification(title, content ?: "", NotificationType.INFORMATION) + ?: Notification(GROUP, title, content ?: "", NotificationType.INFORMATION) + notification.notify(project) + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt index 63f55818d33..2171cb10942 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAgentBehaviorService.kt @@ -33,7 +33,9 @@ class KiloAgentBehaviorService internal constructor( suspend fun agents(directory: String): List = safe(emptyList()) { call { agents(directory) } } - suspend fun skills(directory: String): List = safe(emptyList()) { call { skills(directory) } } + suspend fun loadSkills(directory: String): List = call { skills(directory) } + + suspend fun refreshSkills(directory: String, fallback: List): List = safe(fallback) { call { skills(directory) } } suspend fun commands(directory: String): List = safe(emptyList()) { call { commands(directory) } } @@ -52,6 +54,11 @@ class KiloAgentBehaviorService internal constructor( suspend fun removeSkill(directory: String, location: String): Boolean = safe(false) { call { removeSkill(directory, location) } } + suspend fun reloadSkills(directory: String): Boolean = safe(false) { call { reloadSkills(directory) } } + + suspend fun saveSkills(directory: String, edits: Map): Boolean = + safe(false) { call { saveSkills(directory, edits) } } + suspend fun removeAgent(directory: String, name: String): Boolean = safe(false) { call { removeAgent(directory, name) } } suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean = safe(false) { call { createAgent(directory, input) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAppService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAppService.kt index b24e42f992f..23443265e1b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAppService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloAppService.kt @@ -58,6 +58,12 @@ class KiloAppService internal constructor( val version: String? get() = info?.version + /** + * App-lifetime scope for fire-and-forget work that must outlive transient UIs such as the + * settings dialog (whose own scope is cancelled the moment it closes on OK). + */ + internal val scope: CoroutineScope get() = cs + internal val _state = MutableStateFlow(init) val state: StateFlow = _state.asStateFlow() private val _models = MutableStateFlow(ModelStateDto()) @@ -366,5 +372,6 @@ data class CoreInfo(val version: String, val platform: String) private fun summary(patch: ConfigPatchDto): String { val values = patch.values.keys.sorted().joinToString(",").ifEmpty { "none" } - return "values=$values agents=${patch.agents.size}" + val permission = if (patch.permission != null) " permission" else "" + return "values=$values agents=${patch.agents.size}$permission" } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt index 0bba46a9963..cd7dae807db 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloWorkspaceService.kt @@ -83,7 +83,7 @@ class KiloWorkspaceService internal constructor( LOG.info("Creating workspace for $directory") val state = stream { state(directory) } .stateIn(cs, SharingStarted.Eagerly, INIT) - Workspace(directory, state) { reload(directory) } + Workspace(directory, state, { reload(directory) }) { refreshConfigFiles(directory) } } // Refresh on every workspace access so config actions reflect file system changes. refreshLocalConfigTarget(directory) @@ -169,6 +169,15 @@ class KiloWorkspaceService internal constructor( } } + suspend fun openFile(path: String, line: Int? = null, column: Int? = null): Boolean { + return try { + call { openFile(path, line, column) } + } catch (e: Exception) { + LOG.warn("workspace file open failed for path=$path", e) + false + } + } + suspend fun localConfigTarget(directory: String): ConfigTargetDto? { return try { val target = call { this.localConfigTarget(directory) } @@ -217,6 +226,20 @@ class KiloWorkspaceService internal constructor( } } + fun refreshConfigFiles(directory: String): Job { + return cs.launch { + try { + call { refreshConfigFiles(directory) } + localConfigTarget(directory) + globalConfigTarget() + } catch (e: Exception) { + LOG.warn("config file refresh failed for directory=$directory", e) + } finally { + ActivityTracker.getInstance().inc() + } + } + } + fun openLocalConfig(directory: String, done: (Boolean) -> Unit) { cs.launch { val ok = try { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/Workspace.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/Workspace.kt index a490a267ec1..6ed99d98954 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/Workspace.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/Workspace.kt @@ -14,4 +14,5 @@ class Workspace( val directory: String, val state: StateFlow, val reload: () -> Unit, + val refreshConfigFiles: () -> Unit = {}, ) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/plugin/KiloPluginSettings.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/plugin/KiloPluginSettings.kt index f410fbc3c34..16641aab323 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/plugin/KiloPluginSettings.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/plugin/KiloPluginSettings.kt @@ -4,6 +4,7 @@ import com.intellij.ide.util.PropertiesComponent object KiloPluginSettings { private const val AUTO_APPROVE_KEY = "kilo.session.autoApprove" + private const val PERMISSION_RULES_EXPANDED_KEY = "kilo.session.permissionRulesExpanded" fun getAutoApprove(): Boolean = PropertiesComponent.getInstance().getBoolean(AUTO_APPROVE_KEY, false) @@ -14,4 +15,14 @@ object KiloPluginSettings { internal fun unsetAutoApprove() { PropertiesComponent.getInstance().unsetValue(AUTO_APPROVE_KEY) } + + fun getPermissionRulesExpanded(): Boolean = PropertiesComponent.getInstance().getBoolean(PERMISSION_RULES_EXPANDED_KEY, false) + + fun setPermissionRulesExpanded(value: Boolean) { + PropertiesComponent.getInstance().setValue(PERMISSION_RULES_EXPANDED_KEY, value.toString()) + } + + internal fun unsetPermissionRulesExpanded() { + PropertiesComponent.getInstance().unsetValue(PERMISSION_RULES_EXPANDED_KEY) + } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index a375feb4563..86333825399 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -343,7 +343,7 @@ class SessionUi( focus = focus, ) permission = PermissionView( - reply = { id, dto -> controller.replyPermission(id, dto) }, + reply = { id, dto, rules -> controller.replyPermission(id, dto, rules) }, selection = selection, focus = focus, ) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 21827b7f53a..0570d4bd366 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -13,6 +13,8 @@ import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.session.model.Permission import ai.kilocode.client.session.model.PermissionFileDiff import ai.kilocode.client.session.model.PermissionMeta +import ai.kilocode.client.session.model.PermissionRuleCandidate +import ai.kilocode.client.session.model.PermissionRuleDecision import ai.kilocode.client.session.model.PermissionRequestState import ai.kilocode.client.session.model.Question import ai.kilocode.client.session.model.QuestionItem @@ -41,6 +43,7 @@ import ai.kilocode.rpc.dto.ProfileStatusDto import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto import ai.kilocode.rpc.dto.PermissionReplyDto import ai.kilocode.rpc.dto.PermissionRequestDto +import ai.kilocode.rpc.dto.PermissionRuleDecisionDto import ai.kilocode.rpc.dto.PromptDto import ai.kilocode.rpc.dto.PromptPartDto import ai.kilocode.rpc.dto.ProvidersDto @@ -657,7 +660,10 @@ class SessionController( updatePermission(requestId, PermissionRequestState.RESPONDING) cs.launch { try { - if (rules != null) sessions.savePermissionRules(requestId, directory, rules) + if (rules != null) { + sessions.savePermissionRules(requestId, directory, rules) + workspace.refreshConfigFiles() + } sessions.replyPermission(requestId, directory, reply) capture("Approval Answered", sessionProps() + mapOf( "requestId" to requestId, @@ -2436,6 +2442,9 @@ private fun toPermission(dto: PermissionRequestDto): Permission { ?: dto.metadata["filePath"] ?: dto.metadata["file"] ?: dto.metadata["path"] + val patterns = dto.rules.ifEmpty { dto.always } + val rules = dto.ruleDecisions.map { it.toRuleCandidate() } + .ifEmpty { patterns.map { PermissionRuleCandidate(it) } } return Permission( id = dto.id, sessionId = dto.sessionID, @@ -2444,7 +2453,8 @@ private fun toPermission(dto: PermissionRequestDto): Permission { always = dto.always, meta = PermissionMeta( command = dto.command ?: dto.metadata["command"], - rules = dto.rules, + rules = patterns, + ruleDecisions = rules, diff = dto.metadata["diff"], filePath = file, fileDiff = diffs.firstOrNull(), @@ -2457,6 +2467,20 @@ private fun toPermission(dto: PermissionRequestDto): Permission { ) } +private fun PermissionRuleDecisionDto.toRuleCandidate(): PermissionRuleCandidate { + val next = decision.toPermissionRuleDecision() + val default = defaultDecision.toPermissionRuleDecision() + return PermissionRuleCandidate(pattern, next, default) +} + +private fun String.toPermissionRuleDecision(): PermissionRuleDecision { + return when (lowercase()) { + "approved", "allow" -> PermissionRuleDecision.APPROVED + "denied", "deny" -> PermissionRuleDecision.DENIED + else -> PermissionRuleDecision.PENDING + } +} + private fun toQuestion(dto: QuestionRequestDto): Question { val ref = dto.tool?.let { ToolCallRef(it.messageID, it.callID) } val items = dto.questions.map { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Permission.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Permission.kt index efc9d568497..09f4ea7d974 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Permission.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/model/Permission.kt @@ -4,6 +4,8 @@ enum class PermissionRequestState { PENDING, RESPONDING, RESOLVED, ERROR } enum class PermissionReply { ONCE, ALWAYS, REJECT } +enum class PermissionRuleDecision { APPROVED, DENIED, PENDING } + data class Permission( val id: String, val sessionId: String, @@ -19,6 +21,7 @@ data class Permission( data class PermissionMeta( val command: String? = null, val rules: List = emptyList(), + val ruleDecisions: List = emptyList(), val diff: String? = null, val filePath: String? = null, val fileDiff: PermissionFileDiff? = null, @@ -26,6 +29,12 @@ data class PermissionMeta( val raw: Map = emptyMap(), ) +data class PermissionRuleCandidate( + val pattern: String, + val decision: PermissionRuleDecision = PermissionRuleDecision.PENDING, + val defaultDecision: PermissionRuleDecision = decision, +) + data class PermissionFileDiff( val file: String, val patch: String? = null, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt index 04c7bdb56e2..d9e1cd84506 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/scroll/SessionScroll.kt @@ -346,7 +346,7 @@ internal class SessionScroll( @RequiresEdt private fun layoutScroll() { - root.validate() + component.validate() } @RequiresEdt diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt index 2905bceb0fe..3b29734149d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionLayout.kt @@ -8,6 +8,7 @@ import java.awt.Container import java.awt.Dimension import java.awt.Insets import java.awt.LayoutManager +import java.util.IdentityHashMap /** * A vertical, width-aware layout manager for the session transcript. @@ -33,8 +34,12 @@ class SessionLayout( private val basePad: Insets = JBUI.emptyInsets(), ) : LayoutManager { + private val cache = IdentityHashMap() + override fun addLayoutComponent(name: String, comp: Component) = Unit - override fun removeLayoutComponent(comp: Component) = Unit + override fun removeLayoutComponent(comp: Component) { + cache.remove(comp) + } override fun preferredLayoutSize(parent: Container): Dimension { val ins = insets(parent) @@ -46,9 +51,7 @@ class SessionLayout( if (!first) h += gap(comp) first = false val child = bounds(ins, w, comp) - // Pre-size to available width so HTML panes reflow before we measure - comp.setSize(child.width, comp.height.coerceAtLeast(1)) - h += comp.preferredSize.height + h += measure(comp, child.width) } // w and h are already scaled px (child preferred heights + scaled gaps/insets) and // match what layoutContainer stacks, so return a plain Dimension. A JBDimension would @@ -68,14 +71,36 @@ class SessionLayout( if (!first) y += gap(comp) first = false val child = bounds(ins, w, comp) - // Fix width first so HTML reflows, then read the resulting height - comp.setSize(child.width, comp.height.coerceAtLeast(1)) - val h = comp.preferredSize.height + val h = measure(comp, child.width) comp.setBounds(child.left, y, child.width, h) y += h } } + /** + * Drop the cached measurement for [comp] so the next layout pass re-measures it. + * + * [measure] trusts `comp.isValid` as a freshness signal, which is safe only while `comp` is + * invalidated through this container. A child that is its own validate root (see + * [ai.kilocode.client.session.views.TurnView.isValidateRoot]) can be re-validated independently + * by `RepaintManager` — its `isValid` flips back to `true` before this layout re-measures it, + * so a content change that grows/shrinks its height would otherwise return a stale cached value. + * Callers that mutate such a child's content must forget it here so the cache stays honest. + */ + fun forget(comp: Component) { + cache.remove(comp) + } + + private fun measure(comp: Component, width: Int): Int { + val hit = cache[comp] + if (comp.isValid && hit?.width == width) return hit.height + // Pre-size to available width so HTML panes reflow before we measure. + comp.setSize(width, comp.height.coerceAtLeast(1)) + val h = comp.preferredSize.height + cache[comp] = Measured(width, h) + return h + } + private fun bounds(ins: Insets, width: Int, comp: Component): Bounds { val view = view(comp) ?: return Bounds(ins.left, width) if (view.sessionViewKind != SessionView.Kind.UserPrompt) return Bounds(ins.left, width) @@ -103,6 +128,8 @@ class SessionLayout( private fun view(comp: Component): SessionView? = comp as? SessionView private data class Bounds(val left: Int, val width: Int) + + private data class Measured(val width: Int, val height: Int) } /** diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt index 980972f3b1f..a0dc4d7dfdc 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanel.kt @@ -96,34 +96,37 @@ class SessionMessageListPanel( is SessionModelEvent.TurnRemoved -> onTurnRemoved(event.id) is SessionModelEvent.ContentAdded -> { - msgToView[event.messageId]?.upsertPart(event.content) - msgToTurn[event.messageId]?.syncCopyToolbars() - refresh() + if (msgToView[event.messageId]?.upsertPartChanged(event.content) == true) { + onContentChanged(event.messageId) + } } is SessionModelEvent.ContentUpdated -> { - msgToView[event.messageId]?.upsertPart(event.content) - msgToTurn[event.messageId]?.syncCopyToolbars() - refresh() + if (msgToView[event.messageId]?.upsertPartChanged(event.content) == true) { + onContentChanged(event.messageId) + } } is SessionModelEvent.ContentRemoved -> { - msgToView[event.messageId]?.removePart(event.contentId) - msgToTurn[event.messageId]?.syncCopyToolbars() - refresh() + if (msgToView[event.messageId]?.removePartChanged(event.contentId) == true) { + onContentChanged(event.messageId) + } } is SessionModelEvent.ContentDelta -> { if (event.created) return@addListener + if (event.delta.isEmpty()) return@addListener val handled = msgToView[event.messageId]?.appendDelta(event.contentId, event.delta) == true if (handled) { msgToTurn[event.messageId]?.syncCopyToolbars() + forgetTurn(event.messageId) return@addListener } val content = model.content(event.messageId, event.contentId) if (content != null) { - msgToView[event.messageId]?.upsertPart(content) - msgToTurn[event.messageId]?.syncCopyToolbars() + if (msgToView[event.messageId]?.upsertPartChanged(content) == true) { + onContentChanged(event.messageId) + } } } @@ -132,6 +135,7 @@ class SessionMessageListPanel( is SessionModelEvent.StateChanged -> { syncActive(event.state) + syncSettled(event.state) syncReverted() syncReverting(event.state) anchorFooter() @@ -222,6 +226,7 @@ class SessionMessageListPanel( tv.syncCopyToolbars() syncReverted() add(tv) + syncSettled() anchorFooter() refresh() } @@ -234,8 +239,7 @@ class SessionMessageListPanel( // Remove messages no longer in this turn for (id in prev) { if (id !in next) { - tv.removeMessage(id) - unregister(id) + if (tv.removeMessageChanged(id)) unregister(id) } } @@ -248,6 +252,7 @@ class SessionMessageListPanel( } tv.syncCopyToolbars() syncReverted() + syncSettled() refresh() } @@ -257,6 +262,7 @@ class SessionMessageListPanel( for (msgId in tv.messageIds()) unregister(msgId) remove(tv) Disposer.dispose(tv) + syncSettled() anchorFooter() refresh() } @@ -285,6 +291,7 @@ class SessionMessageListPanel( } syncActive(model.state) + syncSettled(model.state) syncReverted() syncReverting(model.state) banner?.update() @@ -313,6 +320,7 @@ class SessionMessageListPanel( revertingMessage = null removeAll() syncActive(model.state) + syncSettled(model.state) syncReverting(model.state) banner?.update() anchorFooter() @@ -375,6 +383,11 @@ class SessionMessageListPanel( for (mv in msgToView.values) mv.setHiddenQuestionTool(ref) } + private fun syncSettled(state: SessionState = model.state) { + val active = if (state.isBusy()) turnViews.values.lastOrNull() else null + for (view in turnViews.values) view.setSettled(view !== active) + } + /** * Re-insert [question], [permission], [login], and [progress] as the last children * so active views always render after all turn views, and progress is last. @@ -413,6 +426,25 @@ class SessionMessageListPanel( repaint() } + /** + * Handle a content mutation that changed an already-rendered message: sync the turn's copy + * toolbars, forget its cached height, then relayout. [forgetTurn] is essential when the update + * lands on a settled turn — a settled [TurnView] is its own validate root, so `RepaintManager` + * re-validates it independently and its `isValid` flag no longer signals the height change to + * [SessionLayout]'s measurement cache. + */ + private fun onContentChanged(messageId: String) { + msgToTurn[messageId]?.syncCopyToolbars() + forgetTurn(messageId) + refresh() + } + + /** Drop [SessionLayout]'s cached height for the turn holding [messageId] after its content changes. */ + private fun forgetTurn(messageId: String) { + val tv = msgToTurn[messageId] ?: return + (layout as? SessionLayout)?.forget(tv) + } + private fun hover(view: PartView, value: Boolean) { if (value) { val prev = hovered @@ -448,6 +480,9 @@ class SessionMessageListPanel( override fun dispose() { clearHover() + question?.hideView() + permission?.hideView() + login?.hideView() turnViews.values.forEach { remove(it) Disposer.dispose(it) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/popup/HeaderPopup.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/popup/HeaderPopup.kt index a0e0daf0a55..39e8d60de21 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/popup/HeaderPopup.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/popup/HeaderPopup.kt @@ -1,14 +1,20 @@ package ai.kilocode.client.session.ui.popup +import ai.kilocode.client.session.ui.style.SessionUiStyle import com.intellij.openapi.Disposable +import com.intellij.ui.EditorTextField +import com.intellij.ui.components.JBTextArea import com.intellij.util.ui.JBUI import java.awt.BorderLayout import java.awt.Color +import java.awt.Component import java.awt.Container import java.awt.Dimension +import java.awt.Insets import javax.swing.JComponent import javax.swing.JEditorPane import javax.swing.JPanel +import javax.swing.JScrollPane class HeaderPopupRequest( val anchor: JComponent, @@ -20,11 +26,15 @@ class HeaderPopupBody( component: JComponent, val disposable: Disposable, val background: Color, + maxWidth: Int = SessionUiStyle.View.Popup.MAX_WIDTH, ) { - val component: JComponent = HeaderPopupPanel(component) + val component: JComponent = HeaderPopupPanel(component, JBUI.scale(maxWidth)) } -private class HeaderPopupPanel(private val child: JComponent) : JPanel(BorderLayout()) { +private class HeaderPopupPanel( + private val child: JComponent, + private val maxWidth: Int, +) : JPanel(BorderLayout()) { init { // Transparent so the balloon fill shows uniformly behind nested popup content. isOpaque = false @@ -32,14 +42,32 @@ private class HeaderPopupPanel(private val child: JComponent) : JPanel(BorderLay } override fun getPreferredSize(): Dimension { - val size = super.getPreferredSize() - val cap = JBUI.scale(350) - val width = size.width.takeIf { it > 0 }?.coerceAtMost(cap) ?: cap + val width = contentWidth(child).takeIf { it > 0 }?.coerceAtMost(maxWidth) ?: maxWidth fit(child, width) - val height = super.getPreferredSize().height.coerceAtMost(JBUI.scale(450)) + val height = super.getPreferredSize().height.coerceAtMost(JBUI.scale(SessionUiStyle.View.Popup.MAX_HEIGHT)) return Dimension(width, height) } + private fun contentWidth(item: Component): Int = when (item) { + is EditorTextField -> item.preferredSize.width + is JBTextArea -> item.preferredSize.width + is JEditorPane -> item.preferredSize.width + is JScrollPane -> { + val view = item.viewport?.view?.let(::contentWidth) ?: 0 + view + horiz(item.insets) + horiz(item.viewportBorder?.getBorderInsets(item)) + } + // JComponent is a Container, so leaf components (labels, buttons, icons) reach here with no + // children — fall back to their own preferred width instead of measuring an empty child set. + is Container -> { + val kids = item.components + if (kids.isEmpty()) (item as? JComponent)?.preferredSize?.width ?: 0 + else (kids.maxOfOrNull(::contentWidth) ?: 0) + horiz((item as? JComponent)?.insets) + } + else -> 0 + } + + private fun horiz(insets: Insets?): Int = (insets?.left ?: 0) + (insets?.right ?: 0) + private fun fit(item: JComponent, width: Int) { if (width <= 0) return // JBHtmlPane derives wrapped preferred height from the current width, not just HTML content. diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt index d94bbe75266..cb4bc8cb6ce 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/style/SessionUiStyle.kt @@ -37,6 +37,12 @@ object SessionUiStyle { const val BODY_EXTRA_HEIGHT = 16 } + object Popup { + const val MAX_WIDTH = 350 + const val WIDE_MAX_WIDTH = MAX_WIDTH * 2 + const val MAX_HEIGHT = 450 + } + internal const val BORDER_DELTA = 80 internal const val HOVER_BORDER_ALPHA = 0.18f internal const val HOVER_FILL_ALPHA = 0.10f @@ -169,6 +175,7 @@ object SessionUiStyle { object Tool { const val BODY_LINES = 15 const val TASK_LINES = 10 + const val DIFF_LINES = 20 const val PREVIEW_LIMIT = 20_000 fun pending(): Color = UiStyle.Colors.weak() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt index b043bfbf77f..aed735293b8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/MessageView.kt @@ -7,6 +7,7 @@ import ai.kilocode.client.session.model.FileAttachment import ai.kilocode.client.session.model.Message import ai.kilocode.client.session.model.Reasoning import ai.kilocode.client.session.model.StepFinish +import ai.kilocode.client.session.model.Text import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolCallRef import ai.kilocode.client.session.model.ToolExecState @@ -110,7 +111,12 @@ class MessageView( /** Add or update the renderer for [content]. */ @RequiresEdt fun upsertPart(content: Content) { - if (content is StepFinish) return + upsertPartChanged(content) + } + + @RequiresEdt + fun upsertPartChanged(content: Content): Boolean { + if (content is StepFinish) return false if (isHidden(content)) { if (isPromptMention(content)) syncPromptMentions() // Remove any stale view for this content so it disappears when suppressed @@ -122,7 +128,7 @@ class MessageView( stale.remove(content.id) if (!stale.isEmpty()) { refresh() - return + return true } attachments = null } @@ -131,14 +137,15 @@ class MessageView( Disposer.dispose(stale) syncBorder() refresh() + return true } - return + return false } val id = aliases[content.id] if (id != null && content is Reasoning) { - updateAlias(content, id) + if (!updateAlias(content, id)) return false refresh() - return + return true } if (id != null) { aliases.remove(content.id) @@ -149,20 +156,24 @@ class MessageView( if (existing is PromptAttachmentView && content is FileAttachment) { existing.upsert(content) refresh() - return + return true } if (ViewFactory.shouldReplace(existing, content)) { replacePart(content, existing) - return + return true + } + if (content is Text && existing is TextView && existing !is PromptView && existing.markdown() == content.content.toString()) { + return false } existing.update(content) syncPromptToolbar() refresh() - return + return true } addPart(content) syncBorder() refresh() + return true } @RequiresEdt @@ -203,14 +214,15 @@ class MessageView( } @RequiresEdt - private fun updateAlias(content: Reasoning, id: String) { - val view = parts[id] as? ReasoningView ?: return + private fun updateAlias(content: Reasoning, id: String): Boolean { + val view = parts[id] as? ReasoningView ?: return false val prev = sources[content.id].orEmpty() val next = content.content.toString() val delta = if (next.startsWith(prev)) next.removePrefix(prev) else next sources[content.id] = next - if (delta.isEmpty()) return + if (delta.isEmpty()) return false view.update(merged(view, content, delta)) + return true } private fun merged(view: ReasoningView, content: Reasoning, delta: String) = Reasoning(view.contentId).also { @@ -242,16 +254,21 @@ class MessageView( /** Remove the renderer for [contentId] if present. */ @RequiresEdt fun removePart(contentId: String) { + removePartChanged(contentId) + } + + @RequiresEdt + fun removePartChanged(contentId: String): Boolean { if (aliases.remove(contentId) != null) { sources.remove(contentId) - return + return true } - val view = parts.remove(contentId) ?: return + val view = parts.remove(contentId) ?: return false if (view is PromptAttachmentView) { view.remove(contentId) if (!view.isEmpty()) { refresh() - return + return true } attachments = null } @@ -262,6 +279,7 @@ class MessageView( Disposer.dispose(view) syncBorder() refresh() + return true } /** @@ -335,6 +353,7 @@ class MessageView( /** Append a streaming delta to the renderer for [contentId]. */ @RequiresEdt fun appendDelta(contentId: String, delta: String): Boolean { + if (delta.isEmpty()) return false val id = aliases[contentId] if (id != null) sources[contentId] = sources[contentId].orEmpty() + delta val part = parts[id ?: contentId] ?: return false diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionViewIcons.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionViewIcons.kt index 4ce4d3b423d..6eb68f643b6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionViewIcons.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/SessionViewIcons.kt @@ -19,6 +19,10 @@ object SessionViewIcons { val eye = icon("eye") val glasses = icon("glasses") val mcp = icon("mcp") + val ruleApprove = icon("check-small") + val ruleApproveActive = icon("check-small-active") + val ruleDeny = icon("close-small") + val ruleDenyActive = icon("close-small-active") val search = icon("magnifying-glass-menu") val task = icon("task") val warning = icon("warning") diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt index d64f85791cb..b4d8684004f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/TurnView.kt @@ -12,6 +12,7 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.views.base.PartView import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.registry.Registry import com.intellij.util.concurrency.annotations.RequiresEdt import javax.swing.JComponent @@ -38,6 +39,7 @@ class TurnView( ) : SessionLayoutPanel(SessionUiStyle.SessionLayout.GAP), Disposable, SessionEditorStyleTarget, SessionView { private val messages = LinkedHashMap() + private var settled = true override val sessionViewKind = SessionView.Kind.Default @@ -48,6 +50,17 @@ class TurnView( isOpaque = false } + @RequiresEdt + fun setSettled(value: Boolean) { + if (settled == value) return + settled = value + revalidate() + } + + override fun isValidateRoot(): Boolean { + return Registry.`is`("kilo.session.validateRoots", true) && settled + } + /** Add a new [MessageView] for [msg] at the end of this turn. */ fun addMessage(msg: Message): MessageView { val view = MessageView(msg, openFile, style, openUrl, selection, openAttachment, resize, repo, hover, revert) @@ -60,11 +73,17 @@ class TurnView( /** Remove the [MessageView] for [msgId] if present. */ fun removeMessage(msgId: String) { - val view = messages.remove(msgId) ?: return + removeMessageChanged(msgId) + } + + @RequiresEdt + fun removeMessageChanged(msgId: String): Boolean { + val view = messages.remove(msgId) ?: return false remove(view) Disposer.dispose(view) syncCopyToolbars() revalidate() + return true } @RequiresEdt diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ViewFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ViewFactory.kt index 68c06d142a3..bc6cf2b3140 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ViewFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/ViewFactory.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.session.SessionFileOpener import ai.kilocode.client.session.views.base.GenericView import ai.kilocode.client.session.views.base.PartView import ai.kilocode.client.session.views.question.QuestionResultView +import ai.kilocode.client.session.views.tool.EditToolView import ai.kilocode.client.session.views.tool.GlobToolView import ai.kilocode.client.session.views.tool.ReadToolView import ai.kilocode.client.session.views.tool.SearchToolView @@ -54,6 +55,7 @@ object ViewFactory { GlobToolView.canRender(content) -> GlobToolView(content, selection = selection, repo = repo) SearchToolView.canRender(content) -> SearchToolView(content, selection = selection, repo = repo) ReadToolView.canRender(content) -> ReadToolView(content, openFile, selection = selection) + EditToolView.canRender(content) -> EditToolView(content, openFile, selection = selection) TaskToolView.canRender(content) -> TaskToolView(content, selection = selection) else -> ToolView(content, selection = selection) } @@ -100,6 +102,8 @@ object ViewFactory { if (view !is SearchToolView && SearchToolView.canRender(content)) return true if (view is ReadToolView) return !ReadToolView.canRender(content) || QuestionResultView.canRender(content) if (view is ToolView && ReadToolView.canRender(content)) return true + if (view is EditToolView) return !EditToolView.canRender(content) || QuestionResultView.canRender(content) + if (view is ToolView && EditToolView.canRender(content)) return true if (view is TaskToolView) return !TaskToolView.canRender(content) || QuestionResultView.canRender(content) if (view !is TaskToolView && TaskToolView.canRender(content)) return true if (view is ToolView) return QuestionResultView.canRender(content) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/base/BaseQuestionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/base/BaseQuestionView.kt index 28763c346ab..5d39e41feb1 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/base/BaseQuestionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/base/BaseQuestionView.kt @@ -77,7 +77,7 @@ class BaseQuestionView( private val text = Stack.vertical() - private val header = object : JPanel(BorderLayout(UiStyle.Gap.sm(), 0)) { + private val header = object : JPanel(BorderLayout(UiStyle.Gap.md(), 0)) { override fun getMaximumSize(): Dimension { val size = preferredSize return Dimension(Int.MAX_VALUE, size.height) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionDiffView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionDiffView.kt index fd9c200b8e9..026228d3c58 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionDiffView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionDiffView.kt @@ -4,10 +4,9 @@ import ai.kilocode.client.session.model.PermissionFileDiff import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget import ai.kilocode.client.ui.DiffStatBadge -import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.Stack import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel -import java.awt.FlowLayout /** * Renders a single [PermissionFileDiff] inside a permission card as a compact diff-stat badge. @@ -35,9 +34,7 @@ class PermissionDiffView( isOpaque = false border = JBUI.Borders.empty() - val inner = object : javax.swing.JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)) { - init { isOpaque = false } - } + val inner = Stack.horizontal() inner.add(badge) addToCenter(inner) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt index a3acdfe1c2e..d5323dd58a3 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/permission/PermissionView.kt @@ -1,39 +1,62 @@ package ai.kilocode.client.session.views.permission import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.plugin.KiloPluginSettings import ai.kilocode.client.session.model.Permission import ai.kilocode.client.session.model.PermissionFileDiff +import ai.kilocode.client.session.model.PermissionRuleCandidate +import ai.kilocode.client.session.model.PermissionRuleDecision import ai.kilocode.client.session.model.PermissionRequestState import ai.kilocode.client.session.ui.SessionView import ai.kilocode.client.session.views.base.BaseQuestionView import ai.kilocode.client.session.ui.selection.SessionSelection import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget -import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.views.SessionViewIcons import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.iconButton +import ai.kilocode.client.ui.editor.BashCommandHighlighter import ai.kilocode.client.ui.layout.HAlign import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.client.ui.layout.StackAxis import ai.kilocode.client.ui.layout.VAlign import ai.kilocode.client.ui.layout.align +import ai.kilocode.client.ui.md.MdCodeBlockBorder +import ai.kilocode.client.ui.md.MdCodeBlockFactory +import ai.kilocode.client.ui.md.MdCodeBlockOptions +import ai.kilocode.client.ui.md.MdCommon +import ai.kilocode.client.ui.md.MdView +import ai.kilocode.client.ui.md.MdViewFactory +import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto import ai.kilocode.rpc.dto.PermissionReplyDto +import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.fileTypes.PlainTextFileType +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.Disposer -import com.intellij.ui.ColorUtil -import com.intellij.ui.components.JBHtmlPane -import com.intellij.ui.components.JBHtmlPaneConfiguration -import com.intellij.ui.components.JBHtmlPaneStyleConfiguration +import com.intellij.ui.EditorTextField import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.JBTextArea +import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel -import com.intellij.xml.util.XmlStringUtil import java.awt.BorderLayout +import java.awt.Dimension import java.awt.Container -import java.awt.FlowLayout +import java.awt.Cursor +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.Rectangle +import java.awt.RenderingHints +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent import javax.swing.JButton +import javax.swing.JComponent import javax.swing.JPanel -import javax.swing.text.html.StyleSheet +import javax.swing.ScrollPaneConstants /** * Transcript-style permission view — rendered inside [ai.kilocode.client.session.ui.SessionMessageListPanel] @@ -43,22 +66,29 @@ import javax.swing.text.html.StyleSheet * Shows a compact row with action label and target as an inline code fragment, plus diff badges. */ class PermissionView( - private val reply: (String, PermissionReplyDto) -> Unit, + private val reply: (String, PermissionReplyDto, PermissionAlwaysRulesDto?) -> Unit, private val selection: SessionSelection? = null, focus: (() -> Unit)? = null, -) : BorderLayoutPanel(), SessionEditorStyleTarget, SessionView { +) : BorderLayoutPanel(), SessionEditorStyleTarget, SessionView, Disposable { override val sessionViewKind = SessionView.Kind.Default private var requestId: String? = null + private var responding = false private var style = SessionEditorStyle.current() private val card = BaseQuestionView(selection, focus) - private val body = Stack.vertical() + private val body = Stack.vertical(gap = UiStyle.Gap.sm()) + private val desc = makeDescription() + private val codeSlot = BorderLayoutPanel().apply { isVisible = false } + private val diffRow = Stack.horizontal().apply { isVisible = false } + private val rules = PermissionRulesView(selection) { syncPrimaryText() }.apply { isVisible = false } + private val state = JBLabel().apply { + border = JBUI.Borders.empty(UiStyle.Gap.sm(), 0, 0, 0) + isVisible = false + } - // Track target panes for style updates - private val panes = mutableListOf() - private val regs = mutableListOf() + private var md: MdView? = null private val diffViews = mutableListOf() private val ID_DENY = "deny" @@ -68,125 +98,91 @@ class PermissionView( isOpaque = false isVisible = false - card.setHeaderIcon(SessionViewIcons.warning, KiloBundle.message("session.permission.title")) + card.setHeaderIcon(AllIcons.General.Warning, KiloBundle.message("session.permission.title")) card.setContent(body) - card.setActions(listOf( - BaseQuestionView.Action(ID_DENY, KiloBundle.message("session.permission.deny"), primary = false) { decide("reject") }, - BaseQuestionView.Action(ID_RUN, KiloBundle.message("session.permission.run"), primary = true) { decide("once") }, - )) + body.next(desc).next(codeSlot).next(diffRow).next(rules).next(state) + card.setActions( + listOf( + BaseQuestionView.Action(ID_DENY, KiloBundle.message("session.permission.reject"), primary = false) { reject() }, + BaseQuestionView.Action(ID_RUN, KiloBundle.message("session.permission.allow.once"), primary = true) { allow() }, + ), + ) addToCenter(card) } /** Populate the view for [permission] and make it visible. */ + @RequiresEdt fun show(permission: Permission) { + val prev = requestId requestId = permission.id card.setHeader(KiloBundle.message("session.permission.title")) - - body.removeAll() - disposeRegs() - panes.clear() - diffViews.clear() + syncDescription(description(permission)) val tool = permission.name - val cmd = permission.meta.command + val target = if (tool == "bash") permission.meta.command else resolveTarget(permission) + syncCode(tool, target) + syncDiffs(permission.meta.fileDiffs) + responding = permission.state == PermissionRequestState.RESPONDING || permission.state == PermissionRequestState.RESOLVED + rules.update(permission.meta.ruleDecisions, reset = prev != permission.id) + syncState(permission) + syncPrimaryText() - val action = toolLabel(tool) - val target = cmd ?: resolveTarget(permission) - addDetailRow(action, target, permission.meta.fileDiffs) - addStateMessage(permission) - - val responding = permission.state == PermissionRequestState.RESPONDING || permission.state == PermissionRequestState.RESOLVED - card.setActionEnabled(ID_RUN, !responding) - card.setActionEnabled(ID_DENY, !responding) + syncButtons(responding) + rules.setControlsEnabled(!responding) isVisible = true refresh() } /** Hide this view and clear the active request id. */ + @RequiresEdt fun hideView() { requestId = null - body.removeAll() - disposeRegs() - panes.clear() + responding = false + disposeMd() diffViews.clear() + diffRow.removeAll() + diffRow.isVisible = false + rules.update(emptyList(), reset = true) + state.isVisible = false isVisible = false refresh() } + @RequiresEdt override fun applyStyle(style: SessionEditorStyle) { this.style = style card.applyStyle(style) - for (pane in panes) { - applyTargetPane(pane) - } + desc.font = style.hintFont + desc.foreground = UiStyle.Colors.weak() + rules.applyStyle(style) + md?.let { applyCodeStyle(it) } for (dv in diffViews) { dv.applyStyle(style) } } - /** Adds a three-column permission detail row: tool, target, and changes. */ - private fun addDetailRow(action: String, target: String?, diffs: List) { - val row = JPanel(BorderLayout(SessionUiStyle.View.Layout.GAP, 0)).apply { - isOpaque = false - } - - val actionLbl = JBLabel(action).apply { - font = UiStyle.Fonts.bold() - } - row.add(actionLbl.align(HAlign.LEFT, VAlign.CENTER), BorderLayout.WEST) - - if (!target.isNullOrBlank()) { - val pane = targetPane(target) - panes.add(pane) - row.add(pane.align(HAlign.TRACK, VAlign.CENTER), BorderLayout.CENTER) - } + @RequiresEdt + private fun syncDescription(text: String) { + if (desc.text != text) desc.text = text + desc.isVisible = text.isNotBlank() + } + @RequiresEdt + private fun syncDiffs(diffs: List) { + diffRow.removeAll() + diffViews.clear() + diffRow.isVisible = diffs.isNotEmpty() if (diffs.isNotEmpty()) { - val changes = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply { - isOpaque = false - } for (diff in diffs) { val dv = PermissionDiffView(diff) diffViews.add(dv) - changes.add(dv) + diffRow.add(dv) } - row.add(changes.align(HAlign.RIGHT, VAlign.CENTER), BorderLayout.EAST) } - - body.add(row) - } - - private fun targetPane(text: String) = JBHtmlPane( - JBHtmlPaneStyleConfiguration {}, - JBHtmlPaneConfiguration { - customStyleSheetProvider { targetSheet() } - }, - ).apply { - isEditable = false - isOpaque = true - this.text = "
${XmlStringUtil.escapeString(text)}
" - applyTargetPane(this) - selection?.register(this)?.let(regs::add) - } - - private fun applyTargetPane(pane: JBHtmlPane) { - pane.font = style.transcriptFont - pane.foreground = style.editorForeground - pane.background = SessionUiStyle.View.Surface.headerHoverBgColor() - pane.reloadCssStylesheets() - } - - private fun targetSheet(): StyleSheet { - val sheet = StyleSheet() - val font = style.transcriptFont - val fg = ColorUtil.toHtmlColor(style.editorForeground) - val bg = ColorUtil.toHtmlColor(SessionUiStyle.View.Surface.headerHoverBgColor()) - val family = font.name.replace("\\", "\\\\").replace("'", "\\'") - sheet.addRule("body { margin: 0; padding: 0 ${UiStyle.Gap.xs()}px; color: $fg; background: $bg; font-family: '$family', monospace; font-size: ${font.size}pt }") - sheet.addRule("pre { margin: 0; white-space: pre-wrap; font-family: '$family', monospace; font-size: ${font.size}pt }") - return sheet + diffRow.revalidate() + diffRow.repaint() } private fun resolveTarget(permission: Permission): String? { @@ -201,19 +197,135 @@ class PermissionView( } } - private fun addStateMessage(permission: Permission) { + @RequiresEdt + private fun syncState(permission: Permission) { val msg = when (permission.state) { PermissionRequestState.ERROR -> permission.message ?: KiloBundle.message("session.permission.error") PermissionRequestState.RESPONDING -> KiloBundle.message("session.permission.responding") else -> null - } ?: return - - val label = JBLabel(msg).apply { - border = JBUI.Borders.empty(UiStyle.Gap.sm(), 0, 0, 0) } - body.add(label) + state.text = msg.orEmpty() + state.isVisible = msg != null + } + + @RequiresEdt + private fun syncButtons(responding: Boolean) { + val approved = rules.approved().isNotEmpty() + val denied = rules.denied().isNotEmpty() + card.setActionEnabled(ID_RUN, !responding && !(denied && !approved)) + card.setActionEnabled(ID_DENY, !responding && !(approved && !denied)) + } + + @RequiresEdt + private fun syncCode(tool: String, target: String?) { + if (target.isNullOrBlank()) { + codeSlot.isVisible = false + md?.clear() + return + } + + val view = ensureMd() + val lang = if (tool == "bash") "bash" else "" + val text = fenced(target, lang) + if (view.markdown() != text) view.set(text) + applyCodeStyle(view) + codeSlot.isVisible = true + } + + @RequiresEdt + private fun ensureMd(): MdView { + md?.let { return it } + val view = MdViewFactory.create( + style, + selection, + MdCodeBlockFactory.default( + MdCodeBlockOptions( + border = MdCodeBlockBorder.None, + verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + editorOnly = true, + ), + ), + ) + md = view + applyCodeStyle(view) + codeSlot.add(view.component, BorderLayout.CENTER) + return view + } + + @RequiresEdt + private fun applyCodeStyle(view: MdView) { + view.applyStyle(style) + view.font = style.transcriptFont + view.foreground = style.editorForeground + view.background = style.editorBackground + view.preBg = MdCommon.defaults(style).preBg + view.codeFont = style.editorFamily + view.component.border = JBUI.Borders.empty() + } + + private fun description(permission: Permission): String = if (permission.name == "bash") { + permission.meta.raw["description"] ?: toolLabel(permission.name) + } else { + toolLabel(permission.name) + } + + private fun makeDescription(): JBTextArea { + val area = object : JBTextArea() { + override fun getPreferredSize() = withWidth(super.getPreferredSize().height) + + override fun getMaximumSize(): Dimension { + val size = preferredSize + return Dimension(Int.MAX_VALUE, size.height) + } + + override fun scrollRectToVisible(aRect: Rectangle) {} + + private fun withWidth(fallback: Int): Dimension { + val w = availableWidth() + if (w <= 0) return Dimension(super.getPreferredSize().width, fallback) + val old = size + setSize(w, Int.MAX_VALUE) + val ps = super.getPreferredSize() + setSize(old) + return Dimension(w, ps.height) + } + + private fun availableWidth(): Int { + var node = parent + while (node != null) { + if (node.width > 0) { + val ins = node.insets + return (node.width - ins.left - ins.right).coerceAtLeast(0) + } + node = node.parent + } + return width + } + }.apply { + isEditable = false + isOpaque = false + isFocusable = false + caret.isVisible = false + caret.isSelectionVisible = false + lineWrap = true + wrapStyleWord = true + foreground = UiStyle.Colors.weak() + font = style.hintFont + border = JBUI.Borders.empty() + isVisible = false + } + selection?.register(area) + return area + } + + private fun fenced(text: String, lang: String): String = buildString { + val fence = fence(text) + append(fence).append(lang).append('\n') + append(text) + if (!text.endsWith('\n')) append('\n') + append(fence) } private fun toolLabel(tool: String): String = when (tool) { @@ -238,11 +350,42 @@ class PermissionView( else -> tool } - private fun decide(value: String) { + @RequiresEdt + private fun allow() { val id = requestId ?: return card.setActionEnabled(ID_RUN, false) card.setActionEnabled(ID_DENY, false) - reply(id, PermissionReplyDto(reply = value)) + rules.setControlsEnabled(false) + reply(id, PermissionReplyDto(reply = "once"), rulePayload()) + } + + @RequiresEdt + private fun reject() { + val id = requestId ?: return + card.setActionEnabled(ID_RUN, false) + card.setActionEnabled(ID_DENY, false) + rules.setControlsEnabled(false) + reply(id, PermissionReplyDto(reply = "reject"), rulePayload()) + } + + @RequiresEdt + private fun rulePayload(): PermissionAlwaysRulesDto? { + if (!rules.anyDecided()) return null + return PermissionAlwaysRulesDto(approvedAlways = rules.approved(), deniedAlways = rules.denied()) + } + + @RequiresEdt + private fun syncPrimaryText() { + val key = if (rules.anyDecided()) "session.permission.allow" else "session.permission.allow.once" + card.setActionText( + ID_RUN, + KiloBundle.message(key), + ) + card.setActionText( + ID_DENY, + KiloBundle.message("session.permission.reject"), + ) + syncButtons(responding) } private fun refresh() { @@ -252,17 +395,36 @@ class PermissionView( parent?.repaint() } - private fun disposeRegs() { - regs.forEach(Disposer::dispose) - regs.clear() + @RequiresEdt + private fun disposeMd() { + val view = md ?: return + md = null + codeSlot.remove(view.component) + codeSlot.isVisible = false + Disposer.dispose(view) + } + + override fun dispose() { + disposeMd() + Disposer.dispose(rules) + } + + private fun codeEditors(): List = mdScrolls().mapNotNull { it.viewport.view as? EditorTextField } + + private fun mdScrolls(): List = (md?.component as? JPanel)?.components?.filterIsInstance() ?: emptyList() + + private fun fence(text: String): String { + val size = Regex("`+").findAll(text).maxOfOrNull { it.value.length } ?: 0 + return "`".repeat(maxOf(3, size + 1)) } // Test helpers - internal fun runButtonForTest() = buttons(card).first { it.text == KiloBundle.message("session.permission.run") } - internal fun denyButtonForTest() = buttons(card).first { it.text == KiloBundle.message("session.permission.deny") } - internal fun codeLabelsForTest() = panes.toList() + internal fun runButtonForTest() = buttons(card).first { it.text == KiloBundle.message("session.permission.allow") || it.text == KiloBundle.message("session.permission.allow.once") } + internal fun denyButtonForTest() = buttons(card).first { it.text == KiloBundle.message("session.permission.reject") } + internal fun codeLabelsForTest() = codeEditors() internal fun diffViewsForTest() = diffViews.toList() internal fun headerFontForTest() = textAreas(card).first { it.font.isBold }.font + internal fun rulesForTest() = rules private fun buttons(root: Container): List { val result = mutableListOf() @@ -282,3 +444,375 @@ class PermissionView( return result } } + +internal class PermissionRulesView( + private val selection: SessionSelection?, + private val changed: () -> Unit, +) : Stack(StackAxis.VERTICAL, UiStyle.Gap.sm()), Disposable { + private val title = JBLabel(KiloBundle.message("session.permission.rules.title")) + private val arrow = JBLabel(SessionViewIcons.chevronCollapsed) + private val header = Stack.horizontal(gap = UiStyle.Gap.xs()) + private val inset = Stack.vertical(gap = UiStyle.Gap.xs()).apply { + border = JBUI.Borders.emptyLeft(SessionViewIcons.chevronCollapsed.iconWidth) + } + private var box: Stack? = null + private val rows = mutableListOf() + private var style = SessionEditorStyle.current() + + init { + header.next(arrow.align(HAlign.LEFT, VAlign.CENTER)).next(title.align(HAlign.LEFT, VAlign.CENTER)).fill(0) + header.cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + header.addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + toggle() + } + }) + next(header) + next(inset) + syncArrow() + } + + private var candidates = emptyList() + private var baseline = emptyMap() + private var decisions = emptyMap() + + @RequiresEdt + fun update(candidates: List, reset: Boolean = false) { + isVisible = candidates.isNotEmpty() + val old = if (reset) emptyMap() else decisions + rows.associate { it.pattern to it.decision } + val patterns = candidates.map { it.pattern } + val stale = this.candidates.map { it.pattern } != patterns + this.candidates = candidates + if (reset || stale) baseline = candidates.associate { it.pattern to it.decision } + decisions = candidates.associate { it.pattern to (old[it.pattern] ?: it.decision) } + if (candidates.isEmpty()) { + box?.let { + if (it.parent === inset) inset.remove(it) + } + box = null + disposeRows() + syncArrow() + changed() + return + } + if (stale && box != null) syncBody(rebuild = true) else syncRows() + syncExpanded() + syncArrow() + changed() + } + + @RequiresEdt + private fun body(): Stack { + val current = box + if (current != null) return current + val root = Stack.vertical(gap = UiStyle.Gap.xs()) + box = root + syncBody(rebuild = true) + return root + } + + @RequiresEdt + private fun syncBody(rebuild: Boolean) { + val root = box ?: return + if (rebuild) { + root.removeAll() + disposeRows() + for (candidate in candidates) { + val row = RuleRow(candidate.pattern, candidate.defaultDecision, style, selection) { pattern, decision -> + decisions = decisions + (pattern to decision) + syncRows() + changed() + } + rows.add(row) + root.next(row) + } + } + syncRows() + root.revalidate() + root.repaint() + } + + @RequiresEdt + private fun syncRows() { + for (row in rows) row.update(decisions[row.pattern] ?: PermissionRuleDecision.PENDING) + } + + @RequiresEdt + private fun syncExpanded() { + if (box?.parent === inset) return + if (!KiloPluginSettings.getPermissionRulesExpanded()) return + inset.add(body()) + } + + @RequiresEdt + fun toggle() { + if (candidates.isEmpty()) return + val root = body() + if (isExpanded()) inset.remove(root) else inset.add(root) + KiloPluginSettings.setPermissionRulesExpanded(isExpanded()) + syncArrow() + revalidate() + repaint() + } + + @RequiresEdt + fun isExpanded(): Boolean = box?.parent === inset + + @RequiresEdt + fun approved(): List = candidates.map { it.pattern }.filter { decisions[it] == PermissionRuleDecision.APPROVED } + + @RequiresEdt + fun denied(): List = candidates.map { it.pattern }.filter { decisions[it] == PermissionRuleDecision.DENIED } + + @RequiresEdt + fun anyDecided(): Boolean = decisions.any { baseline[it.key] != it.value } + + @RequiresEdt + fun setControlsEnabled(enabled: Boolean) { + for (row in rows) row.setControlsEnabled(enabled) + } + + @RequiresEdt + fun applyStyle(style: SessionEditorStyle) { + this.style = style + for (row in rows) row.applyStyle(style) + } + + @RequiresEdt + fun approveButtonsForTest(): List = rows.map { it.approveButtonForTest() } + + @RequiresEdt + fun denyButtonsForTest(): List = rows.map { it.denyButtonForTest() } + + @RequiresEdt + fun commandFieldsForTest(): List = rows.map { it.commandFieldForTest() } + + @RequiresEdt + fun hintLabelsForTest(): List = rows.map { it.hintLabelForTest() } + + @RequiresEdt + private fun syncArrow() { + arrow.icon = if (isExpanded()) SessionViewIcons.chevronExpanded else SessionViewIcons.chevronCollapsed + } + + @RequiresEdt + private fun disposeRows() { + for (row in rows) Disposer.dispose(row) + rows.clear() + } + + override fun dispose() { + disposeRows() + } + + private class RuleRow( + val pattern: String, + private val default: PermissionRuleDecision, + style: SessionEditorStyle, + selection: SessionSelection?, + private val changed: (String, PermissionRuleDecision) -> Unit, + ) : Stack(StackAxis.VERTICAL, UiStyle.Gap.xs()), Disposable { + var decision = PermissionRuleDecision.PENDING + private set + + private val approve = RuleToggleButton(true) { + changed(pattern, if (decision == PermissionRuleDecision.APPROVED) PermissionRuleDecision.PENDING else PermissionRuleDecision.APPROVED) + } + private val deny = RuleToggleButton(false) { + changed(pattern, if (decision == PermissionRuleDecision.DENIED) PermissionRuleDecision.PENDING else PermissionRuleDecision.DENIED) + } + private val hint = JBLabel() + private val field = RuleCommandField(pattern, style, selection) + private val controls = Stack.horizontal(gap = UiStyle.Gap.xs()) + + init { + controls.next(approve.align(HAlign.LEFT, VAlign.CENTER)) + controls.next(deny.align(HAlign.LEFT, VAlign.CENTER)) + controls.gap(UiStyle.Gap.lg()) + controls.next(field.align(HAlign.LEFT, VAlign.CENTER)) + controls.fill(0) + next(controls) + next(hint.align(HAlign.LEFT, VAlign.CENTER)) + applyStyle(style) + update(PermissionRuleDecision.PENDING) + } + + @RequiresEdt + fun update(value: PermissionRuleDecision) { + decision = value + approve.update(value == PermissionRuleDecision.APPROVED) + deny.update(value == PermissionRuleDecision.DENIED) + hint.text = KiloBundle.message(when (value) { + PermissionRuleDecision.APPROVED -> "session.permission.rule.hint.approve" + PermissionRuleDecision.DENIED -> "session.permission.rule.hint.deny" + PermissionRuleDecision.PENDING -> "session.permission.rule.hint.default" + }, defaultLabel()) + } + + private fun defaultLabel(): String = when (default) { + PermissionRuleDecision.APPROVED -> KiloBundle.message("session.permission.allow") + PermissionRuleDecision.DENIED -> KiloBundle.message("session.permission.reject") + PermissionRuleDecision.PENDING -> KiloBundle.message("session.permission.ask") + } + + @RequiresEdt + fun setControlsEnabled(enabled: Boolean) { + approve.isEnabled = enabled + deny.isEnabled = enabled + } + + @RequiresEdt + fun applyStyle(style: SessionEditorStyle) { + hint.font = style.hintFont + hint.foreground = UiStyle.Colors.weak() + field.applyStyle(style) + } + + fun approveButtonForTest(): JButton = approve + + fun denyButtonForTest(): JButton = deny + + fun commandFieldForTest(): EditorTextField = field + + fun hintLabelForTest(): JBLabel = hint + + override fun dispose() { + field.dispose() + } + } + + private class RuleCommandField( + value: String, + private var style: SessionEditorStyle, + private val selection: SessionSelection?, + ) : EditorTextField( + EditorFactory.getInstance().createDocument(value.trimEnd('\n')), + ProjectManager.getInstance().defaultProject, + PlainTextFileType.INSTANCE, + true, + false, + ) { + private var reg: Disposable? = null + + init { + setFontInheritedFromLAF(false) + font = style.editorFont + addSettingsProvider(::install) + reg = selection?.register(this) + } + + override fun getMaximumSize(): Dimension { + val size = preferredSize + return Dimension(Int.MAX_VALUE, size.height) + } + + @RequiresEdt + fun applyStyle(style: SessionEditorStyle) { + this.style = style + font = style.editorFont + getEditor(false)?.let(::apply) + } + + @RequiresEdt + fun dispose() { + reg?.let(Disposer::dispose) + reg = null + getEditor(false)?.let(EditorFactory.getInstance()::releaseEditor) + } + + private fun install(ed: com.intellij.openapi.editor.Editor) { + (ed as? EditorEx)?.let(::apply) + } + + private fun apply(ed: EditorEx) { + style.applyToEditor(ed) + ed.setBorder(JBUI.Borders.empty()) + ed.scrollPane.border = JBUI.Borders.empty() + ed.scrollPane.viewportBorder = JBUI.Borders.empty() + ed.backgroundColor = style.editorBackground + ed.scrollPane.background = style.editorBackground + ed.scrollPane.isOpaque = true + ed.scrollPane.viewport.isOpaque = true + ed.scrollPane.viewport.background = style.editorBackground + ed.settings.isUseSoftWraps = false + ed.settings.isAdditionalPageAtBottom = false + ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + ed.scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER + BashCommandHighlighter.apply(ed, text) + } + } + + private class RuleToggleButton( + private val approve: Boolean, + private val changed: () -> Unit, + ) : JButton() { + private var active = false + private var over = false + + init { + iconButton(this) + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + addActionListener { changed() } + addMouseListener(object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent) = syncOver(true) + + override fun mouseExited(e: MouseEvent) = syncOver(false) + }) + update(false) + } + + override fun getPreferredSize(): Dimension = JBUI.size(24, 24) + + override fun getMinimumSize(): Dimension = preferredSize + + override fun getMaximumSize(): Dimension = preferredSize + + override fun paintComponent(g: Graphics) { + if (isEnabled && (active || over)) paintFill(g) + super.paintComponent(g) + } + + @RequiresEdt + fun update(value: Boolean) { + active = value + icon = when { + approve && value -> SessionViewIcons.ruleApproveActive + approve -> SessionViewIcons.ruleApprove + value -> SessionViewIcons.ruleDenyActive + else -> SessionViewIcons.ruleDeny + } + val key = when { + approve && value -> "session.permission.rule.approve.remove" + approve -> "session.permission.rule.approve.add" + value -> "session.permission.rule.deny.remove" + else -> "session.permission.rule.deny.add" + } + val text = KiloBundle.message(key) + toolTipText = text + getAccessibleContext().accessibleName = text + repaint() + } + + private fun paintFill(g: Graphics) { + val g2 = g.create() as Graphics2D + try { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON) + val base = UiStyle.Colors.bg() + g2.color = when { + active -> UiStyle.Colors.blend(base, if (approve) UiStyle.Colors.addedForeground() else UiStyle.Colors.removedForeground(), 0.15f) + else -> UiStyle.Colors.actionHoverBackground() + } + val arc = JBUI.scale(JBUI.getInt("Button.arc", 6)) + g2.fillRoundRect(0, 0, width, height, arc, arc) + } finally { + g2.dispose() + } + } + + private fun syncOver(value: Boolean) { + if (over == value) return + over = value + repaint() + } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/EditToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/EditToolView.kt new file mode 100644 index 00000000000..865e2d42f40 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/EditToolView.kt @@ -0,0 +1,276 @@ +package ai.kilocode.client.session.views.tool + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionFileOpener +import ai.kilocode.client.session.model.Content +import ai.kilocode.client.session.model.Tool +import ai.kilocode.client.session.model.ToolKind +import ai.kilocode.client.session.ui.popup.HeaderPopupBody +import ai.kilocode.client.session.ui.popup.HeaderPopupRequest +import ai.kilocode.client.session.ui.selection.SessionSelection +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionUiStyle +import ai.kilocode.client.session.views.base.SecondarySessionPartView +import ai.kilocode.client.telemetry.Telemetry +import ai.kilocode.client.ui.DiffStatBadge +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.md.MdCodeBlockBorder +import ai.kilocode.client.ui.md.MdCodeBlockOptions +import com.intellij.openapi.actionSystem.DataSink +import com.intellij.openapi.actionSystem.UiDataProvider +import com.intellij.openapi.util.Disposer +import com.intellij.ui.EditorTextField +import com.intellij.ui.components.JBLabel +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI +import java.awt.Dimension +import javax.swing.ScrollPaneConstants + +/** + * Renders write tools (edit/write/apply_patch) with a Read-style header — an "Edit" title and a + * clickable file link — plus a diff-stat changes tag. The expandable body and the collapsed hover + * popup both render the unified diff via the shared markdown code editor, which colors it as a diff. + */ +class EditToolView( + tool: Tool, + private val openFile: SessionFileOpener = { _, _ -> }, + private val selection: SessionSelection? = null, + private val parts: ToolParts = toolParts(tool, openFile), + private var body: EditBody = editBody(tool, selection, openFile), +) : SecondarySessionPartView(parts.header, { body.mount(tool) }), UiDataProvider { + + override val contentId: String = tool.id + + private var item = tool + private var style = SessionEditorStyle.current() + private var multi = editFiles(tool).size > 1 + private val badge = DiffStatBadge(0, 0) + private val filesTag = JBLabel().apply { + foreground = UiStyle.Colors.weak() + font = JBFont.small() + border = JBUI.Borders.emptyRight(SessionUiStyle.View.Layout.HORIZONTAL_PADDING) + isVisible = false + } + + init { + body.parent = this + parts.controls.add(filesTag) + parts.controls.add(badge) + bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot, filesTag, badge) + applyStyle(style) + sync() + } + + override fun uiDataSnapshot(sink: DataSink) { + selection?.provideCopy(sink) { body.markdown() ?: diffMarkdown(item) } + } + + @RequiresEdt + override fun expand(): Boolean { + val changed = super.expand() + if (!changed) return false + syncBody() + body.applyStyle(style) + return true + } + + @RequiresEdt + override fun getPreferredSize(): Dimension { + val size = super.getPreferredSize() + if (!bodyVisible()) return size + val height = row.preferredSize.height + (body.panel()?.preferredSize?.height ?: 0) + return Dimension(size.width, minOf(size.height, height)) + } + + @RequiresEdt + override fun update(content: Content) { + if (content !is Tool) return + item = content + var changed = if (!expandable()) collapse() else false + changed = swapBody() || changed + changed = sync() || changed + changed = syncBody() || changed + if (changed) refresh() + } + + /** Rebuild the body delegate when a streaming tool crosses the single/multi-file boundary. */ + @RequiresEdt + private fun swapBody(): Boolean { + val next = editFiles(item).size > 1 + if (next == multi) return false + multi = next + val expanded = isExpanded() + discardBody() + body.disposeBody() + body = editBody(item, selection, openFile).also { it.parent = this } + if (expanded) expand() + return true + } + + @RequiresEdt + fun labelText(): String = listOf(parts.title.text, subtitleText(parts), parts.state.text) + .filter { it.isNotBlank() } + .joinToString(" ") + + @RequiresEdt + fun bodyText(): String = editDiff(item) + @RequiresEdt + fun hasToggle(): Boolean = arrow.isVisible + @RequiresEdt + fun diffStat(): Pair = diffStat(item) + @RequiresEdt + internal fun badgeVisible() = badge.isVisible + @RequiresEdt + internal fun filesTagVisible() = filesTag.isVisible + @RequiresEdt + internal fun filesTagText() = filesTag.text + @RequiresEdt + internal fun linkVisible() = parts.link.isVisible + @RequiresEdt + internal fun linkLabel() = parts.label + @RequiresEdt + internal fun linkHref() = parts.href + @RequiresEdt + internal fun linkTooltip() = parts.link.toolTipText + @RequiresEdt + internal fun openLink() = parts.openLink() + @RequiresEdt + internal fun bodyCreated() = body.created() + @RequiresEdt + internal fun bodyVisible() = body.attached(this) + @RequiresEdt + internal fun markdown() = body.markdown() ?: diffMarkdown(item) + @RequiresEdt + internal fun codeEditors(): List = body.codeEditors() + + @RequiresEdt + override fun headerPopup(): HeaderPopupRequest? { + if (isExpanded()) return null + if (editDiff(item).isBlank()) return null + return HeaderPopupRequest(row, build = { buildPopupBody() }) { + Telemetry.send("Header Popup Shown", mapOf("surface" to "session", "tool" to "edit")) + } + } + + @RequiresEdt + override fun applyStyle(style: SessionEditorStyle) { + this.style = style + var changed = false + changed = setFont(parts.title, style.boldEditorFont) || changed + changed = setFont(parts.sub, style.transcriptFont) || changed + changed = setFont(parts.link, style.transcriptFont) || changed + changed = setFont(parts.state, style.smallEditorFont) || changed + changed = body.applyStyle(style) || changed + if (changed) refresh() + } + + private fun expandable(): Boolean = + editDiff(item).isNotBlank() || output(item).isNotBlank() || !item.error.isNullOrBlank() + + private fun sync(): Boolean { + val expand = expandable() + var changed = false + changed = syncExpandable(expand) || changed + changed = setVisible(parts.state, !expand) || changed + changed = setIcon(parts.glyph, icon(item)) || changed + changed = setForeground(parts.glyph, color(item)) || changed + val count = editFiles(item).size + val titleText = if (count > 1) KiloBundle.message("session.part.tool.patch") else title(item) + changed = setText(parts.title, titleText) || changed + val path = if (count > 1) null else editPath(item) + changed = setFileTarget(parts, path, if (path == null) "" else tail(path)) || changed + changed = setForeground(parts.title, titleColor(item)) || changed + changed = setForeground(parts.link, UiStyle.Colors.fg()) || changed + changed = setText(parts.state, stateText(item)) || changed + changed = setForeground(parts.state, color(item)) || changed + changed = syncFilesTag(count) || changed + changed = syncBadge() || changed + return changed + } + + private fun syncFilesTag(count: Int): Boolean { + val show = count > 1 + var changed = setVisible(filesTag, show) + if (show) changed = setText(filesTag, KiloBundle.message("session.part.tool.edit.files", count)) || changed + return changed + } + + private fun syncBadge(): Boolean { + val (added, removed) = diffStat(item) + val show = added > 0 || removed > 0 + val changed = setVisible(badge, show) + if (show) badge.update(added, removed) + return changed + } + + private fun syncBody(): Boolean = body.update(item) + + @RequiresEdt + private fun buildPopupBody(): HeaderPopupBody { + val owner = Disposer.newDisposable("Edit popup body") + val popup = popupBody(item, selection, openFile).also { it.parent = owner } + // mount() already renders the current item (ToolMarkdownBody.mount calls update; PatchBody.mount + // calls rebuild and sets its signature), so a follow-up update() here would be a no-op. + val panel = popup.mount(item) + popup.applyStyle(style) + return HeaderPopupBody(panel, owner, style.editorBackground, SessionUiStyle.View.Popup.WIDE_MAX_WIDTH) + } + + override fun dumpLabel() = "EditToolView#$contentId(${labelText()})" + + companion object { + fun canRender(tool: Tool) = tool.kind == ToolKind.WRITE + } +} + +/** Picks the multi-file patch body for apply_patch spanning several files, else the single diff. */ +private fun editBody(tool: Tool, selection: SessionSelection?, openFile: SessionFileOpener): EditBody = + if (editFiles(tool).size > 1) PatchBody(selection, openFile) else diffBody(selection) + +private fun popupBody(tool: Tool, selection: SessionSelection?, openFile: SessionFileOpener): EditBody = + if (editFiles(tool).size > 1) PatchBody(selection, openFile, POPUP_OPTS) else popupDiffBody(selection) + +private fun diffBody(selection: SessionSelection?) = ToolMarkdownBody( + MdCodeBlockOptions( + border = MdCodeBlockBorder.Bottom, + maxLines = SessionUiStyle.View.Tool.DIFF_LINES, + verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + editorOnly = true, + ), + selection, + render = ::diffMarkdown, +) + +private fun popupDiffBody(selection: SessionSelection?) = ToolMarkdownBody( + POPUP_OPTS, + selection, + render = ::diffMarkdown, +) + +private val POPUP_OPTS = MdCodeBlockOptions( + border = MdCodeBlockBorder.None, + verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + editorOnly = true, +) + +/** + * Diff body markdown: per-file sections when an apply_patch touched multiple files, otherwise the + * single unified patch, falling back to the tool output/error when no diff is available. + */ +@RequiresEdt +internal fun diffMarkdown(tool: Tool): String { + val files = editFiles(tool) + if (files.count { it.patch.isNotBlank() } > 1) return multiFileDiffMarkdown(files) + val diff = editDiff(tool) + if (diff.isNotBlank()) return patchMarkdown(diff) + val body = plainBody(tool) + if (body.isBlank()) return "" + val fence = fence(body) + return buildString { + append(fence).append('\n') + append(body) + if (!body.endsWith('\n')) append('\n') + append(fence) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/PatchBody.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/PatchBody.kt new file mode 100644 index 00000000000..85cd7f2cc5b --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/PatchBody.kt @@ -0,0 +1,191 @@ +package ai.kilocode.client.session.views.tool + +import ai.kilocode.client.session.SessionFileOpener +import ai.kilocode.client.session.model.Tool +import ai.kilocode.client.session.ui.selection.SessionSelection +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.session.ui.style.SessionUiStyle +import ai.kilocode.client.ui.DiffStatBadge +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.client.ui.md.MdCodeBlockBorder +import ai.kilocode.client.ui.md.MdCodeBlockFactory +import ai.kilocode.client.ui.md.MdCodeBlockOptions +import ai.kilocode.client.ui.md.MdView +import ai.kilocode.client.ui.md.MdViewFactory +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.intellij.ui.EditorTextField +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBUI +import java.awt.Component +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.ScrollPaneConstants + +/** + * Body surface shared by the single-file markdown diff ([ToolMarkdownBody]) and the multi-file + * apply_patch view ([PatchBody]), so [EditToolView] can hold either behind one type and swap between + * them when a streaming tool crosses the single/multi boundary. + */ +interface EditBody { + var parent: Disposable? + + @RequiresEdt fun mount(tool: Tool): JComponent + @RequiresEdt fun created(): Boolean + @RequiresEdt fun panel(): JComponent? + @RequiresEdt fun attached(host: Component): Boolean + @RequiresEdt fun update(tool: Tool): Boolean + @RequiresEdt fun applyStyle(style: SessionEditorStyle): Boolean + @RequiresEdt fun markdown(): String? + @RequiresEdt fun codeEditors(): List + @RequiresEdt fun disposeBody() +} + +/** + * Renders an apply_patch that touched several files as one section per file: a clickable filename + * link (same chrome as the Read/Edit header link) plus a per-file changes badge, left-aligned to the + * diff's own text inset, followed by that file's unified diff. Sections are rebuilt as a group when + * the underlying file set changes, matching the retained-Swing rebuild-on-add/remove convention. + */ +class PatchBody( + private val selection: SessionSelection?, + private val openFile: SessionFileOpener, + private val opts: MdCodeBlockOptions = DIFF_OPTS, +) : EditBody { + override var parent: Disposable? = null + + private var root: Stack? = null + private var owner: Disposable? = null + private val views = mutableListOf() + private val links = mutableListOf() + private var style = SessionEditorStyle.current() + private var signature = "" + + @RequiresEdt + override fun mount(tool: Tool): JComponent { + root?.let { return it } + val panel = Stack.vertical() + root = panel + rebuild(tool) + return panel + } + + @RequiresEdt + override fun created(): Boolean = root != null + + @RequiresEdt + override fun panel(): JComponent? = root + + @RequiresEdt + override fun attached(host: Component): Boolean = root?.parent === host + + @RequiresEdt + override fun update(tool: Tool): Boolean { + if (root == null) return false + if (signatureOf(tool) == signature) return false + rebuild(tool) + return true + } + + @RequiresEdt + override fun applyStyle(style: SessionEditorStyle): Boolean { + this.style = style + var changed = false + views.forEach { changed = applyMd(it) || changed } + links.forEach { if (it.font != style.transcriptFont) { it.font = style.transcriptFont; changed = true } } + return changed + } + + @RequiresEdt + override fun markdown(): String? { + if (views.isEmpty()) return null + return views.joinToString("\n\n") { it.markdown() } + } + + @RequiresEdt + override fun codeEditors(): List = views.flatMap { view -> + (view.component as? JPanel)?.components + ?.filterIsInstance() + ?.mapNotNull { it.viewport.view as? EditorTextField } + ?: emptyList() + } + + @RequiresEdt + override fun disposeBody() { + val panel = root + owner?.let(Disposer::dispose) + owner = null + views.clear() + links.clear() + panel?.removeAll() + signature = "" + } + + @RequiresEdt + private fun rebuild(tool: Tool) { + val panel = root ?: return + val parent = parent ?: error("Patch body has no parent") + disposeBody() + val disposable = Disposer.newDisposable("Patch body") + Disposer.register(parent, disposable) + owner = disposable + editFiles(tool).filter { it.patch.isNotBlank() }.forEachIndexed { index, file -> + if (index > 0) panel.gap(JBUI.scale(SessionUiStyle.View.Code.BLOCK_GAP)) + panel.next(header(file)) + panel.gap(UiStyle.Gap.sm()) + val md = MdViewFactory.create(style, selection, MdCodeBlockFactory.default(opts)) + Disposer.register(disposable, md) + applyMd(md) + md.set(patchMarkdown(file.patch)) + views.add(md) + panel.next(md.component) + } + signature = signatureOf(tool) + panel.revalidate() + panel.repaint() + } + + private fun signatureOf(tool: Tool): String = editFiles(tool) + .joinToString("\u0000") { "${it.path}\u0001${it.additions}\u0001${it.deletions}\u0001${it.patch}" } + + @RequiresEdt + private fun header(file: EditFileChange): JComponent { + val link = FileLinkLabel(openFile).apply { + foreground = UiStyle.Colors.fg() + font = style.transcriptFont + setTarget(file.path, tail(file.path)) + isVisible = true + } + links.add(link) + val row = Stack.horizontal(UiStyle.Gap.sm()) + .next(link) + .next(DiffStatBadge(file.additions, file.deletions)) + return JBUI.Panels.simplePanel(row).apply { + isOpaque = false + border = JBUI.Borders.emptyLeft(SessionUiStyle.View.Code.VIEWPORT_HORIZONTAL_PADDING) + } + } + + private fun applyMd(md: MdView): Boolean { + val before = md.font + md.applyStyle(style) + md.font = style.editorFont + md.foreground = style.editorForeground + md.background = style.editorBackground + md.preBg = style.editorBackground + md.codeFont = style.editorFamily + md.component.border = JBUI.Borders.empty() + return before != md.font + } + + private companion object { + val DIFF_OPTS = MdCodeBlockOptions( + border = MdCodeBlockBorder.Bottom, + maxLines = SessionUiStyle.View.Tool.DIFF_LINES, + verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + editorOnly = true, + ) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ReadToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ReadToolView.kt index 2a205091884..c9b80039d11 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ReadToolView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ReadToolView.kt @@ -96,6 +96,8 @@ class ReadToolView( @RequiresEdt internal fun linkHref() = parts.href @RequiresEdt + internal fun linkTooltip() = parts.link.toolTipText + @RequiresEdt internal fun openLink() = parts.openLink() @RequiresEdt @@ -129,24 +131,9 @@ class ReadToolView( private fun syncSubtitle(): Boolean { val target = target(item)?.takeIf { it.type == "file" } - if (target != null) { - var changed = false - if (parts.href != target.path) { - parts.href = target.path - changed = true - } - changed = setLinkText(parts, tail(target.path).ifBlank { target.path }) || changed - changed = show(parts, true) || changed - return changed - } - - var changed = false - if (parts.href != null) { - parts.href = null - changed = true - } + if (target != null) return setFileTarget(parts, target.path, tail(target.path)) + var changed = setFileTarget(parts, null, "") changed = setText(parts.sub, subtitle(item)) || changed - changed = show(parts, false) || changed return changed } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ShellToolView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ShellToolView.kt index a08b54a8a7c..a14bd9f5262 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ShellToolView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ShellToolView.kt @@ -14,12 +14,11 @@ import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.md.MdCodeBlockBorder import ai.kilocode.client.ui.md.MdCodeBlockFactory import ai.kilocode.client.ui.md.MdCodeBlockOptions +import ai.kilocode.client.ui.md.MdView import ai.kilocode.client.ui.md.MdViewFactory import ai.kilocode.client.ui.md.hybrid.MdTerminal import com.intellij.openapi.actionSystem.DataSink import com.intellij.openapi.actionSystem.UiDataProvider -import com.intellij.openapi.Disposable -import com.intellij.openapi.util.Disposer import com.intellij.ui.EditorTextField import com.intellij.ui.components.JBHtmlPane import com.intellij.ui.components.JBScrollPane @@ -34,8 +33,8 @@ class ShellToolView( tool: Tool, private val selection: SessionSelection? = null, private val parts: ToolParts = toolParts(tool), - private val holder: ShellHolder = ShellHolder(tool, selection), -) : SecondarySessionPartView(parts.header, { holder.body().panel }), UiDataProvider { + private val body: ToolMarkdownBody = shellBody(selection), +) : SecondarySessionPartView(parts.header, { body.mount(tool) }), UiDataProvider { override val contentId: String = tool.id @@ -43,14 +42,14 @@ class ShellToolView( private var style = SessionEditorStyle.current() init { - holder.parent = this + body.parent = this bindHeader(parts.glyph, parts.title, parts.sub, parts.state, parts.center, parts.controls, parts.slot) applyStyle(style) sync() } override fun uiDataSnapshot(sink: DataSink) { - selection?.provideCopy(sink) { holder.shell?.markdown() ?: fallbackText() } + selection?.provideCopy(sink) { body.markdown() ?: fallbackText() } } private fun fallbackText() = ShellContent(item).body @@ -60,7 +59,7 @@ class ShellToolView( val changed = super.expand() if (!changed) return false syncBody() - holder.shell?.applyStyle(style) + body.applyStyle(style) return true } @@ -68,7 +67,7 @@ class ShellToolView( override fun getPreferredSize(): Dimension { val size = super.getPreferredSize() if (!bodyVisible()) return size - val height = row.preferredSize.height + (holder.shell?.panel?.preferredSize?.height ?: 0) + val height = row.preferredSize.height + (body.panel()?.preferredSize?.height ?: 0) return Dimension(size.width, minOf(size.height, height)) } @@ -105,16 +104,16 @@ class ShellToolView( fun hasToggle(): Boolean = arrow.isVisible @RequiresEdt - internal fun bodyCreated() = holder.shell != null + internal fun bodyCreated() = body.created() @RequiresEdt - internal fun bodyVisible() = holder.shell?.panel?.parent === this + internal fun bodyVisible() = body.attached(this) @RequiresEdt - internal fun markdown() = holder.shell?.markdown() ?: ShellContent(item).markdown + internal fun markdown() = body.markdown() ?: ShellContent(item).markdown @RequiresEdt - internal fun codeEditors(): List = holder.shell?.codeEditors() ?: emptyList() + internal fun codeEditors(): List = body.codeEditors() @RequiresEdt internal fun commandFont() = codeEditors().firstOrNull()?.font ?: style.editorFont @@ -138,10 +137,10 @@ class ShellToolView( internal fun controlCount() = if (arrow.isVisible) 1 else 0 @RequiresEdt - internal fun mdComponent() = holder.shell?.mdComponent() + internal fun mdComponent() = body.panel() @RequiresEdt - internal fun horizontalPolicy() = holder.shell?.scrolls()?.firstOrNull()?.horizontalScrollBarPolicy + internal fun horizontalPolicy() = body.scrolls().firstOrNull()?.horizontalScrollBarPolicy ?: ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER @RequiresEdt @@ -161,7 +160,7 @@ class ShellToolView( changed = setFont(parts.sub, style.transcriptFont) || changed changed = setFont(parts.link, style.smallEditorFont) || changed changed = setFont(parts.state, style.smallEditorFont) || changed - holder.shell?.let { changed = it.applyStyle(style) || changed } + changed = body.applyStyle(style) || changed if (changed) refresh() } @@ -181,10 +180,7 @@ class ShellToolView( return changed } - private fun syncBody(): Boolean { - val body = holder.shell ?: return false - return body.update(item) - } + private fun syncBody(): Boolean = body.update(item) @RequiresEdt private fun buildPopupBody(cmd: String): HeaderPopupBody { @@ -208,7 +204,7 @@ class ShellToolView( md.component.border = JBUI.Borders.empty() md.set(popupMd(formatCommand(cmd))) padPopup(md.component) - return HeaderPopupBody(md.component, md, style.editorBackground) + return HeaderPopupBody(md.component, md, style.editorBackground, SessionUiStyle.View.Popup.WIDE_MAX_WIDTH) } override fun dumpLabel() = "ShellToolView#$contentId(${labelText()})" @@ -234,96 +230,28 @@ private fun padPopup(root: JComponent) { private fun grow(size: Dimension, pad: Int) = Dimension(size.width, size.height + pad) -class ShellHolder( - private val tool: Tool, - private val selection: SessionSelection?, -) { - var parent: Disposable? = null - var shell: ShellBody? = null +private fun shellBody(selection: SessionSelection?) = ToolMarkdownBody( + MdCodeBlockOptions( + border = MdCodeBlockBorder.Bottom, + maxLines = 15, + verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + editorOnly = true, + ), + selection, + render = { ShellContent(it).markdown }, + font = SessionEditorStyle::transcriptFont, + chrome = ::styleShellHtml, +) - @RequiresEdt - fun body(): ShellBody { - val current = shell - if (current != null) return current - val owner = parent ?: error("Shell holder has no parent") - return ShellBody(tool, selection, owner).also { - shell = it - Disposer.register(owner, it) - } +/** Pads the left edge of shell section headers ("Command"/"Output") to line up with code text. */ +@RequiresEdt +private fun styleShellHtml(md: MdView) { + val root = md.component as? JPanel ?: return + root.components.filterIsInstance().forEach { + it.border = JBUI.Borders.emptyLeft(SessionUiStyle.View.Code.VIEWPORT_HORIZONTAL_PADDING) } } -class ShellBody( - tool: Tool, - selection: SessionSelection?, - parent: Disposable, -) : Disposable { - private val md = MdViewFactory.create( - SessionEditorStyle.current(), - selection, - MdCodeBlockFactory.default( - MdCodeBlockOptions( - border = MdCodeBlockBorder.Bottom, - maxLines = 15, - verticalPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, - editorOnly = true, - ), - ), - ) - val panel = md.component - - init { - Disposer.register(parent, md) - applyStyle(SessionEditorStyle.current()) - update(tool) - } - - @RequiresEdt - fun update(tool: Tool): Boolean { - val content = ShellContent(tool) - if (md.markdown() == content.markdown) return false - md.set(content.markdown) - styleShell() - return true - } - - @RequiresEdt - fun applyStyle(style: SessionEditorStyle): Boolean { - val before = md.font - md.applyStyle(style) - md.font = style.transcriptFont - md.foreground = style.editorForeground - md.background = style.editorBackground - md.preBg = style.editorBackground - md.codeFont = style.editorFamily - md.component.border = JBUI.Borders.empty() - styleShell() - return before != md.font - } - - @RequiresEdt - private fun styleShell() { - val root = md.component as? JPanel ?: return - root.components.filterIsInstance().forEach { - it.border = JBUI.Borders.emptyLeft(SessionUiStyle.View.Code.VIEWPORT_HORIZONTAL_PADDING) - } - } - - @RequiresEdt - fun markdown() = md.markdown() - - @RequiresEdt - fun mdComponent() = md.component - - @RequiresEdt - fun scrolls(): List = (md.component as? JPanel)?.components?.filterIsInstance() ?: emptyList() - - @RequiresEdt - fun codeEditors(): List = scrolls().mapNotNull { it.viewport.view as? EditorTextField } - - override fun dispose() = Unit -} - private data class ShellContent( val command: String, val output: String, @@ -342,7 +270,7 @@ private data class ShellContent( val body: String = listOf(command, output, error).filter { it.isNotBlank() }.joinToString("\n\n") val markdown: String = buildString { - section(KiloBundle.message("session.part.tool.shell.command"), command, "shell-command") + section(KiloBundle.message("session.part.tool.shell.command"), command, "bash") section(KiloBundle.message("session.part.tool.shell.output"), rawOutput, outputLang(rawOutput)) section(KiloBundle.message("session.part.tool.shell.error"), rawError, "ansi-stderr") } @@ -352,7 +280,7 @@ private fun outputLang(text: String): String = if (MdTerminal.hasAnsi(text)) "an private fun popupMd(text: String): String = buildString { val fence = fence(text) - append(fence).append("shell-command\n") + append(fence).append("bash\n") append(text) if (!text.endsWith('\n')) append('\n') append(fence) @@ -406,9 +334,4 @@ private fun StringBuilder.section(title: String, text: String, lang: String) { append(fence) } -private fun fence(text: String): String { - val size = Regex("`+").findAll(text).maxOfOrNull { it.value.length } ?: 0 - return "`".repeat(maxOf(3, size + 1)) -} - private fun clean(text: String): String = MdTerminal.strip(MdTerminal.reduce(text, keepSgr = false)) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolMarkdownBody.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolMarkdownBody.kt new file mode 100644 index 00000000000..3b5e1830e9d --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolMarkdownBody.kt @@ -0,0 +1,102 @@ +package ai.kilocode.client.session.views.tool + +import ai.kilocode.client.session.model.Tool +import ai.kilocode.client.session.ui.selection.SessionSelection +import ai.kilocode.client.session.ui.style.SessionEditorStyle +import ai.kilocode.client.ui.md.MdCodeBlockFactory +import ai.kilocode.client.ui.md.MdCodeBlockOptions +import ai.kilocode.client.ui.md.MdView +import ai.kilocode.client.ui.md.MdViewFactory +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.intellij.ui.EditorTextField +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBUI +import java.awt.Component +import java.awt.Font +import javax.swing.JComponent +import javax.swing.JPanel + +/** + * A markdown-backed tool body (unified diff, shell transcript, ...) that is built lazily on first + * expansion and then mutated in place. Shared by [ShellToolView] and [EditToolView] so the + * lazy-init, styling, disposal, and editor-lookup logic lives in one place instead of being + * duplicated per tool. + * + * [render] turns the current [Tool] into the markdown to display, [font] picks the body font from + * the active style, and [chrome] applies any per-view tweaks after the markdown is (re)built. + */ +class ToolMarkdownBody( + private val opts: MdCodeBlockOptions, + private val selection: SessionSelection?, + private val render: (Tool) -> String, + private val font: (SessionEditorStyle) -> Font = SessionEditorStyle::editorFont, + private val chrome: (MdView) -> Unit = {}, +) : EditBody { + override var parent: Disposable? = null + private var view: MdView? = null + + /** Builds the body on first call, wiring it into [parent]'s disposable tree, then returns it. */ + @RequiresEdt + override fun mount(tool: Tool): JComponent { + view?.let { return it.component } + val owner = parent ?: error("Tool markdown body has no parent") + val md = MdViewFactory.create(SessionEditorStyle.current(), selection, MdCodeBlockFactory.default(opts)) + Disposer.register(owner, md) + view = md + applyStyle(SessionEditorStyle.current()) + update(tool) + return md.component + } + + @RequiresEdt + override fun created(): Boolean = view != null + + @RequiresEdt + override fun panel(): JComponent? = view?.component + + @RequiresEdt + override fun attached(host: Component): Boolean = view?.component?.parent === host + + @RequiresEdt + override fun update(tool: Tool): Boolean { + val md = view ?: return false + val value = render(tool) + if (md.markdown() == value) return false + md.set(value) + chrome(md) + return true + } + + @RequiresEdt + override fun applyStyle(style: SessionEditorStyle): Boolean { + val md = view ?: return false + val before = md.font + md.applyStyle(style) + md.font = font(style) + md.foreground = style.editorForeground + md.background = style.editorBackground + md.preBg = style.editorBackground + md.codeFont = style.editorFamily + md.component.border = JBUI.Borders.empty() + chrome(md) + return before != md.font + } + + @RequiresEdt + override fun markdown(): String? = view?.markdown() + + @RequiresEdt + fun scrolls(): List = + (view?.component as? JPanel)?.components?.filterIsInstance() ?: emptyList() + + @RequiresEdt + override fun codeEditors(): List = scrolls().mapNotNull { it.viewport.view as? EditorTextField } + + @RequiresEdt + override fun disposeBody() { + view?.let(Disposer::dispose) + view = null + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolSupport.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolSupport.kt index b580de6f9e8..cbc8bf09753 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolSupport.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/views/tool/ToolSupport.kt @@ -6,12 +6,14 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionFileOpener import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState +import ai.kilocode.client.session.model.ToolKind import ai.kilocode.client.session.ui.selection.SessionSelection import ai.kilocode.client.session.ui.selection.SessionCopyTarget import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.views.SessionViewIcons import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.editor.BashCommandHighlighter import ai.kilocode.client.ui.layout.HAlign import ai.kilocode.client.ui.layout.Stack import ai.kilocode.client.ui.layout.VAlign @@ -35,8 +37,14 @@ import com.intellij.ui.components.JBTextArea import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.ui.JBUI import com.intellij.xml.util.XmlStringUtil +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import java.awt.BorderLayout -import java.awt.CardLayout import java.awt.Color import java.awt.Cursor import java.awt.Dimension @@ -58,18 +66,17 @@ class ToolParts( val glyph: JBLabel, val title: JBLabel, val sub: JBLabel, - val link: JBLabel, + val link: FileLinkLabel, val slot: JPanel, val state: JBLabel, val center: JPanel, val controls: JComponent, - private val open: SessionFileOpener? = null, val extra: JBLabel? = null, val targets: List = emptyList(), private val mode: ToolBodyMode = ToolBodyMode.EDITOR, ) { - var href: String? = null - var label: String = "" + val href: String? get() = link.href + val label: String get() = link.label private var body: ToolBody? = null val text: JBTextArea? @@ -92,8 +99,7 @@ class ToolParts( @RequiresEdt fun openLink(anchor: RelativePoint? = null) { - val value = href ?: return - open?.invoke(value, anchor) + link.openLink(anchor) } @RequiresEdt @@ -108,6 +114,52 @@ class ToolParts( } } +class FileLinkLabel( + private val open: SessionFileOpener? = null, +) : JBLabel() { + var href: String? = null + private set + var label: String = "" + private set + + init { + isVisible = false + isFocusable = false + foreground = UiStyle.Colors.fg() + cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) + setRequestFocusEnabled(false) + addMouseListener(object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + openLink(RelativePoint(this@FileLinkLabel, Point(width / 2, height))) + } + }) + } + + @RequiresEdt + fun setTarget(path: String?, text: String): Boolean { + val next = single(text.ifBlank { path.orEmpty() }) + val value = if (next.isBlank()) "" else XmlStringUtil.wrapInHtml("${XmlStringUtil.escapeString(next)}") + var changed = false + if (href != path) { + href = path + toolTipText = path + changed = true + } + if (label != next || this.text != value) { + label = next + this.text = value + changed = true + } + return changed + } + + @RequiresEdt + fun openLink(anchor: RelativePoint? = null) { + val value = href ?: return + open?.invoke(value, anchor) + } +} + class ToolBody private constructor( val area: JBTextArea?, val ed: EditorTextField?, @@ -122,6 +174,7 @@ class ToolBody private constructor( if (text == value) return area?.text = value ed?.text = value + (ed as? ToolField)?.syncHighlight() caretStart() size() } @@ -162,6 +215,7 @@ class ToolBody private constructor( area?.font = style.transcriptFont ed?.font = style.editorFont ed?.getEditor(false)?.let(style::applyToEditor) + (ed as? ToolField)?.syncHighlight() size() return before != font } @@ -222,7 +276,7 @@ class ToolBody private constructor( fun editor(tool: Tool): ToolBody { val disposable = Disposer.newDisposable("Tool body") val body = runCatching { - val field = ToolField(preview(tool), SessionEditorStyle.current()).also { ed -> + val field = ToolField(preview(tool), SessionEditorStyle.current(), tool.name == "bash").also { ed -> Disposer.register(disposable) { ed.getEditor(false)?.let(EditorFactory.getInstance()::releaseEditor) } @@ -296,7 +350,7 @@ private class ToolArea : JBTextArea(), UiDataProvider, SessionCopyTarget { } } -private class ToolField(value: String, private var style: SessionEditorStyle) : EditorTextField( +private class ToolField(value: String, private var style: SessionEditorStyle, private val bash: Boolean) : EditorTextField( EditorFactory.getInstance().createDocument(value.trimEnd('\n')), ProjectManager.getInstance().defaultProject, PlainTextFileType.INSTANCE, @@ -323,45 +377,39 @@ private class ToolField(value: String, private var style: SessionEditorStyle) : ed.settings.isAdditionalPageAtBottom = false ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ed.scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER + syncHighlight(ed) } } + fun syncHighlight() { + getEditor(false)?.let(::syncHighlight) + } + + private fun syncHighlight(ed: com.intellij.openapi.editor.ex.EditorEx) { + if (!bash) return + BashCommandHighlighter.apply(ed, text) + } + override fun uiDataSnapshot(sink: DataSink) { super.uiDataSnapshot(sink) selection?.provideCopy(sink) { copyText() } } } -private const val SUB_CARD = "sub" -private const val LINK_CARD = "link" - @RequiresEdt internal fun toolParts( tool: Tool, openFile: SessionFileOpener? = null, mode: ToolBodyMode = ToolBodyMode.TEXT, ): ToolParts { - lateinit var parts: ToolParts val glyph = JBLabel() val title = clip(JBLabel()) val sub = clip(JBLabel()).apply { foreground = UiStyle.Colors.weak() } - val link = clip(JBLabel()).apply { - isVisible = false - isFocusable = false - foreground = UiStyle.Colors.fg() - cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) - setRequestFocusEnabled(false) - addMouseListener(object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent) { - parts.openLink(RelativePoint(this@apply, Point(width / 2, 0))) - } - }) - } - val slot = JPanel(CardLayout()).apply { - isOpaque = false + val link = clip(FileLinkLabel(openFile)) + val slot = Stack.fitHorizontal().apply { minimumSize = Dimension(0, minimumSize.height) - add(sub, SUB_CARD) - add(link, LINK_CARD) + next(sub) + next(link) } val state = clip(JBLabel()).apply { foreground = UiStyle.Colors.weak() } val center = JPanel(BorderLayout(UiStyle.Gap.md(), 0)).apply { @@ -377,7 +425,7 @@ internal fun toolParts( add(center, BorderLayout.CENTER) add(controls, BorderLayout.EAST) } - parts = ToolParts(header, glyph, title, sub, link, slot, state, center, controls, openFile, mode = mode) + val parts = ToolParts(header, glyph, title, sub, link, slot, state, center, controls, mode = mode) return parts.also { controls.add(it.state) } @@ -393,12 +441,11 @@ internal fun searchParts(count: Int): ToolParts { foreground = UiStyle.Colors.fg() } } - val link = clip(JBLabel()).apply { isVisible = false } - val slot = JPanel(CardLayout()).apply { - isOpaque = false + val link = clip(FileLinkLabel()) + val slot = Stack.fitHorizontal().apply { minimumSize = Dimension(0, minimumSize.height) - add(sub, SUB_CARD) - add(link, LINK_CARD) + next(sub) + next(link) } val state = clip(JBLabel()).apply { foreground = UiStyle.Colors.weak() } val stack = Stack.fitHorizontal(UiStyle.Gap.md()).apply { targets.forEach { next(it) } } @@ -436,9 +483,10 @@ internal fun icon(tool: Tool) = when (tool.name) { else -> SessionViewIcons.mcp } -internal fun title(tool: Tool) = when (tool.name) { - "read" -> KiloBundle.message("session.part.tool.read") - "bash" -> KiloBundle.message("session.part.tool.shell") +internal fun title(tool: Tool) = when { + tool.name == "read" -> KiloBundle.message("session.part.tool.read") + tool.name == "bash" -> KiloBundle.message("session.part.tool.shell") + tool.kind == ToolKind.WRITE -> KiloBundle.message("session.part.tool.edit") else -> toolTitle(tool) } @@ -464,17 +512,18 @@ internal fun setTargetText(label: JBLabel, text: String): Boolean { return true } +/** + * Shows [path] as a clickable file link in the header slot, or clears the link when [path] is null. + * Shared by [ai.kilocode.client.session.views.tool.ReadToolView] and + * [ai.kilocode.client.session.views.tool.EditToolView] so both render file targets identically. + */ @RequiresEdt -internal fun setLinkText(parts: ToolParts, text: String): Boolean { - val label = single(text) - val value = if (label.isBlank()) "" else XmlStringUtil.wrapInHtml("${XmlStringUtil.escapeString(label)}") - if (parts.label == label && parts.link.text == value) return false - parts.label = label - parts.link.text = value - return true +internal fun setFileTarget(parts: ToolParts, path: String?, label: String): Boolean { + val changed = parts.link.setTarget(path, label) + return show(parts, path != null) || changed } -private fun clip(label: JBLabel): JBLabel = label.apply { +private fun clip(label: T): T = label.apply { minimumSize = Dimension(0, minimumSize.height) } @@ -491,9 +540,10 @@ private fun single(text: String): String = text.lineSequence() @RequiresEdt internal fun show(parts: ToolParts, link: Boolean): Boolean { - if (parts.link.isVisible == link && parts.sub.isVisible != link) return false - (parts.slot.layout as CardLayout).show(parts.slot, if (link) LINK_CARD else SUB_CARD) - return true + var changed = false + changed = setVisible(parts.link, link) || changed + changed = setVisible(parts.sub, !link) || changed + return changed } internal fun subtitleText(parts: ToolParts): String = if (parts.link.isVisible) parts.label else parts.sub.text @@ -691,6 +741,161 @@ private fun toolSubtitle(tool: Tool): String { return listOfNotNull(base).plus(args).joinToString(" ") } +/** File path targeted by a write tool, preferring the most specific resolvable path. */ +internal fun editPath(tool: Tool): String = editPaths(tool).maxWithOrNull( + compareBy({ OSAgnosticPathUtil.isAbsolute(it) }, { depth(it) }), +) ?: tool.name + +private fun editPaths(tool: Tool): List { + val direct = listOf(tool.input["filePath"], tool.input["path"]) + val diff = listOfNotNull(editFile(parseJsonObject(tool.metadata["filediff"]))) + val files = parseJsonArray(tool.metadata["files"])?.mapNotNull { editFile(it.jsonObject) } ?: emptyList() + return (direct + diff + files + listOf(tool.title, tool.name)) + .mapNotNull { it?.takeIf { value -> value.isNotBlank() } } +} + +private fun editFile(obj: JsonObject?): String? = listOf("filePath", "path", "file", "relativePath") + .firstNotNullOfOrNull { key -> obj?.get(key)?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() } } + +private fun depth(path: String): Int = path.count { it == '/' || it == '\\' } + +private val DIFF_JSON = Json { ignoreUnknownKeys = true; isLenient = true } + +private fun parseJsonObject(raw: String?): JsonObject? = + raw?.takeIf { it.isNotBlank() }?.let { runCatching { DIFF_JSON.parseToJsonElement(it).jsonObject }.getOrNull() } + +private fun parseJsonArray(raw: String?): JsonArray? = + raw?.takeIf { it.isNotBlank() }?.let { runCatching { DIFF_JSON.parseToJsonElement(it) as? JsonArray }.getOrNull() } + +private fun patchOf(obj: JsonObject?): String? = + obj?.get("patch")?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() } + +/** + * Unified diff patch produced by a write tool, or empty when none is available. Kilo strips the raw + * `diff` field from stored parts (see stripPartMetadata) but keeps `filediff.patch` (edit/write) and + * per-file `files[].patch` (apply_patch) when under the size cap, so read those first. + */ +internal fun editDiff(tool: Tool): String { + tool.metadata["diff"]?.takeIf { it.isNotBlank() }?.let { return it } + patchOf(parseJsonObject(tool.metadata["filediff"]))?.let { return it } + parseJsonArray(tool.metadata["files"])?.let { files -> + val joined = files.mapNotNull { patchOf(it.jsonObject) }.joinToString("\n") + if (joined.isNotBlank()) return joined + } + return "" +} + +/** One file touched by an apply_patch call, parsed from the tool's `files[]` metadata. */ +internal data class EditFileChange( + val path: String, + val type: String, + val additions: Int, + val deletions: Int, + val patch: String, +) + +/** Per-file changes from an apply_patch tool; empty for single-file edit/write tools (`filediff`). */ +internal fun editFiles(tool: Tool): List = + parseJsonArray(tool.metadata["files"])?.mapNotNull { element -> + val obj = element.jsonObject + val path = editFile(obj) ?: return@mapNotNull null + EditFileChange( + path = path, + type = obj["type"]?.jsonPrimitive?.contentOrNull.orEmpty(), + additions = obj["additions"]?.jsonPrimitive?.intOrNull ?: 0, + deletions = obj["deletions"]?.jsonPrimitive?.intOrNull ?: 0, + patch = patchOf(obj).orEmpty(), + ) + } ?: emptyList() + +/** + * Sectioned markdown for a multi-file patch: each file gets a labeled header line (path plus its own + * add/remove counts) followed by its own fenced diff, so the joined apply_patch diff no longer runs + * together into one indistinguishable block. The path is wrapped in inline code so characters like + * underscores are not parsed as markdown emphasis. + */ +internal fun multiFileDiffMarkdown(files: List): String = + files.filter { it.patch.isNotBlank() }.joinToString("\n\n") { file -> + buildString { + append('`').append(tail(file.path)).append('`') + append(" +").append(file.additions).append(" -").append(file.deletions) + append("\n\n") + append(patchMarkdown(file.patch)) + } + } + +/** Added/removed line counts, preferring the counts computed by the CLI, else counting patch lines. */ +internal fun diffStat(tool: Tool): Pair { + parseJsonObject(tool.metadata["filediff"])?.let { fd -> + val add = fd["additions"]?.jsonPrimitive?.intOrNull + val del = fd["deletions"]?.jsonPrimitive?.intOrNull + if (add != null || del != null) return (add ?: 0) to (del ?: 0) + } + parseJsonArray(tool.metadata["files"])?.let { files -> + var add = 0 + var del = 0 + var found = false + files.forEach { + it.jsonObject["additions"]?.jsonPrimitive?.intOrNull?.let { v -> add += v; found = true } + it.jsonObject["deletions"]?.jsonPrimitive?.intOrNull?.let { v -> del += v; found = true } + } + if (found) return add to del + } + val patch = editDiff(tool) + if (patch.isBlank()) return 0 to 0 + var added = 0 + var removed = 0 + for (line in patch.lineSequence()) { + when { + line.startsWith("+++") || line.startsWith("---") -> Unit + line.startsWith("+") -> added++ + line.startsWith("-") -> removed++ + } + } + return added to removed +} + +/** Display-only diff body without VCS/file metadata headers (Index, diff --git, ---, +++, etc.). */ +internal fun pureDiff(diff: String): String = diff.lineSequence() + .filterNot(::diffMeta) + .joinToString("\n") + .trim('\n') + +private fun diffMeta(line: String): Boolean = line.startsWith("Index:") || + line.startsWith("====") || + line.startsWith("diff --git ") || + line.startsWith("@@") || + line.startsWith("index ") || + line.startsWith("--- ") || + line.startsWith("+++ ") || + line.startsWith("new file mode ") || + line.startsWith("deleted file mode ") || + line.startsWith("old mode ") || + line.startsWith("new mode ") || + line.startsWith("similarity index ") || + line.startsWith("dissimilarity index ") || + line.startsWith("rename from ") || + line.startsWith("rename to ") || + line.startsWith("copy from ") || + line.startsWith("copy to ") + +/** Wraps a unified patch in a fenced `patch` block so the markdown code editor highlights it. */ +internal fun patchMarkdown(diff: String): String = buildString { + // Fall back to the raw patch when stripping metadata leaves nothing (e.g. a pure rename or + // mode-only change with no +/-/context lines) so we never render an empty fenced block. + val body = pureDiff(diff).ifBlank { diff.trim('\n') } + val fence = fence(body) + append(fence).append("patch-pure\n") + append(body) + if (!body.endsWith('\n')) append('\n') + append(fence) +} + +internal fun fence(text: String): String { + val size = Regex("`+").findAll(text).maxOfOrNull { it.value.length } ?: 0 + return "`".repeat(maxOf(3, size + 1)) +} + internal fun tail(path: String): String { val value = path.trimEnd('/', '\\') val index = maxOf(value.lastIndexOf('/'), value.lastIndexOf('\\')) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurable.kt index e94db1b366e..5197413d311 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurable.kt @@ -2,6 +2,7 @@ package ai.kilocode.client.settings import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable +import ai.kilocode.client.settings.autoapprove.AutoApproveConfigurable import ai.kilocode.client.settings.context.ContextConfigurable import ai.kilocode.client.settings.models.ModelsConfigurable import ai.kilocode.client.settings.providers.ProvidersConfigurable @@ -74,6 +75,14 @@ class KiloSettingsConfigurable : SearchableConfigurable { behavior.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm()) panel.next(behavior) + val autoApprove = ActionLink(KiloBundle.message("settings.autoApprove.displayName")) { e -> + val src = e.source as? JComponent ?: return@ActionLink + val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink + open(settings, AutoApproveConfigurable.ID) + } + autoApprove.border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm()) + panel.next(autoApprove) + val context = ActionLink(KiloBundle.message("settings.context.displayName")) { e -> val src = e.source as? JComponent ?: return@ActionLink val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(src)) ?: return@ActionLink diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurable.kt index 800f9e26a83..e70fc418447 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurable.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.settings.agents import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.rules.RulesConfigurable import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.layout.Stack import com.intellij.ide.DataManager @@ -25,6 +26,8 @@ class AgentBehaviorConfigurable : SearchableConfigurable { listOf( KiloBundle.message("settings.agentBehavior.agents.displayName") to AgentsConfigurable.ID, KiloBundle.message("settings.agentBehavior.mcp.displayName") to McpConfigurable.ID, + KiloBundle.message("settings.agentBehavior.skills.displayName") to SkillsConfigurable.ID, + KiloBundle.message("settings.agentBehavior.rules.displayName") to RulesConfigurable.ID, ).forEach { (label, id) -> panel.next(ActionLink(label) { e -> val src = e.source as? JComponent ?: return@ActionLink diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt new file mode 100644 index 00000000000..5d58e207556 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/agents/SkillsConfigurable.kt @@ -0,0 +1,501 @@ +package ai.kilocode.client.settings.agents + +import ai.kilocode.client.app.KiloAgentBehaviorService +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.KiloNotifications +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.base.SettingsBadge +import ai.kilocode.client.settings.base.SettingsDraftPage +import ai.kilocode.client.settings.base.SettingsDraftState +import ai.kilocode.client.settings.base.SettingsListCell +import ai.kilocode.client.settings.base.SettingsListConfig +import ai.kilocode.client.settings.base.SettingsListItem +import ai.kilocode.client.settings.base.SettingsListPanel +import ai.kilocode.client.settings.base.SettingsListSelection +import ai.kilocode.client.settings.base.SettingsListView +import ai.kilocode.client.settings.base.SettingsContentField +import ai.kilocode.client.settings.base.SettingsMessageException +import ai.kilocode.client.settings.base.SettingsPathDialog +import ai.kilocode.client.settings.base.SettingsPathDialogHandle +import ai.kilocode.client.settings.base.settingsChoosePath +import ai.kilocode.client.settings.base.settingsContentScroll +import ai.kilocode.client.settings.base.settingsEditorFileType +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.log.KiloLog +import ai.kilocode.rpc.dto.ConfigPatchDto +import ai.kilocode.rpc.dto.SkillsConfigDto +import ai.kilocode.rpc.dto.SkillsPatchDto +import ai.kilocode.rpc.dto.SkillDto +import com.intellij.CommonBundle +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.ui.TitledSeparator +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.JBUI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import java.awt.BorderLayout +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.ScrollPaneConstants +import javax.swing.ListSelectionModel + +private val edt = Dispatchers.EDT + ModalityState.any().asContextElement() + +class SkillsConfigurable : AgentBehaviorConfigurableBase() { + override fun getId(): String = ID + override fun getDisplayName(): String = KiloBundle.message("settings.agentBehavior.skills.displayName") + override fun create(cs: CoroutineScope, dir: String): JComponent = SkillsSettingsUi(cs, dir) + override fun update(ui: JComponent, dir: String) { + (ui as? SkillsSettingsUi)?.setDirectory(dir) + } + override fun scrollReadyShell() = false + + companion object { const val ID = "ai.kilocode.jetbrains.settings.agentBehavior.skills" } +} + +internal class SkillsSettingsUi( + scope: CoroutineScope, + dir: String, + private val choose: (JComponent) -> String? = ::chooseSkillPath, + private val source: (Boolean, Boolean, String) -> SettingsPathDialogHandle = { adding, path, value -> + SettingsPathDialog(sourceDialogTitle(adding, path), value, if (path) choose else null) + }, + private val edit: (SkillDto, Boolean) -> SkillEditDialogHandle = ::SkillEditDialog, +) : SettingsListPanel(scope, SettingsListConfig.Equal.copy(tooltip = false)), SettingsDraftPage { + private val cs = scope + private var dir = dir + private var skills = emptyMap() + private val app get() = service() + private val state = SettingsDraftState(skillsDraft(app.state.value.config?.skills ?: SkillsConfigDto()), ::saved) + private var draft: SkillsDraft + get() = state.draft + set(value) { + state.draft = value + } + internal val sources = SkillSourcesView(this, source) + + init { + start() + setCenter(skillScroll()) + content.add(sources, BorderLayout.SOUTH) + } + + fun setDirectory(value: String) { + if (value == dir) return + dir = value + reload() + } + + override suspend fun fetch(): List { + val items = withTimeoutOrNull(SKILL_LOAD_TIMEOUT_MS) { + service().loadSkills(dir) + } ?: throw SettingsMessageException(KiloBundle.message("settings.agentBehavior.skills.load.timeout")) + withContext(edt) { + val dirty = state.modified() + val edit = draft + state.accept(skillsDraft(config())) + if (dirty) draft = state.draft.copy(edited = edit.edited, deleted = edit.deleted) + skills = items.associateBy { key(it) } + sources.refresh(draft.sources) + } + LOG.info("skills settings fetch dir=$dir total=${items.size}") + return rows(items) + } + + override fun afterApply() { + sources.refresh(draft.sources) + } + + override fun onCell(key: String, cellId: String) { + val skill = skills[key] ?: return + when (cellId) { + OPEN_CELL -> open(skill) + EDIT_CELL -> edit(skill) + DELETE_CELL -> remove(skill) + } + } + + override fun searchPlaceholder() = KiloBundle.message("settings.agentBehavior.skills.search") + + override fun emptyText() = KiloBundle.message("settings.agentBehavior.skills.empty") + + internal fun updateSources(paths: List, urls: List) { + state.update { copy(sources = SkillsConfigDto(paths = paths, urls = urls)) } + sources.refresh(draft.sources) + } + + override fun modified(): Boolean = state.modified() + + override fun resetDraft() { + state.reset() + sources.refresh(draft.sources) + view.update(rows()) + clearProgress() + } + + override fun applyDraft() { + val token = state.start() ?: return + val fallback = skillFallback(token.target) + if (!launch("apply") { id -> + val target = token.target + var failed: String? = null + val behavior = service() + LOG.info("skills settings apply start dir=$dir edited=${target.edited.size} deleted=${target.deleted.size} paths=${target.sources.paths.size} urls=${target.sources.urls.size}") + if (target.edited.isNotEmpty() && !behavior.saveSkills(dir, target.edited)) { + failed = KiloBundle.message("settings.agentBehavior.save.failed") + } + if (failed == null) { + for (location in target.deleted) { + if (!behavior.removeSkill(dir, location)) { + failed = KiloBundle.message("settings.agentBehavior.skills.delete.failed") + break + } + } + } + if (failed == null && target.sources != token.previous.sources) { + val patch = ConfigPatchDto(skills = SkillsPatchDto(paths = target.sources.paths, urls = target.sources.urls)) + if (app.updateConfig(patch) == null) failed = KiloBundle.message("settings.agentBehavior.save.failed") + } + val reloaded = if (failed == null) behavior.reloadSkills(dir) else true + val items = behavior.refreshSkills(dir, fallback) + withContext(edt) { + if (!active(id)) { + if (failed == null) KiloNotifications.info(KiloBundle.message("settings.agentBehavior.skills.saved.notification")) + else KiloNotifications.error(failed) + return@withContext + } + if (failed == null) { + skills = items.associateBy { key(it) } + val next = skillsDraft(config()) + state.complete(token, next) + sources.refresh(draft.sources) + view.update(rows(items)) + if (reloaded) clearProgress() else showProgress(KiloBundle.message("settings.agentBehavior.skills.reload.blocked")) + LOG.info("skills settings apply succeeded dir=$dir") + } else { + state.fail(token, failed) + sources.refresh(draft.sources) + view.update(rows(items)) + showError(failed) + LOG.warn("skills settings apply failed dir=$dir message=$failed") + } + setBusy(false) + } + }) return + showProgress(KiloBundle.message("settings.agentBehavior.saving")) + } + + private fun skillScroll() = JBScrollPane(view).apply { + border = null + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED + } + + private fun rows(items: List = skills.values.toList()): List = items.mapNotNull { skill -> + if (skill.location in draft.deleted) return@mapNotNull null + item(skill) + } + + private fun skillFallback(target: SkillsDraft): List = skills.values.mapNotNull { skill -> + if (skill.location in target.deleted) return@mapNotNull null + target.edited[skill.location]?.let { skill.copy(content = it) } ?: skill + } + + private fun item(skill: SkillDto) = object : SettingsListItem { + override val key = key(skill) + override val title = skill.name + override val note = skill.location.takeUnless { builtin(it) } + override val description = skill.description + override val doubleClick = EDIT_CELL + override val badges = listOf( + SettingsBadge(KiloBundle.message("settings.agentBehavior.badge.builtin"), UiStyle.Badge.Secondary), + ).takeIf { builtin(skill.location) } ?: emptyList() + override val cells = listOfNotNull( + SettingsListCell( + OPEN_CELL, + KiloBundle.message("settings.agentBehavior.skills.openInEditor"), + primary = true, + ).takeIf { skill.editable }, + SettingsListCell( + EDIT_CELL, + KiloBundle.message(if (skill.editable) "settings.agentBehavior.edit" else "common.open"), + primary = !skill.editable, + ), + SettingsListCell( + DELETE_CELL, + KiloBundle.message("common.delete"), + icon = AllIcons.Actions.GC, + iconOnly = true, + ).takeIf { skill.editable }, + ) + } + + private fun edit(skill: SkillDto) { + val current = skill.copy(content = content(skill)) + val dialog = edit(current, skill.editable) + if (!skill.editable) { + dialog.showAndGet() + return + } + if (!dialog.showAndGet()) return + state.update { copy(edited = edited + (skill.location to dialog.content())) } + view.update(rows(), SettingsListSelection.Key(key(skill))) + } + + private fun open(skill: SkillDto) { + if (!skill.editable) return + showProgress(KiloBundle.message("settings.agentBehavior.skills.openInEditor.pending")) + cs.launch { + val opened = service().openFile(skill.location) + if (opened) return@launch + withContext(edt) { KiloNotifications.error(KiloBundle.message("settings.agentBehavior.skills.openInEditor.failed")) } + } + } + + private fun remove(skill: SkillDto) { + val result = Messages.showYesNoDialog( + KiloBundle.message("settings.agentBehavior.skills.delete.message", skill.name), + KiloBundle.message("settings.agentBehavior.skills.delete.title"), + KiloBundle.message("common.delete"), + Messages.getCancelButton(), + Messages.getQuestionIcon(), + ) + if (result != Messages.YES) return + state.update { copy(deleted = deleted + skill.location, edited = edited - skill.location) } + view.update(rows(), selectionIndex()) + } + + private fun content(skill: SkillDto) = draft.edited[skill.location] ?: skill.content + + private fun config() = app.state.value.config?.skills ?: SkillsConfigDto() + + private companion object { + const val EDIT_CELL = "edit" + const val OPEN_CELL = "open" + const val DELETE_CELL = "delete" + const val BUILTIN = "builtin" + const val LEGACY_BUILTIN = "" + val LOG = KiloLog.create(SkillsSettingsUi::class.java) + + fun key(skill: SkillDto) = skill.location.ifBlank { skill.name } + fun builtin(location: String) = location == BUILTIN || location == LEGACY_BUILTIN + } +} + +internal interface SkillEditDialogHandle { + fun showAndGet(): Boolean + fun content(): String +} + +private data class SkillsDraft( + val sources: SkillsConfigDto, + val edited: Map = emptyMap(), + val deleted: Set = emptySet(), +) + +private fun skillsDraft(sources: SkillsConfigDto) = SkillsDraft(sources) + +private fun saved(base: SkillsDraft, draft: SkillsDraft): Boolean = base == draft + +internal class SkillEditDialog(private val skill: SkillDto, private val savable: Boolean) : DialogWrapper(true), SkillEditDialogHandle { + private val base = initial() + private val editor = SettingsContentField(base, skillFileType(skill.location, base), savable) + + init { + title = skill.name + setOKButtonText(CommonBundle.getOkButtonText()) + setCancelButtonText(CommonBundle.getCloseButtonText()) + init() + isOKActionEnabled = false + editor.document.addDocumentListener(object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + isOKActionEnabled = savable && editor.text != base + } + }) + } + + override fun createCenterPanel(): JComponent = settingsContentScroll(editor) + + override fun createActions() = if (savable) arrayOf(okAction, cancelAction) else arrayOf(cancelAction) + + override fun content() = editor.text + + private fun initial() = skill.content?.takeIf { it.isNotBlank() } + ?: skill.description?.takeIf { it.isNotBlank() } + ?: KiloBundle.message("settings.agentBehavior.skills.content.empty") +} + +internal class SkillSourcesView( + private val parent: SkillsSettingsUi, + private val source: (Boolean, Boolean, String) -> SettingsPathDialogHandle, +) : Stack(ai.kilocode.client.ui.layout.StackAxis.VERTICAL, UiStyle.Gap.sm()) { + private val view = SettingsListView( + KiloBundle.message("settings.agentBehavior.skills.sources.empty"), + SettingsListConfig.Preferred.copy(description = false, selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION), + ) { key, id -> + if (id == EDIT_CELL) edit(key) + } + private var cfg = SkillsConfigDto() + + internal fun sourceList() = view.list + + init { + border = JBUI.Borders.empty(UiStyle.Gap.pad(), 0, 0, 0) + next(TitledSeparator(KiloBundle.message("settings.agentBehavior.skills.sources.title"))) + next(toolbar()) + next(JBScrollPane(view).apply { + border = null + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + preferredSize = JBUI.size(0, JBUI.scale(160)) + maximumSize = JBUI.size(Int.MAX_VALUE, JBUI.scale(160)) + }) + } + + fun refresh(config: SkillsConfigDto) { + cfg = config + view.update(rows(config)) + } + + private fun toolbar(): JComponent { + val add = DefaultActionGroup(KiloBundle.message("settings.agentBehavior.skills.sources.add"), true).apply { + templatePresentation.icon = AllIcons.General.Add + add(AddPathAction()) + add(AddUrlAction()) + } + val group = DefaultActionGroup(add, RemoveAction()) + val toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, group, true) + toolbar.targetComponent = this + toolbar.updateActionsImmediately() + return toolbar.component + } + + internal fun addPath() { + val dialog = source(true, true, "") + if (!dialog.showAndGet()) return + val path = dialog.value().trim().takeIf { it.isNotBlank() } ?: return + if (path in cfg.paths) return + parent.updateSources(cfg.paths + path, cfg.urls) + } + + internal fun addUrl() { + val dialog = source(true, false, "") + if (!dialog.showAndGet()) return + val url = dialog.value().trim().takeIf { it.isNotBlank() } ?: return + if (url in cfg.urls) return + parent.updateSources(cfg.paths, cfg.urls + url) + } + + private fun rows(config: SkillsConfigDto): List { + val paths = config.paths.map { source(PATH_PREFIX, it) } + val urls = config.urls.map { source(URL_PREFIX, it) } + return paths + urls + } + + private fun source(prefix: String, value: String) = object : SettingsListItem { + override val key = prefix + value + override val title = value + override val doubleClick = EDIT_CELL + } + + internal fun removeSelected() { + val keys = view.selectedItems().map { it.key }.toSet() + if (keys.isEmpty()) return + val paths = cfg.paths.filterNot { PATH_PREFIX + it in keys } + val urls = cfg.urls.filterNot { URL_PREFIX + it in keys } + parent.updateSources(paths, urls) + } + + private fun edit(key: String) { + val path = key.startsWith(PATH_PREFIX) + val old = key.removePrefix(if (path) PATH_PREFIX else URL_PREFIX) + val dialog = source(false, path, old) + if (!dialog.showAndGet()) return + val next = dialog.value().trim().takeIf { it.isNotBlank() } ?: return + if (path) { + parent.updateSources(cfg.paths.map { if (it == old) next else it }.distinct(), cfg.urls) + return + } + parent.updateSources(cfg.paths, cfg.urls.map { if (it == old) next else it }.distinct()) + } + + private inner class AddPathAction : DumbAwareAction( + KiloBundle.message("settings.agentBehavior.skills.sources.addPath"), + null, + null, + ) { + override fun getActionUpdateThread() = ActionUpdateThread.EDT + override fun actionPerformed(e: AnActionEvent) = addPath() + } + + private inner class AddUrlAction : DumbAwareAction( + KiloBundle.message("settings.agentBehavior.skills.sources.addUrl"), + null, + null, + ) { + override fun getActionUpdateThread() = ActionUpdateThread.EDT + override fun actionPerformed(e: AnActionEvent) = addUrl() + } + + private inner class RemoveAction : DumbAwareAction( + KiloBundle.message("common.delete"), + null, + AllIcons.General.Remove, + ) { + override fun getActionUpdateThread() = ActionUpdateThread.EDT + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = view.selectedItems().isNotEmpty() + } + override fun actionPerformed(e: AnActionEvent) = removeSelected() + } + + private companion object { + const val EDIT_CELL = "edit" + const val PATH_PREFIX = "path:" + const val URL_PREFIX = "url:" + } +} + +private fun sourceDialogTitle(adding: Boolean, path: Boolean): String = KiloBundle.message( + when { + adding && path -> "settings.agentBehavior.skills.sources.addPath.title" + adding -> "settings.agentBehavior.skills.sources.addUrl.title" + path -> "settings.agentBehavior.skills.sources.editPath.title" + else -> "settings.agentBehavior.skills.sources.editUrl.title" + }, +) + +private fun chooseSkillPath(parent: JComponent): String? { + return settingsChoosePath(parent, skillPathDescriptor()) +} + +internal fun skillPathDescriptor() = FileChooserDescriptor(false, true, false, false, false, false).apply { + title = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.title") + description = KiloBundle.message("settings.agentBehavior.skills.sources.addPath.prompt") +} + +internal fun skillFileType(location: String, content: String? = null): FileType = + settingsEditorFileType(location.ifBlank { SKILL_FILE }, content) + +private const val SKILL_FILE = "SKILL.md" +private const val SKILL_LOAD_TIMEOUT_MS = 10_000L diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveConfigurable.kt new file mode 100644 index 00000000000..4ccd9ef31d7 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveConfigurable.kt @@ -0,0 +1,22 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.base.DraftReadyConfigurable +import kotlinx.coroutines.CoroutineScope +import javax.swing.JComponent + +class AutoApproveConfigurable : DraftReadyConfigurable() { + override fun getId(): String = ID + + override fun getDisplayName(): String = KiloBundle.message("settings.autoApprove.displayName") + + // The page renders its own fixed search field plus a scrollable body, so the shell must not + // add another scroll pane around it. + override fun scrollReadyShell(): Boolean = false + + override fun create(cs: CoroutineScope): JComponent = AutoApproveSettingsUi(cs) + + companion object { + const val ID = "ai.kilocode.jetbrains.settings.autoApprove" + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveContent.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveContent.kt new file mode 100644 index 00000000000..a444347f25c --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveContent.kt @@ -0,0 +1,140 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.base.BaseContentPanel +import com.intellij.util.concurrency.annotations.RequiresEdt + +private enum class ExceptionKind { PATH, COMMAND } + +private val GRANULAR_TOOLS = listOf( + "external_directory" to ExceptionKind.PATH, + "bash" to ExceptionKind.COMMAND, + "read" to ExceptionKind.PATH, + "edit" to ExceptionKind.PATH, +) + +private val SIMPLE_TOOLS = listOf("glob", "grep", "list", "task", "skill", "lsp") +private val GROUPED_IDS = listOf("todoread", "todowrite") +private val TRAILING_TOOLS = listOf("websearch", "webfetch", "doom_loop") + +/** + * Full Auto-Approve page layout: four granular tool sections, then a section covering the + * remaining simple/grouped/trailing tools, in the exact order used by VS Code's + * `PermissionEditor.tsx`. + */ +internal class AutoApproveContent( + private val update: (PermissionDraft.() -> PermissionDraft) -> Unit, + private val picker: LevelPicker = PopupLevelPicker, +) : BaseContentPanel() { + private var selected: Selection? = null + private val granular = GRANULAR_TOOLS.map { (id, kind) -> id to granularSection(id, kind) } + private val tools = SettingsInlineList( + empty = KiloBundle.message("settings.autoApprove.tools.empty"), + onSetLevel = { key, level -> update { setListTool(this, key, level) } }, + onInherit = { key -> update { inheritListTool(this, key) } }, + onSelect = { key -> selected = key?.let { Selection(TOOLS_KEY, it) } }, + picker = picker, + ) + + init { + granular.forEach { (_, section) -> next(section) } + section(KiloBundle.message("settings.autoApprove.title")).row(tools) + } + + @RequiresEdt + fun sync(draft: PermissionDraft, enabled: Boolean) { + val saved = selected + for ((id, section) in granular) section.sync(draft.rules[id], enabled) + tools.syncRows(toolRows(draft), enabled) + restoreSelection(saved) + } + + /** Filter every list on the page by [query], driven by the shared search field. */ + @RequiresEdt + fun filter(query: String) { + for ((_, section) in granular) section.filter(query) + tools.filter(query) + } + + private fun granularSection(tool: String, kind: ExceptionKind): GranularToolSection { + val commands = kind == ExceptionKind.COMMAND + val wildcardKey = if (commands) "commands" else "paths" + val emptyKey = if (commands) "commands" else "paths" + val addKey = if (commands) "addCommand" else "addPath" + val placeholderKey = if (commands) "placeholder.command" else "placeholder.path" + return GranularToolSection( + tool, + KiloBundle.message("settings.autoApprove.tool.$tool"), + KiloBundle.message("settings.autoApprove.wildcardLabel.$wildcardKey"), + KiloBundle.message("settings.autoApprove.filters.empty.$emptyKey"), + KiloBundle.message("settings.autoApprove.$addKey"), + KiloBundle.message("settings.autoApprove.$placeholderKey"), + picker, + { level -> update { setWildcard(this, tool, level) } }, + { update { inheritWildcard(this, tool) } }, + { pattern -> update { addException(this, tool, pattern) } }, + { pattern, level -> update { setException(this, tool, pattern, level) } }, + { from, to -> + selected = Selection(tool, to) + update { editException(this, tool, from, to) } + }, + { patterns -> update { removeExceptions(this, tool, patterns) } }, + { section, key -> selected = key?.let { Selection(section, it) } }, + ) + } + + private fun restoreSelection(saved: Selection?) { + if (saved == null) return + val found = if (saved.section == TOOLS_KEY) { + val ok = tools.selectKey(saved.key, scroll = false) + if (ok) tools.focusList() + ok + } else { + granular.firstOrNull { it.first == saved.section }?.second?.restore(saved.key, active = true) == true + } + if (!found) selected = null + } + + private fun toolRows(draft: PermissionDraft): List = buildList { + for (id in SIMPLE_TOOLS) add(toolRow(draft, id)) + add(groupedRow(draft)) + for (id in TRAILING_TOOLS) add(toolRow(draft, id)) + } + + private fun toolRow(draft: PermissionDraft, tool: String) = PermissionListRow( + key = tool, + title = toolTitle(tool), + description = KiloBundle.message("settings.autoApprove.tool.$tool"), + level = effectiveLevel(draft, tool), + inherited = inheritedWildcard(draft.rules[tool]), + defaultLevel = defaultLevel(tool), + canInherit = true, + ) + + private fun groupedRow(draft: PermissionDraft) = PermissionListRow( + key = GROUP_KEY, + title = "Todoread / Todowrite", + description = KiloBundle.message("settings.autoApprove.tool.todoreadwrite"), + level = mostRestrictive(GROUPED_IDS.map { effectiveLevel(draft, it) }), + inherited = GROUPED_IDS.all { inheritedWildcard(draft.rules[it]) }, + defaultLevel = mostRestrictive(GROUPED_IDS.map(::defaultLevel)), + canInherit = true, + ) + + private fun setListTool(draft: PermissionDraft, key: String, level: String): PermissionDraft { + if (key == GROUP_KEY) return setGrouped(draft, GROUPED_IDS, level) + return setWildcard(draft, key, level) + } + + private fun inheritListTool(draft: PermissionDraft, key: String): PermissionDraft { + if (key == GROUP_KEY) return inheritGrouped(draft, GROUPED_IDS) + return inheritWildcard(draft, key) + } + + private companion object { + const val GROUP_KEY = "todoread+todowrite" + const val TOOLS_KEY = "tools" + } + + private data class Selection(val section: String, val key: String) +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsState.kt new file mode 100644 index 00000000000..82a01edfcab --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsState.kt @@ -0,0 +1,199 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.rpc.dto.ConfigDto +import ai.kilocode.rpc.dto.ConfigPatchDto +import ai.kilocode.rpc.dto.PermissionConfigDto +import ai.kilocode.rpc.dto.PermissionRuleDto + +/** + * Draft state for the Auto-Approve settings page: the desired `config.permission` map. + * + * Rule maps never hold explicit `null` values here — a tool/pattern is either present (with a + * real level) or absent (inherited). `null` values only appear in the [PermissionConfigDto] patch + * produced by [permissionPatch], where they signal deletion to the CLI's PATCH merge. + */ +internal data class PermissionDraft(val rules: Map = emptyMap()) + +/** The three permission levels, in restrictiveness order. Shared by [LevelSelect] and [SettingsInlineList]. */ +internal val LEVELS = listOf("allow", "ask", "deny") + +// Keep aligned with the CLI's DEFAULT_RULES (permission-utils.ts:8-13). +private val DEFAULT_LEVEL = mapOf( + "external_directory" to "ask", + "bash" to "ask", + "doom_loop" to "ask", +) + +private val RESTRICTION_ORDER = mapOf("allow" to 0, "ask" to 1, "deny" to 2) + +internal fun defaultLevel(tool: String): String = DEFAULT_LEVEL[tool] ?: "allow" + +internal fun permissionDraft(config: ConfigDto?): PermissionDraft = PermissionDraft(config?.permission ?: emptyMap()) + +internal fun wildcardLevel(rule: PermissionRuleDto?): String? = when (rule) { + null -> null + is PermissionRuleDto.Level -> rule.value + is PermissionRuleDto.Patterns -> rule.map["*"] +} + +internal fun inheritedWildcard(rule: PermissionRuleDto?): Boolean = when (rule) { + null -> true + is PermissionRuleDto.Level -> false + is PermissionRuleDto.Patterns -> rule.map["*"] == null +} + +internal fun effectiveLevel(draft: PermissionDraft, tool: String): String = + wildcardLevel(draft.rules[tool]) ?: defaultLevel(tool) + +internal fun exceptions(rule: PermissionRuleDto?): List> { + if (rule !is PermissionRuleDto.Patterns) return emptyList() + return rule.map.entries + .filter { it.key != "*" && it.value != null } + .map { it.key to it.value!! } +} + +internal fun mostRestrictive(levels: List): String { + val start = levels.firstOrNull() ?: "allow" + return levels.fold(start) { best, level -> + if ((RESTRICTION_ORDER[level] ?: 0) > (RESTRICTION_ORDER[best] ?: 0)) level else best + } +} + +/** Set the wildcard level for [tool], preserving any existing exceptions. */ +internal fun setWildcard(draft: PermissionDraft, tool: String, level: String): PermissionDraft { + val excs = exceptions(draft.rules[tool]) + val rule = if (excs.isEmpty()) { + PermissionRuleDto.Level(level) + } else { + PermissionRuleDto.Patterns(mapOf("*" to level) + excs.toMap()) + } + return draft.copy(rules = draft.rules + (tool to rule)) +} + +/** Revert [tool]'s wildcard to the CLI default, preserving any existing exceptions. */ +internal fun inheritWildcard(draft: PermissionDraft, tool: String): PermissionDraft { + val excs = exceptions(draft.rules[tool]) + return if (excs.isNotEmpty()) { + draft.copy(rules = draft.rules + (tool to PermissionRuleDto.Patterns(excs.toMap()))) + } else { + draft.copy(rules = draft.rules - tool) + } +} + +/** Set (add or change) a single exception pattern's level for [tool]. */ +internal fun setException(draft: PermissionDraft, tool: String, pattern: String, level: String): PermissionDraft { + val rule = draft.rules[tool] + val base = when (rule) { + null -> emptyMap() + is PermissionRuleDto.Level -> rule.value?.let { mapOf("*" to it) } ?: emptyMap() + is PermissionRuleDto.Patterns -> rule.map.mapNotNull { (key, value) -> value?.let { key to it } }.toMap() + } + return draft.copy(rules = draft.rules + (tool to PermissionRuleDto.Patterns(base + (pattern to level)))) +} + +/** Add a new exception pattern for [tool], defaulting its level to allow. */ +internal fun addException(draft: PermissionDraft, tool: String, pattern: String): PermissionDraft { + val rule = draft.rules[tool] as? PermissionRuleDto.Patterns + if (rule?.map?.get(pattern) != null) return draft + return setException(draft, tool, pattern, "allow") +} + +internal fun editException(draft: PermissionDraft, tool: String, from: String, to: String): PermissionDraft { + if (from == to) return draft + val rule = draft.rules[tool] as? PermissionRuleDto.Patterns ?: return draft + val level = rule.map[from] ?: return draft + if (rule.map[to] != null) return draft + val map = rule.map.filterKeys { it != from } + (to to level) + return draft.copy(rules = draft.rules + (tool to PermissionRuleDto.Patterns(map))) +} + +/** Remove a single exception pattern for [tool]. */ +internal fun removeException(draft: PermissionDraft, tool: String, pattern: String): PermissionDraft { + val rule = draft.rules[tool] as? PermissionRuleDto.Patterns ?: return draft + val map = rule.map.filterKeys { it != pattern } + return if (map.isEmpty()) { + draft.copy(rules = draft.rules - tool) + } else { + draft.copy(rules = draft.rules + (tool to PermissionRuleDto.Patterns(map))) + } +} + +internal fun removeExceptions(draft: PermissionDraft, tool: String, patterns: List): PermissionDraft { + if (patterns.isEmpty()) return draft + val rule = draft.rules[tool] as? PermissionRuleDto.Patterns ?: return draft + val remove = patterns.toSet() + val map = rule.map.filterKeys { it !in remove } + return if (map.isEmpty()) { + draft.copy(rules = draft.rules - tool) + } else { + draft.copy(rules = draft.rules + (tool to PermissionRuleDto.Patterns(map))) + } +} + +/** Apply the same scalar level to every id in a grouped row (e.g. todoread/todowrite). */ +internal fun setGrouped(draft: PermissionDraft, ids: List, level: String): PermissionDraft = + draft.copy(rules = draft.rules + ids.associateWith { PermissionRuleDto.Level(level) }) + +/** Revert every id in a grouped row to the CLI default. */ +internal fun inheritGrouped(draft: PermissionDraft, ids: List): PermissionDraft = + draft.copy(rules = draft.rules - ids.toSet()) + +/** + * Diff [from] (baseline) against [to] (draft) into a single [PermissionConfigDto] patch, or + * `null` if there is nothing to send. See the plan's diff algorithm for the exact semantics: + * missing-in-`to` tools are deleted (`Level(null)`), new tools are sent in full, and for tools + * present in both, changed `Patterns` rules include `null` deletes for every pattern (including + * `*`) that existed in `from` but is absent from `to`. + */ +internal fun permissionPatch(from: PermissionDraft, to: PermissionDraft): PermissionConfigDto? { + val result = mutableMapOf() + for (tool in from.rules.keys + to.rules.keys) { + val fromRule = from.rules[tool] + val toRule = to.rules[tool] + if (toRule == null) { + if (fromRule != null) result[tool] = PermissionRuleDto.Level(null) + continue + } + if (fromRule == null) { + result[tool] = toRule + continue + } + if (fromRule == toRule) continue + result[tool] = when (toRule) { + is PermissionRuleDto.Level -> toRule + is PermissionRuleDto.Patterns -> { + val map = toRule.map.toMutableMap() + if (fromRule is PermissionRuleDto.Patterns) { + for (key in fromRule.map.keys) { + if (key !in toRule.map) map[key] = null + } + } + PermissionRuleDto.Patterns(map) + } + } + } + return result.takeIf { it.isNotEmpty() } +} + +// Named `patch` (not `change`) to avoid colliding with BaseSettingsUi's `change()` override, which +// would otherwise resolve to itself and recurse infinitely instead of calling this top-level helper. +internal fun patch(from: PermissionDraft, to: PermissionDraft): ConfigPatchDto? = + permissionPatch(from, to)?.let { ConfigPatchDto(permission = it) } + +private fun normalize(rules: Map): Map = + rules.mapNotNull { (tool, rule) -> + when (rule) { + is PermissionRuleDto.Level -> rule.value?.let { tool to rule } + is PermissionRuleDto.Patterns -> { + val map = rule.map.filterValues { it != null } + if (map.isEmpty()) null else tool to PermissionRuleDto.Patterns(map) + } + } + }.toMap() + +internal fun savedMatches(base: PermissionDraft, draft: PermissionDraft): Boolean = + normalize(base.rules) == normalize(draft.rules) + +/** `external_directory` -> `External Directory`. */ +internal fun toolTitle(id: String): String = + id.split("_").joinToString(" ") { word -> word.replaceFirstChar { it.uppercaseChar() } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUi.kt new file mode 100644 index 00000000000..556f5f7541a --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUi.kt @@ -0,0 +1,99 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.base.BaseSettingsUi +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.log.KiloLog +import ai.kilocode.rpc.dto.ConfigPatchDto +import ai.kilocode.rpc.dto.KiloAppStateDto +import ai.kilocode.rpc.dto.KiloAppStatusDto +import com.intellij.openapi.components.service +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.SearchTextField +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBUI +import kotlinx.coroutines.CoroutineScope +import javax.swing.event.DocumentEvent + +internal class AutoApproveSettingsUi( + cs: CoroutineScope, + private val app: KiloAppService = service(), + workspaces: KiloWorkspaceService = service(), + private val picker: LevelPicker = PopupLevelPicker, +) : BaseSettingsUi( + cs, + PermissionDraft(), + app, + workspaces, + loginBanner = false, +) { + private val search = SearchTextField(false) + + init { + search.textEditor.emptyText.text = KiloBundle.message("settings.autoApprove.filter") + search.border = JBUI.Borders.empty(UiStyle.Gap.md(), 0) + search.textEditor.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) = form.filter(search.text) + }) + setHeader(search) + startSettings(AutoApproveContent({ updateDraft(it) }, picker)) + } + + override fun change(from: PermissionDraft, to: PermissionDraft): ConfigPatchDto? = patch(from, to) + + override fun save(change: ConfigPatchDto, done: (KiloAppStateDto?) -> Unit) { + app.updateConfigAsync(change, done) + } + + override fun base(result: KiloAppStateDto): PermissionDraft = permissionDraft(result.config) + + override fun draft(state: KiloAppStateDto): PermissionDraft = permissionDraft(state.config) + + override fun saved(base: PermissionDraft, draft: PermissionDraft): Boolean = savedMatches(base, draft) + + override fun pendingText(): String = KiloBundle.message("settings.autoApprove.save.pending") + + override fun failedText(): String = KiloBundle.message("settings.autoApprove.save.failed") + + override suspend fun loadWorkspace(root: String) = Unit + + override fun applyWorkspace(result: Unit) = Unit + + override fun logSaveStarted(change: ConfigPatchDto) = LOG.info("auto-approve settings save: started") + + override fun logSaveCompleted(change: ConfigPatchDto) = LOG.info("auto-approve settings save: completed") + + override fun logSaveFailed(change: ConfigPatchDto) = LOG.warn("auto-approve settings save: failed") + + override fun logSaveFailedAfterDispose(change: ConfigPatchDto) = LOG.warn("auto-approve settings save: failed after dispose") + + override fun logSaveCompletedAfterDispose(change: ConfigPatchDto) = LOG.info("auto-approve settings save: completed after dispose") + + @RequiresEdt + override fun syncContent() { + val ready = appState.status == KiloAppStatusDto.READY + val editable = ready && !saving + form.sync(draft, editable) + top.hideBanner() + val err = saveError + if (saving) { + showProgress(KiloBundle.message("settings.autoApprove.save.pending")) + return + } + if (err != null) { + showError(err) + return + } + if (!ready) { + showProgress(KiloBundle.message("settings.cli.unavailable.message")) + return + } + clearProgress() + } + + private companion object { + val LOG = KiloLog.create(AutoApproveSettingsUi::class.java) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/GranularToolSection.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/GranularToolSection.kt new file mode 100644 index 00000000000..21ab8775126 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/GranularToolSection.kt @@ -0,0 +1,68 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.base.BaseContentPanel +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.rpc.dto.PermissionRuleDto +import com.intellij.ui.components.JBLabel +import com.intellij.util.concurrency.annotations.RequiresEdt +import javax.swing.ListSelectionModel + +/** One granular permission tool (`external_directory`, `bash`, `read`, `edit`). */ +internal class GranularToolSection( + private val tool: String, + description: String, + private val wildcardLabel: String, + emptyText: String, + addLabel: String, + placeholder: String, + picker: LevelPicker, + private val onWildcardChange: (String) -> Unit, + private val onWildcardInherit: () -> Unit, + private val onExceptionAdd: (String) -> Unit, + private val onExceptionSetLevel: (String, String) -> Unit, + private val onExceptionEdit: (String, String) -> Unit, + private val onExceptionRemove: (List) -> Unit, + private val onSelect: (String, String?) -> Unit = { _, _ -> }, +) : BaseContentPanel() { + private val wildcard = LevelSelect(onWildcardChange) { onWildcardInherit() } + private val list = SettingsInlineList( + empty = emptyText, + addLabel = addLabel, + placeholder = placeholder, + right = toolbarRight(), + onAdd = onExceptionAdd, + onSetLevel = onExceptionSetLevel, + onEdit = onExceptionEdit, + onRemove = onExceptionRemove, + onSelect = { key -> onSelect(tool, key) }, + picker = picker, + selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION, + ) + + init { + section(toolTitle(tool), description) + .row(list) + } + + @RequiresEdt + fun sync(rule: PermissionRuleDto?, enabled: Boolean) { + wildcard.sync(wildcardLevel(rule) ?: defaultLevel(tool), inheritedWildcard(rule), enabled) + list.syncItems(exceptions(rule), enabled) + } + + @RequiresEdt + fun filter(query: String) = list.filter(query) + + @RequiresEdt + fun restore(key: String, active: Boolean): Boolean { + val found = list.selectKey(key, scroll = false) + if (found && active) list.focusList() + return found + } + + private fun toolbarRight() = Stack.horizontal(UiStyle.Gap.sm()) + .next(JBLabel(wildcardLabel)) + .next(wildcard) +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/LevelSelect.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/LevelSelect.kt new file mode 100644 index 00000000000..d3d0faf0018 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/LevelSelect.kt @@ -0,0 +1,61 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.client.plugin.KiloBundle +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.SimpleListCellRenderer +import com.intellij.util.concurrency.annotations.RequiresEdt +import java.awt.event.ItemEvent +import javax.swing.DefaultComboBoxModel + +internal fun levelLabel(level: String): String = when (level) { + "allow" -> KiloBundle.message("settings.autoApprove.level.allow") + "ask" -> KiloBundle.message("settings.autoApprove.level.ask") + "deny" -> KiloBundle.message("settings.autoApprove.level.deny") + else -> level +} + +/** + * Reusable Allow/Ask/Deny combo, optionally prefixed with a "Default (X)" inherit option. + * Used by every non-list permission row and every granular wildcard row. + */ +internal class LevelSelect( + private val onChange: (String) -> Unit, + private val onInherit: (() -> Unit)? = null, +) : ComboBox(DefaultComboBoxModel()) { + + internal sealed class Item { + data class Default(val resolved: String) : Item() + data class Level(val value: String) : Item() + } + + private var syncing = false + + init { + renderer = SimpleListCellRenderer.create("") { item -> + when (item) { + is Item.Default -> KiloBundle.message("settings.autoApprove.default", levelLabel(item.resolved)) + is Item.Level -> levelLabel(item.value) + } + } + addItemListener { e -> + if (syncing || e.stateChange != ItemEvent.SELECTED) return@addItemListener + when (val item = e.item as? Item) { + is Item.Default -> onInherit?.invoke() + is Item.Level -> onChange(item.value) + null -> Unit + } + } + } + + @RequiresEdt + fun sync(currentLevel: String, inherited: Boolean, enabled: Boolean) { + syncing = true + val next = DefaultComboBoxModel() + if (onInherit != null) next.addElement(Item.Default(currentLevel)) + for (level in LEVELS) next.addElement(Item.Level(level)) + model = next + selectedItem = if (inherited && onInherit != null) Item.Default(currentLevel) else Item.Level(currentLevel) + isEnabled = enabled + syncing = false + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineList.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineList.kt new file mode 100644 index 00000000000..b0f77468b18 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineList.kt @@ -0,0 +1,236 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.base.SettingsInlineListPanel +import ai.kilocode.client.settings.base.SettingsListCell +import ai.kilocode.client.settings.base.SettingsListConfig +import ai.kilocode.client.settings.base.SettingsListItem +import ai.kilocode.client.settings.base.SettingsToolbarAction +import ai.kilocode.client.settings.base.settingsListCellBounds +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.ui.SimpleListCellRenderer +import com.intellij.ui.awt.RelativePoint +import java.awt.Point +import javax.swing.JComponent +import javax.swing.ListSelectionModel + +internal data class PermissionListRow( + val key: String, + val title: String, + val description: String? = null, + val level: String, + val inherited: Boolean = false, + val defaultLevel: String = level, + val canInherit: Boolean = false, + val editable: Boolean = false, +) + +/** A level choice offered by the row popup: either revert to the CLI default or a concrete level. */ +internal sealed interface LevelChoice { + data class Default(val level: String) : LevelChoice + data class Level(val level: String) : LevelChoice +} + +/** + * Renders the per-row level chooser. The production picker is a JBPopup anchored under the level + * cell; tests substitute a picker that resolves a choice directly. + */ +internal fun interface LevelPicker { + fun popup(choices: List, choose: (LevelChoice) -> Unit): JBPopup? +} + +internal object PopupLevelPicker : LevelPicker { + override fun popup(choices: List, choose: (LevelChoice) -> Unit): JBPopup = + JBPopupFactory.getInstance() + .createPopupChooserBuilder(choices) + .setRenderer(SimpleListCellRenderer.create("") { levelChoiceLabel(it) }) + .setItemChosenCallback(choose) + .createPopup() +} + +internal fun levelChoiceLabel(choice: LevelChoice): String = when (choice) { + is LevelChoice.Default -> KiloBundle.message("settings.autoApprove.default", levelLabel(choice.level)) + is LevelChoice.Level -> levelLabel(choice.level) +} + +/** + * Embeddable auto-approve permission list. Uses the standard inline settings list layout: + * toolbar, filter field, then list with title/description rows and a right-side level action cell. + */ +internal class SettingsInlineList( + private val empty: String, + private val addLabel: String? = null, + private val placeholder: String = "", + private val right: JComponent? = null, + private val onAdd: ((String) -> Unit)? = null, + private val onSetLevel: (String, String) -> Unit, + private val onInherit: ((String) -> Unit)? = null, + private val onEdit: ((String, String) -> Unit)? = null, + private val onRemove: ((List) -> Unit)? = null, + private val onSelect: (String?) -> Unit = {}, + private val picker: LevelPicker = PopupLevelPicker, + selectionMode: Int = ListSelectionModel.SINGLE_SELECTION, +) : SettingsInlineListPanel( + empty, + SettingsListConfig.Equal, + selectionMode, + showSearch = false, +) { + private var keys = emptySet() + + /** Overridable in tests, mirrors `PatternList.input` in ContextSettingsUi.kt. */ + internal var input: () -> String? = { + Messages.showInputDialog(this, placeholder, addLabel.orEmpty(), null) + } + + internal var editInput: (String) -> String? = { key -> + Messages.showInputDialog( + this, + placeholder, + KiloBundle.message("settings.autoApprove.edit"), + null, + key, + null, + ) + } + + init { + start() + } + + fun syncRows(rows: List, enabled: Boolean) { + keys = rows.map { it.key }.toSet() + setItems(rows.map(::PermissionItem), enabled) + } + + fun syncItems(exceptions: List>, enabled: Boolean) { + syncRows(exceptions.map { (pattern, level) -> PermissionListRow(pattern, pattern, level = level, editable = true) }, enabled) + } + + override fun onCell(key: String, cellId: String) { + if (!isEnabled) return + onSelect(key) + if (cellId == LEVEL_CELL) showLevelPopup(key) + if (cellId == EDIT_CELL) promptEdit(key) + } + + override fun toolbarActions(): List = buildList { + val add = addLabel + if (add != null && onAdd != null) { + add(SettingsToolbarAction( + KiloBundle.message("settings.autoApprove.add"), + add, + AllIcons.General.Add, + { isEnabled }, + ) { promptAdd() }) + } + if (onRemove != null) { + add(SettingsToolbarAction( + KiloBundle.message("settings.autoApprove.delete"), + KiloBundle.message("settings.autoApprove.delete.description"), + AllIcons.General.Remove, + { isEnabled && selectedKeys().isNotEmpty() }, + ) { removeSelected() }) + } + } + + override fun toolbarRight(): JComponent? = right + + override fun onSelectionChanged(keys: List) { + onSelect(keys.firstOrNull()) + } + + private fun promptAdd() { + if (!isEnabled) return + val add = onAdd ?: return + val value = input()?.trim().orEmpty() + if (value.isBlank()) return + if (value in keys) { + selectKey(value, scroll = true) + return + } + add(value) + } + + private fun promptEdit(key: String) { + val edit = onEdit ?: return + val value = editInput(key)?.trim().orEmpty() + if (value.isBlank() || value == key) return + if (value in keys) { + selectKey(value, scroll = true) + return + } + edit(key, value) + } + + private fun removeSelected() { + val remove = onRemove ?: return + val keys = selectedKeys() + if (keys.isEmpty()) return + remove(keys) + } + + private fun showLevelPopup(key: String) { + val item = item(key) ?: return + val idx = index(key) ?: return + val bounds = settingsListCellBounds(view.list, idx, idx == view.list.selectedIndex)[LEVEL_CELL] ?: return + val popup = picker.popup(choices(item.row)) { choice -> choose(key, choice) } ?: return + trackPopup(popup) + popup.show(RelativePoint(view.list, Point(bounds.x, bounds.y + bounds.height))) + } + + private fun item(key: String): PermissionItem? { + val model = view.list.model + return (0 until model.size) + .mapNotNull { model.getElementAt(it) as? PermissionItem } + .firstOrNull { it.key == key } + } + + private fun index(key: String): Int? { + val model = view.list.model + return (0 until model.size).firstOrNull { (model.getElementAt(it) as? PermissionItem)?.key == key } + } + + private fun choices(row: PermissionListRow): List = buildList { + if (row.canInherit && onInherit != null) add(LevelChoice.Default(row.defaultLevel)) + LEVELS.forEach { add(LevelChoice.Level(it)) } + } + + private fun choose(key: String, choice: LevelChoice) { + onSelect(key) + when (choice) { + is LevelChoice.Default -> onInherit?.invoke(key) + is LevelChoice.Level -> onSetLevel(key, choice.level) + } + } + + private data class PermissionItem(val row: PermissionListRow) : SettingsListItem { + override val key: String get() = row.key + override val title: String get() = row.title + override val description: String? get() = row.description + override val doubleClick: String? get() = EDIT_CELL.takeIf { row.editable } + override val cells: List = buildList { + if (row.editable) add(SettingsListCell( + id = EDIT_CELL, + label = KiloBundle.message("settings.autoApprove.edit"), + )) + add(SettingsListCell( + id = LEVEL_CELL, + label = if (row.inherited) KiloBundle.message( + "settings.autoApprove.default", + levelLabel(row.defaultLevel), + ) else levelLabel(row.level), + alwaysVisible = true, + )) + } + } + + private companion object { + const val EDIT_CELL = "edit" + const val LEVEL_CELL = "level" + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseSettingsUi.kt index 35f4452f774..4287b8a8a1b 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/BaseSettingsUi.kt @@ -36,7 +36,9 @@ internal abstract class BaseSettingsUi( private val workspaces: KiloWorkspaceService = service(), private val hint: String? = null, private val loginBanner: Boolean = true, -) : SettingsPanel(), SettingsDraftPage { + scroll: Boolean = true, + pad: Boolean = true, +) : SettingsPanel(scroll, pad), SettingsDraftPage { protected lateinit var form: C private set protected val jobs = mutableListOf() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/DraftReadyConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/DraftReadyConfigurable.kt index 55f6602e651..5f51f48f0c9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/DraftReadyConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/DraftReadyConfigurable.kt @@ -1,10 +1,11 @@ package ai.kilocode.client.settings.base import com.intellij.openapi.Disposable +import com.intellij.openapi.options.Configurable import kotlinx.coroutines.CoroutineScope import javax.swing.JComponent -abstract class DraftReadyConfigurable : KiloReadyConfigurable() { +abstract class DraftReadyConfigurableBase : KiloReadyConfigurableBase() { private var panel: T? = null final override fun createReadyComponent(cs: CoroutineScope): JComponent { @@ -31,3 +32,5 @@ abstract class DraftReadyConfigurable : KiloReadyConfigurable() protected abstract fun create(cs: CoroutineScope): T } + +abstract class DraftReadyConfigurable : DraftReadyConfigurableBase(), Configurable.NoScroll diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/KiloReadyConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/KiloReadyConfigurable.kt index 638821550a0..04191ef4c63 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/KiloReadyConfigurable.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/KiloReadyConfigurable.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.withContext import java.awt.BorderLayout import javax.swing.JComponent -abstract class KiloReadyConfigurable : SearchableConfigurable, Configurable.NoScroll { +abstract class KiloReadyConfigurableBase : SearchableConfigurable, Configurable.NoMargin { private var shell: SettingsOverlayPanel? = null private var scope: CoroutineScope? = null private var ready: JComponent? = null @@ -39,7 +39,9 @@ abstract class KiloReadyConfigurable : SearchableConfigurable, Configurable.NoSc @RequiresEdt override fun createComponent(): JComponent { checkEdt() - val root = if (scrollReadyShell()) SettingsPanel() else SettingsOverlayPanel() + // The shell never pads: the ready UI (or unavailable content) owns its own insets. This keeps + // a single margin when the shell scrolls a nested SettingsPanel. + val root = if (scrollReadyShell()) SettingsPanel(pad = false) else SettingsOverlayPanel() val cs = CoroutineScope(SupervisorJob() + Dispatchers.Default) shell = root scope = cs @@ -161,3 +163,5 @@ abstract class KiloReadyConfigurable : SearchableConfigurable, Configurable.NoSc val edt = Dispatchers.EDT + ModalityState.any().asContextElement() } } + +abstract class KiloReadyConfigurable : KiloReadyConfigurableBase(), Configurable.NoScroll diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsContentEditor.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsContentEditor.kt new file mode 100644 index 00000000000..2ce0f6a0f32 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsContentEditor.kt @@ -0,0 +1,85 @@ +package ai.kilocode.client.settings.base + +import ai.kilocode.client.session.ui.style.SessionUiStyle +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.fileTypes.FileType +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.fileTypes.PlainTextFileType +import com.intellij.openapi.fileTypes.UnknownFileType +import com.intellij.openapi.project.ProjectManager +import com.intellij.ui.EditorTextField +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.JBUI +import javax.swing.ScrollPaneConstants + +/** + * Shared code-editor primitives for settings dialogs (skill content, instruction files). + * + * Keeps the tuned [EditorTextField] configuration, scroll chrome, and content-aware file-type + * detection in one place so pages don't each hand-roll their own editor. + */ +internal class SettingsContentField( + content: String, + fileType: FileType, + editable: Boolean, +) : EditorTextField( + EditorFactory.getInstance().createDocument(content), + ProjectManager.getInstance().defaultProject, + fileType, + !editable, + false, +) { + init { + border = JBUI.Borders.empty() + setOneLineMode(false) + addSettingsProvider { ed -> + ed.setBorder(JBUI.Borders.empty()) + ed.scrollPane.border = JBUI.Borders.empty() + ed.scrollPane.viewportBorder = JBUI.Borders.empty() + ed.settings.isUseSoftWraps = true + ed.settings.isPaintSoftWraps = false + ed.settings.isAdditionalPageAtBottom = false + ed.scrollPane.horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + ed.scrollPane.verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED + } + } +} + +internal fun settingsContentScroll(field: SettingsContentField) = JBScrollPane(field).apply { + viewportBorder = JBUI.Borders.empty( + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_VERTICAL_PADDING), + JBUI.scale(SessionUiStyle.View.Prompt.SHELL_HORIZONTAL_PADDING), + ) + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED + preferredSize = JBUI.size(720, 520) +} + +/** + * Resolve a [FileType] for editor highlighting. Content syntax wins over the file name so + * extension-less locations still highlight correctly; unknown types fall back to plain text. + */ +internal fun settingsEditorFileType(name: String, content: String? = null): FileType { + val syntax = content?.syntaxName() + val fileName = syntax ?: name.substringAfterLast('/').substringAfterLast('\\').ifBlank { "file.txt" } + val type = FileTypeManager.getInstance().getFileTypeByFileName(fileName) + if (type == UnknownFileType.INSTANCE) return PlainTextFileType.INSTANCE + return type +} + +private fun String.syntaxName(): String? { + val text = trimStart() + if (text.isBlank()) return null + if (text.looksHtml()) return "index.html" + if (text.looksMarkdown()) return "content.md" + return null +} + +private fun String.looksHtml() = contains(Regex("^\\s*( + line.matches(Regex("\\s{0,3}(#{1,6}\\s+.+|[-*+]\\s+.+|\\d+\\.\\s+.+|```.*|>\\s+.+)")) || + line.contains(Regex("(`[^`]+`|\\[[^]]+][(][^)]+[)])")) +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsInlineListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsInlineListPanel.kt new file mode 100644 index 00000000000..e141407a18e --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsInlineListPanel.kt @@ -0,0 +1,180 @@ +package ai.kilocode.client.settings.base + +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.Stack +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.ActionToolbar +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.SearchTextField +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBUI +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.KeyEvent +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.KeyStroke +import javax.swing.ListSelectionModel +import javax.swing.event.DocumentEvent + +/** + * Lightweight embedded settings list layout: toolbar, filter text field, then list. + * + * Use [SettingsListPanel] for full async settings pages. Use this class for retained inline lists + * embedded inside an existing settings form. + */ +internal abstract class SettingsInlineListPanel( + emptyText: String, + cfg: SettingsListConfig = SettingsListConfig.Equal, + private val selectionMode: Int = ListSelectionModel.SINGLE_SELECTION, + private val showSearch: Boolean = true, +) : BaseContentPanel() { + private val search = SearchTextField(false) + protected val view = SettingsListView(emptyText, cfg) { key, cellId -> onCell(key, cellId) } + private var toolbar: ActionToolbar? = null + private var syncing = false + + @RequiresEdt + protected fun start() { + checkEdt() + view.list.selectionMode = selectionMode + view.minimumSize = JBUI.size(0, minListHeight()) + view.list.minimumSize = JBUI.size(0, minListHeight()) + view.onSelect = { + toolbar?.updateActionsImmediately() + if (!syncing) onSelectionChanged(selectedKeys()) + } + next(toolbarRow()) + gap(UiStyle.Gap.sm()) + if (showSearch) { + search.textEditor.emptyText.text = searchPlaceholder() + next(search) + gap(UiStyle.Gap.sm()) + wireSearch() + } + next(view) + } + + /** Filter list rows by [query]. Used by an external search field when the list hides its own. */ + @RequiresEdt + fun filter(query: String) { + checkEdt() + view.filter(query) + } + + @RequiresEdt + protected fun trackPopup(popup: JBPopup) { + checkEdt() + view.trackPopup(popup) + } + + @RequiresEdt + fun setItems(items: List, enabled: Boolean) { + checkEdt() + setEnabled(enabled) + syncing = true + try { + view.update(items, SettingsListSelection.PreserveNoScroll) + } finally { + syncing = false + } + toolbar?.updateActionsImmediately() + } + + @RequiresEdt + protected fun selectedKeys(): List { + checkEdt() + return view.list.selectedValuesList.map { it.key } + } + + @RequiresEdt + fun selectKey(key: String, scroll: Boolean = true): Boolean { + checkEdt() + return view.select(key, scroll) + } + + @RequiresEdt + fun focusList() { + checkEdt() + view.focusList() + } + + override fun setEnabled(enabled: Boolean) { + super.setEnabled(enabled) + if (showSearch) { + search.isEnabled = enabled + search.textEditor.isEnabled = enabled + } + view.isEnabled = enabled + view.setBusy(!enabled) + toolbar?.updateActionsImmediately() + } + + override fun getPreferredSize(): Dimension { + val base = super.getPreferredSize() + val missing = maxOf(0, minListHeight() - view.preferredSize.height) + return Dimension(base.width, base.height + missing) + } + + override fun getMinimumSize(): Dimension = preferredSize + + protected abstract fun onCell(key: String, cellId: String) + + protected open fun toolbarActions(): List = emptyList() + + protected open fun toolbarRight(): JComponent? = null + + protected open fun searchPlaceholder(): String = "" + + protected open fun onSelectionChanged(keys: List) = Unit + + private fun toolbarRow(): JComponent { + val row = JPanel(BorderLayout()) + UiStyle.Components.transparent(row) + toolbar = ActionManager.getInstance().createActionToolbar( + ActionPlaces.TOOLBAR, + DefaultActionGroup(toolbarActions()), + true, + ).apply { + targetComponent = this@SettingsInlineListPanel + updateActionsImmediately() + } + row.add(toolbar!!.component, BorderLayout.WEST) + toolbarRight()?.let { row.add(it, BorderLayout.EAST) } + return row + } + + private fun wireSearch() { + search.textEditor.registerKeyboardAction( + { view.primary() }, + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), + JComponent.WHEN_FOCUSED, + ) + search.textEditor.registerKeyboardAction( + { view.move(-1) }, + KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), + JComponent.WHEN_FOCUSED, + ) + search.textEditor.registerKeyboardAction( + { view.move(1) }, + KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), + JComponent.WHEN_FOCUSED, + ) + search.textEditor.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + view.filter(search.text) + } + }) + } + + private fun checkEdt() { + check(ApplicationManager.getApplication().isDispatchThread) { "Settings inline list updates must run on EDT" } + } + + private fun minListHeight() = UiStyle.Gap.xl() + UiStyle.Gap.pad() +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt index 81cac63adc8..dcad8c68633 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListModel.kt @@ -8,6 +8,7 @@ import java.awt.Point import java.awt.Rectangle import javax.swing.Icon import javax.swing.JList +import javax.swing.ListSelectionModel import javax.swing.ListCellRenderer import javax.swing.SwingUtilities @@ -21,6 +22,8 @@ internal data class SettingsListConfig( val height: SettingsListRowHeight, val description: Boolean = true, val descriptionIndent: Boolean = true, + val tooltip: Boolean = true, + val selection: Int = ListSelectionModel.SINGLE_SELECTION, ) { companion object { val Equal = SettingsListConfig(SettingsListRowHeight.EQUAL) @@ -41,7 +44,9 @@ internal data class SettingsListCell( internal interface SettingsListItem { val key: String val title: String + val note: String? get() = null val description: String? get() = null + val doubleClick: String? get() = null val icon: Icon? get() = null val section: String? get() = null val badges: List get() = emptyList() @@ -80,7 +85,10 @@ internal fun settingsListCellBounds( @Suppress("UNCHECKED_CAST") val renderer = list.cellRenderer as? ListCellRenderer ?: return emptyMap() val cell = list.getCellBounds(index, index) ?: return emptyMap() - val comp = renderer.getListCellRendererComponent(list, model.getElementAt(index), index, selected, list.hasFocus()) + // Render as focused so the action-cell geometry is available for hit-testing even when the + // list is not the focus owner. Painting still hides the cells on an unfocused list; this only + // resolves click targets and keeps them stable regardless of focus. + val comp = renderer.getListCellRendererComponent(list, model.getElementAt(index), index, selected, true) comp.setBounds(0, 0, cell.width, cell.height) settingsListLayout(comp) val out = linkedMapOf() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListPanel.kt index e88638001bc..9ef81d3ee32 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListPanel.kt @@ -65,7 +65,7 @@ internal abstract class SettingsListPanel( checkEdt() search.textEditor.emptyText.text = searchPlaceholder() view.setEmptyText(emptyText()) - content.add(header(), BorderLayout.NORTH) + setHeader(header()) setContent(view) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt index 314e2c64b7f..bd6affa2b60 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListRenderer.kt @@ -68,21 +68,24 @@ internal class SettingsListRenderer( selected: Boolean, focused: Boolean, ): JPanel { - val focus = selected || list.hasFocus() || focused - val fg = UIUtil.getListForeground(selected, focus) - val weak = if (selected) fg else UiStyle.Colors.weak() + val active = selected && (focused || list.hasFocus() || (list as? SettingsListActive)?.active() == true) + val fg = UIUtil.getListForeground(active, active || focused) + val weak = if (active) fg else UiStyle.Colors.weak() val current = model.items.getOrNull(index) val section = if (current === value) settingsListSectionTitle(model.items, index) else null background = list.background top.background = list.background - wrap.update(list, selected, focus) + wrap.update(list, active, active || focused) sep.caption = section sep.setHideLine(index == 0) top.isVisible = section != null title.clear() title.append(value.title, SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg)) + value.note?.takeIf { it.isNotBlank() }?.let { + title.append(" $it", SimpleTextAttributes.GRAYED_ATTRIBUTES) + } syncBadges(value) icon.icon = value.icon mark.isVisible = value.icon != null @@ -96,7 +99,9 @@ internal class SettingsListRenderer( } desc.foreground = weak - syncCells(value, selected && list.isEnabled, list.isEnabled) + // In-place action buttons follow the selection highlight: only when the selection is + // visible (list focused, or an owned popup is active). An unfocused list hides them. + syncCells(value, active && list.isEnabled, list.isEnabled) top.invalidate() return this } @@ -132,6 +137,10 @@ internal class SettingsListRenderer( } } +internal interface SettingsListActive { + fun active(): Boolean +} + internal class SettingsListActionCell : JBLabel() { var cellId: String = "" private set diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt index 661ac90103f..f8115269354 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsListView.kt @@ -1,46 +1,69 @@ package ai.kilocode.client.settings.base import ai.kilocode.client.session.ui.model.ModelSearch +import ai.kilocode.client.ui.UiStyle import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupListener +import com.intellij.openapi.ui.popup.LightweightWindowEvent import com.intellij.ui.CollectionListModel import com.intellij.ui.ScrollingUtil import com.intellij.ui.components.JBList import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.xml.util.XmlStringUtil import com.intellij.util.ui.UIUtil +import com.intellij.xml.util.XmlStringUtil +import java.awt.Dimension +import java.awt.Rectangle import java.awt.event.KeyEvent +import java.awt.event.FocusAdapter +import java.awt.event.FocusEvent import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JComponent import javax.swing.KeyStroke import javax.swing.ListSelectionModel +import javax.swing.Scrollable +import javax.swing.SwingConstants import javax.swing.event.ListSelectionEvent internal class SettingsListView( empty: String, private val cfg: SettingsListConfig = SettingsListConfig.Equal, private val onCell: (String, String) -> Unit, -) : BaseContentPanel() { +) : BaseContentPanel(), Scrollable { private val model = CollectionListModel() - internal val list = object : JBList(model) { + internal val list: JBList = object : JBList(model), SettingsListActive { + override fun active(): Boolean = popups > 0 + override fun getToolTipText(event: MouseEvent): String? { - if (!cfg.description) return null + val tip = super.getToolTipText(event) + if (tip != null) return tip val idx = locationToIndex(event.point) if (idx < 0) return null val bounds = getCellBounds(idx, idx) ?: return null if (!bounds.contains(event.point)) return null - val note = model.getElementAt(idx).description?.takeIf { it.isNotBlank() } ?: return null + val item = model.getElementAt(idx) + val selected = isSelectedIndex(idx) + val id = settingsListCellBounds(this, idx, selected) + .entries + .firstOrNull { it.value.contains(event.point) } + ?.key + val cell = settingsListVisibleCells(item, selected).firstOrNull { it.id == id } + if (cell != null) return cell.label.takeIf { it.isNotBlank() } + if (!cfg.description || !cfg.tooltip) return null + val note = item.description?.takeIf { it.isNotBlank() } ?: return null val text = note.lines().joinToString("
") { XmlStringUtil.escapeString(it) } return XmlStringUtil.wrapInHtml(text) } }.apply { - selectionMode = ListSelectionModel.SINGLE_SELECTION + selectionMode = cfg.selection setExpandableItemsEnabled(false) emptyText.text = empty } private var items = emptyList() private var filter = "" private var press: Press? = null + private var popups = 0 internal var onSelect: (() -> Unit)? = null fun setEmptyText(text: String) { @@ -57,6 +80,7 @@ internal class SettingsListView( list.addMouseListener(object : MouseAdapter() { override fun mousePressed(e: MouseEvent) { if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_PRESSED, true)) return + list.requestFocusInWindow() press = null val hit = hit(e) ?: return press = Press(hit.item.key, hit.id ?: return) @@ -67,6 +91,11 @@ internal class SettingsListView( val hit = hit(e, enabled = false) ?: return if (hit.id != null) return val item = hit.item + item.doubleClick?.let { id -> + onCell(item.key, id) + e.consume() + return + } primary(item) e.consume() } @@ -84,6 +113,11 @@ internal class SettingsListView( list.addListSelectionListener { e: ListSelectionEvent -> if (!e.valueIsAdjusting) onSelect?.invoke() } + list.addFocusListener(object : FocusAdapter() { + override fun focusGained(e: FocusEvent) = list.repaint() + + override fun focusLost(e: FocusEvent) = list.repaint() + }) ScrollingUtil.installActions(list) next(list) } @@ -94,12 +128,34 @@ internal class SettingsListView( return list.selectedValue } + @RequiresEdt + fun selectedItems(): List { + checkEdt() + return list.selectedValuesList + } + @RequiresEdt fun selectedIndex(): Int { checkEdt() return list.selectedIndex } + @RequiresEdt + fun select(key: String, scroll: Boolean = true): Boolean { + checkEdt() + val idx = settingsListIndex(model.items, key) + if (idx < 0) return false + choose(idx, scroll) + return true + } + + @RequiresEdt + fun focusList() { + checkEdt() + list.requestFocusInWindow() + list.repaint() + } + @RequiresEdt fun update(items: List, selection: SettingsListSelection = SettingsListSelection.Preserve) { checkEdt() @@ -107,25 +163,51 @@ internal class SettingsListView( val key = when (selection) { is SettingsListSelection.Key -> selection.key is SettingsListSelection.Index -> null + SettingsListSelection.PreserveNoScroll, SettingsListSelection.Preserve -> list.selectedValue?.key } val idx = when (selection) { is SettingsListSelection.Index -> selection.index is SettingsListSelection.Key, + SettingsListSelection.PreserveNoScroll, SettingsListSelection.Preserve, -> null } - sync(key, idx) + sync(key, idx, selection != SettingsListSelection.PreserveNoScroll) } @RequiresEdt fun setBusy(value: Boolean) { checkEdt() + list.setPaintBusy(value) if (list.isEnabled == !value) return list.isEnabled = !value list.repaint() } + @RequiresEdt + fun trackPopup(popup: JBPopup) { + checkEdt() + var tracked = false + fun activate() { + if (tracked) return + tracked = true + popups++ + list.repaint() + } + popup.addListener(object : JBPopupListener { + override fun beforeShown(event: LightweightWindowEvent) = activate() + + override fun onClosed(event: LightweightWindowEvent) { + if (!tracked) return + tracked = false + popups = maxOf(0, popups - 1) + list.repaint() + } + }) + if (popup.isVisible) activate() + } + @RequiresEdt fun filter(query: String) { checkEdt() @@ -135,7 +217,7 @@ internal class SettingsListView( } @RequiresEdt - private fun sync(prefer: String? = list.selectedValue?.key, at: Int? = null) { + private fun sync(prefer: String? = list.selectedValue?.key, at: Int? = null, scroll: Boolean = true) { checkEdt() val q = filter.trim() val rows = if (q.isBlank()) items else items.filter { ModelSearch.matches(q, it.title) } @@ -145,7 +227,7 @@ internal class SettingsListView( ?: settingsListIndex(rows, prefer).takeIf { it >= 0 } ?: rows.indices.firstOrNull() ?: -1 - if (idx >= 0) choose(idx) else list.clearSelection() + if (idx >= 0) choose(idx, scroll) else list.clearSelection() } @RequiresEdt @@ -158,7 +240,7 @@ internal class SettingsListView( return } val height = rows.indices.maxOfOrNull { idx -> - list.cellRenderer.getListCellRendererComponent(list, rows[idx], idx, true, list.hasFocus()).preferredSize.height + list.cellRenderer.getListCellRendererComponent(list, rows[idx], idx, true, true).preferredSize.height } ?: -1 if (list.fixedCellHeight == height) return list.fixedCellHeight = height @@ -166,10 +248,10 @@ internal class SettingsListView( } @RequiresEdt - private fun choose(idx: Int) { + private fun choose(idx: Int, scroll: Boolean = true) { checkEdt() list.selectedIndex = idx - ScrollingUtil.ensureIndexIsVisible(list, idx, 0) + if (scroll) ScrollingUtil.ensureIndexIsVisible(list, idx, 0) } @RequiresEdt @@ -191,9 +273,15 @@ internal class SettingsListView( private fun primary(item: SettingsListItem) { val cells = settingsListVisibleCells(item, true) val cell = cells.firstOrNull { it.enabled && it.primary } - ?: cells.firstOrNull { it.enabled } - ?: return - onCell(item.key, cell.id) + if (cell != null) { + onCell(item.key, cell.id) + return + } + item.doubleClick?.let { id -> + onCell(item.key, id) + return + } + cells.firstOrNull { it.enabled }?.let { onCell(item.key, it.id) } } private fun hit(e: MouseEvent, enabled: Boolean = true): Hit? { @@ -201,7 +289,7 @@ internal class SettingsListView( val bounds = idx.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null if (!bounds.contains(e.point)) return null val item = model.getElementAt(idx) - val selected = idx == list.selectedIndex + val selected = list.isSelectedIndex(idx) val id = if (enabled) { settingsListCellAt(list, idx, e.point, selected) } else { @@ -217,6 +305,27 @@ internal class SettingsListView( check(ApplicationManager.getApplication().isDispatchThread) { "Settings list updates must run on EDT" } } + override fun getScrollableTracksViewportWidth() = true + + override fun getScrollableTracksViewportHeight() = false + + override fun getPreferredScrollableViewportSize(): Dimension = preferredSize + + override fun getScrollableUnitIncrement( + visibleRect: Rectangle, + orientation: Int, + direction: Int, + ): Int { + if (orientation != SwingConstants.VERTICAL) return UiStyle.Gap.pad() + return list.fixedCellHeight.takeIf { it > 0 } ?: UiStyle.Gap.xl() + } + + override fun getScrollableBlockIncrement( + visibleRect: Rectangle, + orientation: Int, + direction: Int, + ) = if (orientation == SwingConstants.VERTICAL) visibleRect.height else visibleRect.width + private data class Hit(val item: SettingsListItem, val id: String?) private data class Press(val key: String, val id: String) @@ -234,6 +343,7 @@ private fun settingsListIndex(items: List, index: Int): Int { internal sealed interface SettingsListSelection { data object Preserve : SettingsListSelection + data object PreserveNoScroll : SettingsListSelection data class Key(val key: String) : SettingsListSelection data class Index(val index: Int) : SettingsListSelection } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPanel.kt index cf87dd58ffb..5378bfb5340 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPanel.kt @@ -4,14 +4,19 @@ import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.ui.layout.Stack import ai.kilocode.client.ui.layout.StackAxis import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.JBUI import java.awt.BorderLayout import java.awt.Dimension import java.awt.Rectangle import javax.swing.JComponent +import javax.swing.JPanel import javax.swing.ScrollPaneConstants import javax.swing.Scrollable -internal open class SettingsPanel : SettingsOverlayPanel() { +// The platform no longer wraps our configurables in its default margin (they are Configurable.NoMargin), +// so the page owns its own insets here. The scroll pane stays flush to the panel edges — its scrollbar +// touches the right edge — while the content border and body inset keep text and controls padded. +internal open class SettingsPanel(scroll: Boolean = true, pad: Boolean = true) : SettingsOverlayPanel() { val top = SettingsTop() val settings = Stack.vertical() @@ -20,10 +25,23 @@ internal open class SettingsPanel : SettingsOverlayPanel() { .next(top) .gap(UiStyle.Gap.lg()) .next(settings) - content.add(JBScrollPane(body).apply { - border = null - horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER - }, BorderLayout.CENTER) + if (pad && scroll) body.border = JBUI.Borders.emptyRight(UiStyle.Gap.xl()) + if (scroll) { + content.add(JBScrollPane(body).apply { + border = null + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + }, BorderLayout.CENTER) + } else { + content.add(body, BorderLayout.CENTER) + } + if (pad) { + content.border = JBUI.Borders.empty( + UiStyle.Gap.pad(), + UiStyle.Gap.xl(), + UiStyle.Gap.xl(), + if (scroll) 0 else UiStyle.Gap.xl(), + ) + } } fun setContent(component: JComponent) { @@ -33,6 +51,22 @@ internal open class SettingsPanel : SettingsOverlayPanel() { repaint() } + fun setHeader(component: JComponent) { + val header = JPanel(BorderLayout()) + header.isOpaque = false + header.border = JBUI.Borders.emptyRight(UiStyle.Gap.xl()) + header.add(component, BorderLayout.CENTER) + content.add(header, BorderLayout.NORTH) + } + + protected fun setCenter(component: JComponent) { + val layout = content.layout as? BorderLayout + layout?.getLayoutComponent(BorderLayout.CENTER)?.let { content.remove(it) } + content.add(component, BorderLayout.CENTER) + revalidate() + repaint() + } + } private class SettingsBody : Stack(StackAxis.VERTICAL), Scrollable { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPathDialog.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPathDialog.kt new file mode 100644 index 00000000000..9d330951ed5 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPathDialog.kt @@ -0,0 +1,44 @@ +package ai.kilocode.client.settings.base + +import com.intellij.CommonBundle +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.components.JBTextField +import javax.swing.JComponent + +/** Testable handle over [SettingsPathDialog] so callers can stub the modal dialog in tests. */ +internal interface SettingsPathDialogHandle { + fun showAndGet(): Boolean + fun value(): String +} + +/** + * Single-line text entry dialog for a path, glob, or URL. When [browse] is provided the field gains + * the standard "..." file-chooser button; otherwise it is a plain text field. Confirms with "OK" + * (the value is persisted later by the owning settings page) and focuses the field on open. + */ +internal class SettingsPathDialog( + title: String, + value: String = "", + private val browse: ((JComponent) -> String?)? = null, +) : DialogWrapper(true), SettingsPathDialogHandle { + private val field = JBTextField(value) + + init { + this.title = title + setOKButtonText(CommonBundle.getOkButtonText()) + init() + } + + override fun createCenterPanel(): JComponent { + field.columns = COLUMNS + return browse?.let { settingsPathInput(field, it) } ?: field + } + + override fun getPreferredFocusedComponent(): JComponent = field + + override fun value(): String = field.text + + private companion object { + const val COLUMNS = 60 + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPathInput.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPathInput.kt new file mode 100644 index 00000000000..f123a32d08a --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/base/SettingsPathInput.kt @@ -0,0 +1,21 @@ +package ai.kilocode.client.settings.base + +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.components.JBTextField +import javax.swing.JComponent + +internal fun settingsPathInput( + field: JBTextField, + choose: (JComponent) -> String?, +): TextFieldWithBrowseButton = TextFieldWithBrowseButton(field).apply { + addActionListener { + choose(this)?.let { field.text = it } + } +} + +internal fun settingsChoosePath(parent: JComponent, descriptor: FileChooserDescriptor): String? { + return FileChooser.chooseFile(descriptor, parent, null, null as VirtualFile?)?.path +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt index eaeed04cd66..79130ab3421 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt @@ -3,6 +3,7 @@ package ai.kilocode.client.settings.profile import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.telemetry.Telemetry +import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.util.UiTimerSource import ai.kilocode.client.util.UiTimers import ai.kilocode.rpc.dto.KiloAppStateDto @@ -16,6 +17,7 @@ import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.asContextElement import com.intellij.openapi.components.service import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBUI import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -84,6 +86,7 @@ internal class ProfileUi( private var shown: Card? = null init { + border = JBUI.Borders.empty(0, UiStyle.Gap.xl(), 0, UiStyle.Gap.xl()) cards.add(out, Card.LOGGED_OUT.name) cards.add(account, Card.LOGGED_IN.name) add(cards, BorderLayout.NORTH) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUi.kt index a5f77ce0028..edb3d216992 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUi.kt @@ -150,7 +150,7 @@ internal class ProvidersSettingsUi( private var oauth: DeviceOAuthPanel? = null init { - content.add(header(), BorderLayout.NORTH) + setHeader(header()) setContent(view) reload() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/rules/RulesConfigurable.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/rules/RulesConfigurable.kt new file mode 100644 index 00000000000..e5df92acbf0 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/rules/RulesConfigurable.kt @@ -0,0 +1,20 @@ +package ai.kilocode.client.settings.rules + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.base.DraftReadyConfigurable +import kotlinx.coroutines.CoroutineScope +import javax.swing.JComponent + +class RulesConfigurable : DraftReadyConfigurable() { + override fun getId(): String = ID + + override fun getDisplayName(): String = KiloBundle.message("settings.agentBehavior.rules.displayName") + + override fun create(cs: CoroutineScope): JComponent = RulesSettingsUi(cs, root = project?.basePath) + + override fun scrollReadyShell() = false + + companion object { + const val ID = "ai.kilocode.jetbrains.settings.agentBehavior.rules" + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/rules/RulesSettingsState.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/rules/RulesSettingsState.kt new file mode 100644 index 00000000000..a489ccf5fd2 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/rules/RulesSettingsState.kt @@ -0,0 +1,40 @@ +package ai.kilocode.client.settings.rules + +import ai.kilocode.rpc.dto.ConfigDto +import ai.kilocode.rpc.dto.ConfigPatchDto + +internal data class RulesDraft( + val instructions: List = emptyList(), + val compat: Boolean = false, + /** Staged file-content edits, keyed by instruction path, written to disk on apply. */ + val edited: Map = emptyMap(), +) + +internal data class RulesChange( + val config: ConfigPatchDto? = null, + val compat: Boolean? = null, + val edited: Map = emptyMap(), +) + +internal fun rulesDraft(config: ConfigDto?, compat: Boolean): RulesDraft = RulesDraft( + instructions = config?.instructions ?: emptyList(), + compat = compat, +) + +internal fun configPatch(from: RulesDraft, to: RulesDraft): ConfigPatchDto? { + if (from.instructions == to.instructions) return null + return ConfigPatchDto(instructions = to.instructions) +} + +internal fun rulesChange(from: RulesDraft, to: RulesDraft): RulesChange? { + val config = configPatch(from, to) + val compat = to.compat.takeIf { it != from.compat } + val edited = to.edited + if (config == null && compat == null && edited.isEmpty()) return null + return RulesChange(config, compat, edited) +} + +// Structural equality: the baseline always carries an empty [RulesDraft.edited], so any staged +// content edit makes the draft unequal (and therefore modified). This symmetric form is required by +// SettingsDraftState.complete, which also uses it to compare returned/target drafts. +internal fun savedMatches(base: RulesDraft, draft: RulesDraft): Boolean = base == draft diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUi.kt new file mode 100644 index 00000000000..89bc6e57e16 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUi.kt @@ -0,0 +1,367 @@ +package ai.kilocode.client.settings.rules + +import ai.kilocode.client.KiloNotifications +import ai.kilocode.client.app.KiloAgentBehaviorService +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.settings.base.SettingsContentField +import ai.kilocode.client.settings.base.SettingsDraftPage +import ai.kilocode.client.settings.base.SettingsDraftState +import ai.kilocode.client.settings.base.SettingsListCell +import ai.kilocode.client.settings.base.SettingsListConfig +import ai.kilocode.client.settings.base.SettingsListItem +import ai.kilocode.client.settings.base.SettingsListPanel +import ai.kilocode.client.settings.base.SettingsListSelection +import ai.kilocode.client.settings.base.SettingsRow +import ai.kilocode.client.settings.base.SettingsToggle +import ai.kilocode.client.settings.base.SettingsToolbarAction +import ai.kilocode.client.settings.base.SettingsPathDialog +import ai.kilocode.client.settings.base.settingsChoosePath +import ai.kilocode.client.settings.base.settingsContentScroll +import ai.kilocode.client.settings.base.settingsEditorFileType +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.layout.Stack +import ai.kilocode.client.ui.layout.StackAxis +import ai.kilocode.log.KiloLog +import ai.kilocode.rpc.dto.KiloAppStateDto +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.components.service +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.ui.TitledSeparator +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBUI +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.awt.BorderLayout +import java.nio.charset.StandardCharsets +import java.nio.file.InvalidPathException +import java.nio.file.Path +import javax.swing.JComponent +import javax.swing.ScrollPaneConstants + +private val edt = Dispatchers.EDT + ModalityState.any().asContextElement() + +internal class RulesSettingsUi( + scope: CoroutineScope, + private val root: String? = null, + private val choose: (JComponent) -> String? = ::chooseRulePath, + private val input: () -> String? = { promptRulePath(choose) }, + private val read: (String) -> String? = { path -> readInstruction(root, path) }, + private val write: (String, String) -> Boolean = { path, text -> writeInstruction(root, path, text) }, + private val editor: (String, String) -> RuleContentDialogHandle = { title, content -> InstructionEditDialog(title, content) }, + private val app: KiloAppService = service(), + private val workspaces: KiloWorkspaceService = service(), + private val agent: KiloAgentBehaviorService = service(), +) : SettingsListPanel(scope, SettingsListConfig.Equal.copy(tooltip = false)), SettingsDraftPage { + private val cs = scope + private val state = SettingsDraftState(rulesDraft(app.state.value.config, false), ::savedMatches) + private val draft get() = state.draft + private var closed = false + internal val footer = RulesFooterView { value -> updateCompat(value) } + + init { + start() + setCenter(ruleScroll()) + content.add(footer, BorderLayout.SOUTH) + reload() + } + + override suspend fun fetch(): List { + val compat = agent.claudeCodeCompat() + return withContext(edt) { + state.accept(rulesDraft(app.state.value.config, compat)) + footer.refresh(draft.compat) + rows() + } + } + + override fun onCell(key: String, cellId: String) { + when (cellId) { + OPEN_CELL -> open(key) + EDIT_CELL -> editFile(key) + DELETE_CELL -> remove(key) + } + } + + override fun extraActions(): List = listOf( + SettingsToolbarAction( + KiloBundle.message("settings.rules.files.add"), + KiloBundle.message("settings.rules.files.add.description"), + AllIcons.General.Add, + { !busy }, + ) { addFile() }, + ) + + override fun showRefresh(): Boolean = false + + override fun searchPlaceholder() = KiloBundle.message("settings.rules.files.search") + + override fun emptyText() = KiloBundle.message("settings.rules.files.empty") + + override fun modified(): Boolean = state.modified() + + override fun resetDraft() { + state.reset() + footer.refresh(draft.compat) + view.update(rows()) + clearProgress() + } + + override fun applyDraft() { + val change = rulesChange(state.baseline, draft) ?: return + val token = state.start() ?: return + val target = token.target + showProgress(KiloBundle.message("settings.rules.save.pending")) + setBusy(true) + app.scope.launch { + val wrote = withContext(edt) { target.edited.all { (path, text) -> write(path, text) } } + val next = when { + !wrote -> null + change.config != null -> app.updateConfig(change.config) + else -> app.state.value + } + val ok = next != null && (change.compat == null || agent.setClaudeCodeCompat(change.compat) == change.compat) + withContext(edt) { finish(token, target, next.takeIf { ok }) } + } + } + + @RequiresEdt + override fun dispose() { + closed = true + super.dispose() + } + + @RequiresEdt + private fun finish(token: ai.kilocode.client.settings.base.SettingsDraftSave, target: RulesDraft, next: KiloAppStateDto?) { + if (closed) { + if (next != null) KiloNotifications.info(KiloBundle.message("settings.rules.saved.notification")) + else KiloNotifications.error(KiloBundle.message("settings.rules.save.failed")) + return + } + if (next != null) { + state.complete(token, rulesDraft(next.config, target.compat)) + LOG.info("rules settings apply succeeded") + } else { + state.fail(token, KiloBundle.message("settings.rules.save.failed")) + showError(KiloBundle.message("settings.rules.save.failed")) + LOG.warn("rules settings apply failed") + } + footer.refresh(draft.compat) + view.update(rows()) + if (next != null) clearProgress() + setBusy(false) + } + + internal fun addFile() { + val value = input()?.trim()?.takeIf { it.isNotBlank() } ?: return + if (value in draft.instructions) { + view.select(value) + return + } + state.update { copy(instructions = instructions + value) } + view.update(rows(), SettingsListSelection.Key(value)) + } + + private fun editFile(path: String) { + val content = draft.edited[path] ?: read(path) + if (content == null) { + KiloNotifications.info(KiloBundle.message("settings.rules.files.cannotEdit")) + return + } + val dialog = editor(path, content) + if (!dialog.showAndGet()) return + state.update { copy(edited = edited + (path to dialog.content())) } + view.update(rows(), SettingsListSelection.Key(path)) + } + + private fun remove(path: String) { + val result = Messages.showYesNoDialog( + KiloBundle.message("settings.rules.files.delete.message", path), + KiloBundle.message("settings.rules.files.delete.title"), + KiloBundle.message("common.delete"), + Messages.getCancelButton(), + Messages.getQuestionIcon(), + ) + if (result != Messages.YES) return + state.update { copy(instructions = instructions - path, edited = edited - path) } + view.update(rows(), selectionIndex()) + } + + private fun open(path: String) { + val abs = resolveInstructionPath(root, path) + if (abs == null) { + KiloNotifications.error(KiloBundle.message("settings.rules.files.openInEditor.failed")) + return + } + showProgress(KiloBundle.message("settings.rules.files.openInEditor.pending")) + cs.launch { + val opened = workspaces.openFile(abs) + withContext(edt) { + if (closed) return@withContext + clearProgress() + if (!opened) KiloNotifications.error(KiloBundle.message("settings.rules.files.openInEditor.failed")) + } + } + } + + private fun updateCompat(value: Boolean) { + state.update { copy(compat = value) } + footer.refresh(draft.compat) + } + + private fun ruleScroll() = JBScrollPane(view).apply { + border = null + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER + verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED + } + + private fun rows(): List = draft.instructions.map { item(it) } + + private fun item(value: String) = object : SettingsListItem { + override val key = value + override val title = value + override val doubleClick = EDIT_CELL + override val cells = listOf( + SettingsListCell( + OPEN_CELL, + KiloBundle.message("settings.rules.files.openInEditor"), + primary = true, + ), + SettingsListCell( + EDIT_CELL, + KiloBundle.message("settings.agentBehavior.edit"), + ), + SettingsListCell( + DELETE_CELL, + KiloBundle.message("common.delete"), + icon = AllIcons.Actions.GC, + iconOnly = true, + ), + ) + } + + private companion object { + const val OPEN_CELL = "open" + const val EDIT_CELL = "edit" + const val DELETE_CELL = "delete" + val LOG = KiloLog.create(RulesSettingsUi::class.java) + } +} + +internal class RulesFooterView( + private val update: (Boolean) -> Unit, +) : Stack(StackAxis.VERTICAL, UiStyle.Gap.sm()) { + private val compat = SettingsToggle { value -> update(value) } + + init { + border = JBUI.Borders.empty(UiStyle.Gap.pad(), 0, 0, UiStyle.Gap.xl()) + next(TitledSeparator(KiloBundle.message("settings.rules.claude.heading"))) + next(SettingsRow( + KiloBundle.message("settings.rules.claude.title"), + KiloBundle.message("settings.rules.claude.description"), + compat, + )) + } + + @RequiresEdt + fun refresh(value: Boolean) { + compat.isSelected = value + } +} + +internal interface RuleContentDialogHandle { + fun showAndGet(): Boolean + fun content(): String +} + +/** In-dialog content editor for an instruction file, mirroring the Skills skill editor. */ +internal class InstructionEditDialog( + private val heading: String, + content: String, +) : DialogWrapper(true), RuleContentDialogHandle { + private val base = content + private val field = SettingsContentField(base, settingsEditorFileType(heading, base), true) + + init { + title = heading + setOKButtonText(com.intellij.CommonBundle.getOkButtonText()) + init() + isOKActionEnabled = false + field.document.addDocumentListener(object : com.intellij.openapi.editor.event.DocumentListener { + override fun documentChanged(event: com.intellij.openapi.editor.event.DocumentEvent) { + isOKActionEnabled = field.text != base + } + }) + } + + override fun createCenterPanel(): JComponent = settingsContentScroll(field) + + override fun content() = field.text +} + +private fun chooseRulePath(parent: JComponent): String? = settingsChoosePath(parent, rulePathDescriptor()) + +private fun promptRulePath(choose: (JComponent) -> String?): String? { + val dialog = SettingsPathDialog(KiloBundle.message("settings.rules.files.input.title"), browse = choose) + return if (dialog.showAndGet()) dialog.value() else null +} + +internal fun rulePathDescriptor() = FileChooserDescriptor(true, false, false, false, false, false).apply { + title = KiloBundle.message("settings.rules.files.input.title") + description = KiloBundle.message("settings.rules.files.input.prompt") +} + +private fun resolveInstructionPath(root: String?, path: String): String? = try { + val nio = Path.of(path.trim()) + when { + nio.isAbsolute -> nio.normalize().toString() + root != null -> Path.of(root).resolve(nio).normalize().toString() + else -> null + } +} catch (e: InvalidPathException) { + null +} + +@RequiresEdt +private fun readInstruction(root: String?, path: String): String? { + val abs = resolveInstructionPath(root, path) ?: return null + val vf = LocalFileSystem.getInstance().findFileByPath(abs) + ?: LocalFileSystem.getInstance().refreshAndFindFileByPath(abs) + ?: return null + if (vf.isDirectory) return null + return String(vf.contentsToByteArray(), StandardCharsets.UTF_8) +} + +@RequiresEdt +private fun writeInstruction(root: String?, path: String, text: String): Boolean { + val abs = resolveInstructionPath(root, path) ?: return false + var ok = false + WriteCommandAction.runWriteCommandAction(null as Project?) { + val nio = Path.of(abs) + val lfs = LocalFileSystem.getInstance() + val target = lfs.refreshAndFindFileByPath(abs) ?: run { + val parent = nio.parent ?: return@runWriteCommandAction + val dir = VfsUtil.createDirectoryIfMissing(parent.toString()) ?: return@runWriteCommandAction + dir.createChildData(RulesSettingsUi::class.java, nio.fileName.toString()) + } + VfsUtil.saveText(target, text) + ok = true + } + return ok +} + + diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/DiffStatBadge.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/DiffStatBadge.kt index 7f0fb7bd6e2..8f47080a59e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/DiffStatBadge.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/DiffStatBadge.kt @@ -17,11 +17,11 @@ internal class DiffStatBadge( deletions: Int, ) : JPanel(GridBagLayout()) { private val removed = JBLabel().apply { - foreground = removedColor() + foreground = UiStyle.Colors.removedForeground() font = JBFont.small() } private val added = JBLabel().apply { - foreground = addedColor() + foreground = UiStyle.Colors.addedForeground() font = JBFont.small() } @@ -62,13 +62,3 @@ private fun backgroundColor(): Color = JBColor.namedColor( "Kilo.DiffStat.background", JBColor(Color(0x26, 0x26, 0x26), Color(0x26, 0x26, 0x26)), ) - -private fun removedColor(): Color = JBColor.namedColor( - "Kilo.DiffStat.removedForeground", - JBColor(Color(0xdb, 0x58, 0x66), Color(0xff, 0x6b, 0x7a)), -) - -private fun addedColor(): Color = JBColor.namedColor( - "Kilo.DiffStat.addedForeground", - JBColor(Color(0x1f, 0x9d, 0x66), Color(0x35, 0xd4, 0x9a)), -) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt index d5292520043..7e711204661 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/UiStyle.kt @@ -148,6 +148,16 @@ object UiStyle { fun errorLabelForeground(): Color = JBColor.namedColor("Label.errorForeground", UIUtil.getErrorForeground()) + fun addedForeground(): Color = JBColor.namedColor( + "Kilo.DiffStat.addedForeground", + JBColor(Color(0x1f, 0x9d, 0x66), Color(0x35, 0xd4, 0x9a)), + ) + + fun removedForeground(): Color = JBColor.namedColor( + "Kilo.DiffStat.removedForeground", + JBColor(Color(0xdb, 0x58, 0x66), Color(0xff, 0x6b, 0x7a)), + ) + fun warningLabelForeground(): Color = JBColor.lazy { UIManager.getColor("Component.warningFocusColor") ?: UIManager.getColor("Label.warningForeground") diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/editor/BashCommandHighlighter.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/editor/BashCommandHighlighter.kt new file mode 100644 index 00000000000..ee0e78f28e9 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/editor/BashCommandHighlighter.kt @@ -0,0 +1,75 @@ +package ai.kilocode.client.ui.editor + +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea + +internal data class BashRange(val start: Int, val end: Int, val key: TextAttributesKey) + +internal data class BashDisplay(val text: String, val ranges: List) + +internal object BashCommandHighlighter { + private val cmd = Regex("(?m)(^\\s*(?:\\$\\s*)?|[|&;]\\s*)([A-Za-z_./~][A-Za-z0-9_./~+-]*)") + private val flag = Regex("(?= end) continue + editor.markupModel.addRangeHighlighter( + range.key, + start, + end, + HighlighterLayer.SYNTAX + 1, + HighlighterTargetArea.EXACT_RANGE, + ) + } + } + + fun ranges(text: String): List = buildList { + val spans = spans(text) + cmd.findAll(text).forEach { match -> + val group = match.groups[2] ?: return@forEach + if (!contains(spans, group.range.first, group.range.last + 1)) return@forEach + add(BashRange(group.range.first, group.range.last + 1, DefaultLanguageHighlighterColors.KEYWORD)) + } + flag.findAll(text).forEach { match -> + if (!contains(spans, match.range.first, match.range.last + 1)) return@forEach + add(BashRange(match.range.first, match.range.last + 1, DefaultLanguageHighlighterColors.KEYWORD)) + } + string.findAll(text).forEach { match -> + if (!contains(spans, match.range.first, match.range.last + 1)) return@forEach + add(BashRange(match.range.first, match.range.last + 1, DefaultLanguageHighlighterColors.STRING)) + } + env.findAll(text).forEach { match -> + val group = match.groups[2] ?: return@forEach + if (!contains(spans, group.range.first, group.range.last + 1)) return@forEach + add(BashRange(group.range.first, group.range.last + 1, DefaultLanguageHighlighterColors.STATIC_FIELD)) + } + } + + private fun spans(text: String): List { + val prompts = prompt.findAll(text).toList() + if (prompts.isEmpty()) return listOf(0 until text.length) + return prompts.map { match -> + val end = text.indexOf('\n', match.range.last + 1).let { if (it == -1) text.length else it } + (match.range.last + 1) until end + } + } + + private fun contains(spans: List, start: Int, end: Int): Boolean { + return spans.any { start >= it.first && end <= it.last + 1 } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdDiffHighlight.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdDiffHighlight.kt new file mode 100644 index 00000000000..f7d2c3a3d0e --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdDiffHighlight.kt @@ -0,0 +1,90 @@ +package ai.kilocode.client.ui.md.hybrid + +import com.intellij.openapi.diff.DiffColors +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea + +/** + * Overlays unified-diff coloring on a plain-text code editor: added lines get the theme's diff + * "inserted" background, removed lines the "deleted" background, hunk headers a keyword color, and + * file/index headers a dimmed comment color. Colors come from the active scheme via [DiffColors] + * and [DefaultLanguageHighlighterColors], so the result tracks the IDE theme like the diff viewer. + */ +internal object MdDiffHighlight { + data class Span(val key: TextAttributesKey, val area: HighlighterTargetArea) + data class Display(val text: String, val spans: List) + data class Range(val start: Int, val end: Int, val span: Span) + + fun apply(editor: EditorEx, text: String) { + editor.markupModel.removeAllHighlighters() + val doc = editor.document + val size = doc.textLength + for (n in 0 until doc.lineCount) { + val start = doc.getLineStartOffset(n).coerceAtMost(size) + val end = doc.getLineEndOffset(n).coerceAtMost(size) + if (start >= end) continue + val span = classify(doc.charsSequence.subSequence(start, end).toString()) ?: continue + editor.markupModel.addRangeHighlighter(span.key, start, end, HighlighterLayer.SYNTAX + 1, span.area) + } + } + + fun applyPure(editor: EditorEx, text: String) { + editor.markupModel.removeAllHighlighters() + val doc = editor.document + for (range in display(text).spans) { + val start = range.start.coerceAtMost(doc.textLength) + val end = range.end.coerceAtMost(doc.textLength) + if (start >= end) continue + editor.markupModel.addRangeHighlighter(range.span.key, start, end, HighlighterLayer.SYNTAX + 1, range.span.area) + } + } + + fun display(text: String): Display { + val out = StringBuilder() + val ranges = mutableListOf() + text.lineSequence().forEachIndexed { i, line -> + if (i > 0) out.append('\n') + val span = classify(line) + val body = when { + line.startsWith("+") || line.startsWith("-") || line.startsWith(" ") -> line.drop(1) + else -> line + } + val start = out.length + out.append(body) + if (span != null) ranges.add(Range(start, out.length, span)) + } + return Display(out.toString(), ranges) + } + + private fun classify(line: String): Span? = when { + fileHeader(line) || meta(line) -> comment + line.startsWith("@@") -> hunk + line.startsWith("+") -> inserted + line.startsWith("-") -> deleted + else -> null + } + + // Unified-diff file headers are the marker followed by a space (or the bare marker), e.g. "+++ b/f". + // Guarding on that shape keeps content lines like "++x;" (an inserted "+x;") from being dimmed. + private fun fileHeader(line: String): Boolean = + (line.startsWith("+++") || line.startsWith("---")) && + (line.length == 3 || line[3] == ' ' || line[3] == '\t') + + private fun meta(line: String): Boolean = line.startsWith("diff ") || + line.startsWith("index ") || + line.startsWith("Index:") || + line.startsWith("===") || + line.startsWith("new file") || + line.startsWith("deleted file") || + line.startsWith("rename ") || + line.startsWith("similarity ") || + line.startsWith("\\ No newline") + + private val inserted = Span(DiffColors.DIFF_INSERTED, HighlighterTargetArea.LINES_IN_RANGE) + private val deleted = Span(DiffColors.DIFF_DELETED, HighlighterTargetArea.LINES_IN_RANGE) + private val hunk = Span(DefaultLanguageHighlighterColors.KEYWORD, HighlighterTargetArea.EXACT_RANGE) + private val comment = Span(DefaultLanguageHighlighterColors.LINE_COMMENT, HighlighterTargetArea.EXACT_RANGE) +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdLanguage.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdLanguage.kt index 4acb36e8d12..3be503139e0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdLanguage.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdLanguage.kt @@ -6,7 +6,7 @@ import com.intellij.openapi.fileTypes.PlainTextFileType import com.intellij.openapi.fileTypes.UnknownFileType internal sealed class Kind { - data class Source(val file: FileType) : Kind() + data class Source(val file: FileType, val highlight: Highlight = Highlight.None) : Kind() data class Terminal(val stream: Stream, val mode: Mode) : Kind() } @@ -14,6 +14,9 @@ internal enum class Stream { Stdout, Stderr } internal enum class Mode { Ansi, Shell, Command } +/** Extra overlay highlighting applied on top of a source code block. */ +internal enum class Highlight { None, Diff, DiffPure } + internal object MdLanguage { /** Internal terminal fence tags produced by ShellToolView shell transcript markdown. */ private val terms = mapOf( @@ -21,6 +24,10 @@ internal object MdLanguage { "ansi-stdout" to Kind.Terminal(Stream.Stdout, Mode.Ansi), "terminal" to Kind.Terminal(Stream.Stdout, Mode.Ansi), "terminal-output" to Kind.Terminal(Stream.Stdout, Mode.Ansi), + "bash" to Kind.Terminal(Stream.Stdout, Mode.Command), + "shell" to Kind.Terminal(Stream.Stdout, Mode.Command), + "zsh" to Kind.Terminal(Stream.Stdout, Mode.Command), + "shellscript" to Kind.Terminal(Stream.Stdout, Mode.Command), "shell-command" to Kind.Terminal(Stream.Stdout, Mode.Command), "shell-output" to Kind.Terminal(Stream.Stdout, Mode.Shell), "ansi-stderr" to Kind.Terminal(Stream.Stderr, Mode.Ansi), @@ -34,10 +41,6 @@ internal object MdLanguage { "javascript" to "js", "typescript" to "ts", "python" to "py", - "bash" to "sh", - "shell" to "sh", - "zsh" to "sh", - "shellscript" to "sh", "markdown" to "md", "yml" to "yaml", "golang" to "go", @@ -58,11 +61,16 @@ internal object MdLanguage { "terraform" to "tf", ) + private val diffs = setOf("diff", "patch", "udiff") + private val pure = setOf("diff-pure", "patch-pure") + fun kind(lang: String?): Kind { val key = lang?.trim()?.split(Regex("\\s+"))?.take(2)?.joinToString(" ")?.lowercase().orEmpty() terms[key]?.let { return it } if (key == "shell script") return Kind.Source(type("sh")) val single = key.substringBefore(' ') + if (key in pure || single in pure) return Kind.Source(PlainTextFileType.INSTANCE, Highlight.DiffPure) + if (key in diffs || single in diffs) return Kind.Source(PlainTextFileType.INSTANCE, Highlight.Diff) terms[single]?.let { return it } files[key]?.let { return Kind.Source(type(it)) } files[single]?.let { return Kind.Source(type(it)) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdShellHighlight.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdShellHighlight.kt index e1754aaf63a..3f01eb2eb84 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdShellHighlight.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdShellHighlight.kt @@ -1,7 +1,11 @@ package ai.kilocode.client.ui.md.hybrid +import ai.kilocode.client.ui.editor.BashCommandHighlighter import com.intellij.openapi.editor.DefaultLanguageHighlighterColors import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.editor.markup.HighlighterTargetArea internal data class ShellRange(val start: Int, val end: Int, val key: TextAttributesKey) @@ -19,11 +23,6 @@ internal object MdShellHighlight { private val deletions = Regex("\\b\\d+ deletions?\\(-\\)") private val meta = Regex("(?m)^<(?:shell_metadata|/shell_metadata)>$") private val cut = Regex("(?m)^\\.\\.\\.output truncated\\.\\.\\.$") - private val cmd = Regex("(?m)(^|[|&;]\\s*)([A-Za-z_./~][A-Za-z0-9_./~+-]*)") - private val flag = Regex("(?() var grouped = false @@ -41,7 +40,28 @@ internal object MdShellHighlight { return ShellDisplay(display, ranges(display)) } - fun command(text: String) = ShellDisplay(text, commandRanges(text)) + fun command(text: String) = BashCommandHighlighter.display(text).let { display -> + ShellDisplay(display.text, display.ranges.map { ShellRange(it.start, it.end, it.key) }) + } + + fun apply(editor: EditorEx, display: ShellDisplay) { + editor.markupModel.removeAllHighlighters() + val size = editor.document.textLength + for (range in display.ranges) { + val start = range.start.coerceAtMost(size) + val end = range.end.coerceAtMost(size) + if (start >= end) continue + editor.markupModel.addRangeHighlighter( + range.key, + start, + end, + HighlighterLayer.SYNTAX + 1, + HighlighterTargetArea.EXACT_RANGE, + ) + } + } + + fun applyCommand(editor: EditorEx, text: String) = BashCommandHighlighter.apply(editor, text) fun ranges(text: String): List = buildList { fun add(regex: Regex, key: TextAttributesKey) { @@ -59,21 +79,4 @@ internal object MdShellHighlight { add(meta, DefaultLanguageHighlighterColors.DOC_COMMENT) add(cut, DefaultLanguageHighlighterColors.KEYWORD) } - - private fun commandRanges(text: String): List = buildList { - cmd.findAll(text).forEach { match -> - val group = match.groups[2] ?: return@forEach - add(ShellRange(group.range.first, group.range.last + 1, DefaultLanguageHighlighterColors.FUNCTION_CALL)) - } - flag.findAll(text).forEach { match -> - add(ShellRange(match.range.first, match.range.last + 1, DefaultLanguageHighlighterColors.KEYWORD)) - } - string.findAll(text).forEach { match -> - add(ShellRange(match.range.first, match.range.last + 1, DefaultLanguageHighlighterColors.STRING)) - } - env.findAll(text).forEach { match -> - val group = match.groups[2] ?: return@forEach - add(ShellRange(group.range.first, group.range.last + 1, DefaultLanguageHighlighterColors.STATIC_FIELD)) - } - } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdViewHybrid.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdViewHybrid.kt index e3d04e8fc80..3453cad9153 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdViewHybrid.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/ui/md/hybrid/MdViewHybrid.kt @@ -314,7 +314,7 @@ internal open class MdViewHybrid( is Desc.Html -> HtmlView(desc, htmlBlock(desc.body, disposable), disposable) is Desc.Table -> TableView(desc, tableBlock(desc.body, disposable), disposable) is Desc.Code -> when (val kind = desc.kind) { - is Kind.Source -> CodeView(desc, codeBlock(desc.text, kind.file, disposable), disposable) + is Kind.Source -> CodeView(desc, codeBlock(desc.text, kind, disposable), disposable) is Kind.Terminal -> TermView(desc, terminalBlock(desc.text, kind, disposable), disposable) } } @@ -331,27 +331,38 @@ internal open class MdViewHybrid( customStyleSheetProvider { sheet() } }, ), UiDataProvider { + // A stationary pointer over scrolling content must keep this pane's hovered link and + // cursor fresh, so we replay a synthetic mouse move whenever the enclosing viewport + // scrolls. Only the pane under the pointer subscribes — otherwise every prose block in a + // large transcript would run a native pointer query + event dispatch on every scroll tick. private var viewport: JViewport? = null + private var listening = false private val scroll = ChangeListener { hover() } + private val pointer = object : java.awt.event.MouseAdapter() { + override fun mouseEntered(e: MouseEvent) = listen(true) + override fun mouseExited(e: MouseEvent) = listen(false) + } private val hierarchy = java.awt.event.HierarchyListener { event -> - if (event.changeFlags and HierarchyEvent.PARENT_CHANGED.toLong() != 0L) attach() + if (event.changeFlags and HierarchyEvent.PARENT_CHANGED.toLong() != 0L) retarget() } init { + addMouseListener(pointer) addHierarchyListener(hierarchy) Disposer.register(disposable) { - viewport?.removeChangeListener(scroll) + listen(false) + removeMouseListener(pointer) removeHierarchyListener(hierarchy) } } override fun addNotify() { super.addNotify() - attach() + retarget() } override fun removeNotify() { - viewport?.removeChangeListener(scroll) + listen(false) viewport = null super.removeNotify() } @@ -360,12 +371,20 @@ internal open class MdViewHybrid( selection?.provideCopy(sink) { document.getText(0, document.length).trim() } } - private fun attach() { + // Follow the enclosing viewport as this pane is reparented, keeping any live subscription. + private fun retarget() { val next = SwingUtilities.getAncestorOfClass(JViewport::class.java, this) as? JViewport if (viewport === next) return - viewport?.removeChangeListener(scroll) + if (listening) viewport?.removeChangeListener(scroll) viewport = next - next?.addChangeListener(scroll) + if (listening) viewport?.addChangeListener(scroll) + } + + // Track viewport scrolls only while the pointer is over this pane. + private fun listen(on: Boolean) { + if (listening == on) return + listening = on + if (on) viewport?.addChangeListener(scroll) else viewport?.removeChangeListener(scroll) } private fun hover() { @@ -424,20 +443,20 @@ internal open class MdViewHybrid( return pane } - private fun codeBlock(text: String, file: FileType, disposable: Disposable): JBScrollPane { + private fun codeBlock(text: String, kind: Kind.Source, disposable: Disposable): JBScrollPane { val opts = opts() - val value = text.trimEnd('\n') + val value = sourceText(text, kind) val field = runCatching { - codeField(file, opts, text, false, disposable) + codeField(kind.file, opts, value, false, disposable) }.getOrElse { err -> LOG.warn("kind=markdown codeEditor=true failed message=${err.message}", err) if (code.opts.editorOnly) runCatching { - codeField(PlainTextFileType.INSTANCE, opts, text, false, disposable) + codeField(PlainTextFileType.INSTANCE, opts, value, false, disposable) }.getOrElse { fallback -> LOG.warn("kind=markdown codeEditor=true fallback=plain failed message=${fallback.message}", fallback) throw fallback } else { - textArea(text, opts, disposable) + textArea(value, opts, disposable) } } sizeCodeField(field, value) @@ -451,6 +470,12 @@ internal open class MdViewHybrid( return pane } + private fun sourceText(text: String, kind: Kind.Source): String { + val value = text.trimEnd('\n') + if (kind.highlight == Highlight.DiffPure) return MdDiffHighlight.display(value).text + return value + } + private fun terminalBlock(text: String, kind: Kind.Terminal, disposable: Disposable): JBScrollPane { val opts = opts() val term = MdTerminal.decode(text, kind.stream) @@ -718,19 +743,7 @@ internal open class MdViewHybrid( private fun applyShell(field: CodeField, display: ShellDisplay) { val editor = field.getEditor(false) ?: return - val size = editor.document.textLength - for (range in display.ranges) { - val start = range.start.coerceAtMost(size) - val end = range.end.coerceAtMost(size) - if (start >= end) continue - editor.markupModel.addRangeHighlighter( - range.key, - start, - end, - HighlighterLayer.SYNTAX + 1, - HighlighterTargetArea.EXACT_RANGE, - ) - } + MdShellHighlight.apply(editor, display) } private fun dispatch(event: MdView.LinkEvent) { @@ -835,12 +848,18 @@ internal open class MdViewHybrid( private inner class CodeView(desc: Desc.Code, private val pane: JBScrollPane, disposable: Disposable) : View(desc, pane, disposable) { + init { + overlay() + } + override fun compatible(desc: Desc) = desc is Desc.Code && (this.desc as Desc.Code).kind == desc.kind override fun update(desc: Desc) { if (this.desc == desc) return this.desc = desc - val value = (desc as Desc.Code).text.trimEnd('\n') + val item = desc as Desc.Code + val kind = item.kind as? Kind.Source + val value = if (kind == null) item.text.trimEnd('\n') else sourceText(item.text, kind) val view = pane.viewport.view when (view) { is CodeField -> view.text = value @@ -850,6 +869,20 @@ internal open class MdViewHybrid( sizeCodeField(view, value) sizeCodePane(pane, view) } + overlay() + } + + /** Applies unified-diff coloring on top of a `diff`/`patch` block; a no-op otherwise. */ + private fun overlay() { + val kind = (desc as Desc.Code).kind + if (kind !is Kind.Source || kind.highlight == Highlight.None) return + val field = pane.viewport.view as? CodeField ?: return + val editor = field.getEditor(true) ?: return + if (kind.highlight == Highlight.DiffPure) { + MdDiffHighlight.applyPure(editor, (desc as Desc.Code).text.trimEnd('\n')) + return + } + MdDiffHighlight.apply(editor, field.text) } override fun grow(delta: String) { @@ -873,6 +906,7 @@ internal open class MdViewHybrid( sizeCodeField(view, text) sizeCodePane(pane, view) } + overlay() } } diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small-active.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small-active.svg new file mode 100644 index 00000000000..5456a7768d9 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small-active_dark.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small-active_dark.svg new file mode 100644 index 00000000000..4e9c204836d --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small-active_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small.svg new file mode 100644 index 00000000000..aad4492699d --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small_dark.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small_dark.svg new file mode 100644 index 00000000000..cb40cf1481c --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/check-small_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small-active.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small-active.svg new file mode 100644 index 00000000000..75cf9101650 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small-active.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small-active_dark.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small-active_dark.svg new file mode 100644 index 00000000000..5c73fe97915 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small-active_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small.svg new file mode 100644 index 00000000000..9a56b9bdc72 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small_dark.svg b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small_dark.svg new file mode 100644 index 00000000000..274744271cd --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/icons/views/close-small_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml index a498f01ab45..57788370bd6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml +++ b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml @@ -65,11 +65,19 @@ + + + + + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 9ee25b97e6a..2557da487ff 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -1,4 +1,6 @@ common.delete=Delete +common.open=Open +common.save=Save session.action.cancel=Cancel session.connection.connecting=Loading... @@ -50,8 +52,19 @@ session.permission.title=Permission required session.permission.title.subagent=Permission required (subagent) session.permission.meta=Tool: {0} • Patterns: {1} session.permission.run=Run +session.permission.ask=Ask session.permission.allow=Allow session.permission.deny=Deny +session.permission.allow.once=Allow once +session.permission.reject=Reject +session.permission.rules.title=Auto-approve Rules +session.permission.rule.approve.add=Add to allowed +session.permission.rule.approve.remove=Remove from allowed +session.permission.rule.deny.add=Add to denied +session.permission.rule.deny.remove=Remove from denied +session.permission.rule.hint.default=Future matching calls will use the default permission setting: {0}. +session.permission.rule.hint.approve=This request and future matching calls will be allowed. +session.permission.rule.hint.deny=This request and future matching calls will be rejected. session.permission.command=Command session.permission.patterns={0}: session.permission.diff=Changes @@ -113,6 +126,9 @@ session.part.tool.error=Error session.part.tool.agent={0} Agent session.part.tool.pending=Pending session.part.tool.read=Read +session.part.tool.edit=Edit +session.part.tool.edit.files={0} files +session.part.tool.patch=Patch session.part.tool.glob=Glob session.part.tool.search=Search session.part.tool.running=Running @@ -324,6 +340,43 @@ settings.context.watcher.remove=Remove selected patterns settings.context.watcher.empty=No ignore patterns configured. settings.context.watcher.input.title=Add ignore pattern settings.context.watcher.input.prompt=Enter a glob pattern to ignore: +settings.autoApprove.displayName=Auto-Approve +settings.autoApprove.title=Auto-Approve +settings.autoApprove.description=Define how tools are allowed to run. Most tools default to Allow. doom_loop and external_directory default to Ask. +settings.autoApprove.default=Default ({0}) +settings.autoApprove.level.allow=Allow +settings.autoApprove.level.ask=Ask +settings.autoApprove.level.deny=Deny +settings.autoApprove.filter=Filter auto-approve rules +settings.autoApprove.wildcardLabel.commands=All commands (*) +settings.autoApprove.wildcardLabel.paths=All paths (*) +settings.autoApprove.filters.empty.commands=No custom commands +settings.autoApprove.filters.empty.paths=No custom paths +settings.autoApprove.tools.empty=No tools +settings.autoApprove.add=Add +settings.autoApprove.edit=Edit +settings.autoApprove.delete=Delete +settings.autoApprove.delete.description=Delete selected +settings.autoApprove.addCommand=Add command +settings.autoApprove.addPath=Add path +settings.autoApprove.placeholder.command=e.g. git * +settings.autoApprove.placeholder.path=e.g. *.env +settings.autoApprove.tool.external_directory=Access files outside workspace. Triggered when accessing files outside the current project directory. +settings.autoApprove.tool.bash=Run terminal commands. Allows execution of shell commands (e.g., git status). +settings.autoApprove.tool.read=Read files. Allows the agent to read files matching the specified path. +settings.autoApprove.tool.edit=Modify files. Allows the agent to create or edit files, including patches and multi-file updates. +settings.autoApprove.tool.glob=Match files by pattern. Allows file matching using glob patterns (e.g., src/**/*.ts). +settings.autoApprove.tool.grep=Search file contents. Allows regex-based search inside files. +settings.autoApprove.tool.list=List directory contents. Allows viewing files and folders within a directory. +settings.autoApprove.tool.task=Launch sub-agents. Allows starting specialized sub-agents for specific tasks. +settings.autoApprove.tool.skill=Load skills. Allows loading predefined skills by name. +settings.autoApprove.tool.lsp=Query language server. Allows running language server queries for code intelligence. +settings.autoApprove.tool.todoreadwrite=Manage task list. Allows reading and updating the internal task list. +settings.autoApprove.tool.webfetch=Fetch a URL. Allows retrieving content from a specific URL. +settings.autoApprove.tool.websearch=Search the web. Allows performing external web searches. +settings.autoApprove.tool.doom_loop=Prevent repeated identical actions. Triggered when the same tool call repeats with identical input. +settings.autoApprove.save.pending=Saving auto-approve settings… +settings.autoApprove.save.failed=Failed to save auto-approve settings. settings.providers.displayName=Providers settings.agentBehavior.displayName=Agent Behavior settings.agentBehavior.description=Configure agents, MCP servers, rules, workflows, and skills. @@ -457,6 +510,53 @@ settings.agentBehavior.mcp.status.failed=failed settings.agentBehavior.mcp.status.needsAuth=needs auth settings.agentBehavior.mcp.status.needsRegistration=needs registration settings.agentBehavior.mcp.status.disabled=disabled +settings.agentBehavior.skills.displayName=Skills +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Filter skills +settings.agentBehavior.skills.empty=No skills found. +settings.agentBehavior.skills.content.empty=No skill content available. +settings.agentBehavior.skills.load.timeout=Skill loading timed out. Existing skills were kept; remove slow or unreachable URLs and refresh. +settings.agentBehavior.skills.reload.deferred=Skills source saved. Reload the core after active sessions finish to apply new skills. +settings.agentBehavior.skills.reload.blocked=Skills settings saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills. +settings.agentBehavior.skills.saved.notification=Skills settings saved +settings.agentBehavior.skills.delete.title=Delete Skill +settings.agentBehavior.skills.delete.message=Delete skill {0}? This removes the skill file and cannot be undone. +settings.agentBehavior.skills.delete.failed=Could not delete the skill. +settings.agentBehavior.skills.openInEditor=Open in Editor +settings.agentBehavior.skills.openInEditor.pending=The skill file will open after you close Settings. +settings.agentBehavior.skills.openInEditor.failed=Could not open the skill file in the editor. +settings.agentBehavior.skills.sources.empty=No skill sources configured. +settings.agentBehavior.skills.sources.title=Additional Skill Sources +settings.agentBehavior.skills.sources.add=Add +settings.agentBehavior.skills.sources.addPath=Add path +settings.agentBehavior.skills.sources.addPath.title=Add Skill Path +settings.agentBehavior.skills.sources.addPath.prompt=Choose a folder containing Kilo skills. +settings.agentBehavior.skills.sources.addUrl=Add URL +settings.agentBehavior.skills.sources.addUrl.title=Add Skill URL +settings.agentBehavior.skills.sources.addUrl.prompt=Enter a skill source URL. +settings.agentBehavior.skills.sources.editPath.title=Edit Skill Path +settings.agentBehavior.skills.sources.editUrl.title=Edit Skill URL settings.providers.loading=Loading providers... settings.providers.connected=Connected providers settings.providers.available=Available providers diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties index 30e2d7bd0ed..f439e041589 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -1,4 +1,6 @@ common.delete=حذف +common.open=فتح +common.save=حفظ session.action.cancel=إلغاء session.connection.connecting=جاري التحميل… session.connection.error.app=فشل الاتصال @@ -28,6 +30,10 @@ session.permission.title=طلب إذن session.permission.meta=الأداة: {0} • الأنماط: {1} session.permission.allow=سماح session.permission.deny=رفض +session.permission.ask=اسأل +session.permission.rule.hint.default=ستستخدم الاستدعاءات المطابقة مستقبلاً إعداد الإذن الافتراضي: {0}. +session.permission.rule.hint.approve=سيتم السماح بهذا الطلب وبالاستدعاءات المطابقة مستقبلاً. +session.permission.rule.hint.deny=سيتم رفض هذا الطلب والاستدعاءات المطابقة مستقبلاً. session.question.dismiss=إغلاق session.status.considering=جار التفكير في الخطوات التالية… @@ -51,6 +57,7 @@ session.part.tool.copy=نسخ session.part.tool.error=خطأ session.part.tool.pending=معلق session.part.tool.read=قراءة +session.part.tool.edit=تحرير session.part.tool.running=قيد التشغيل session.part.tool.shell=Shell session.part.tool.truncated=المخرجات مختصرة في المعاينة المسبقة. المخرجات الكاملة لا تزال في بيانات الجلسة. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=لإضافة خادم MCP، اطلب من الوكيل إضافته. +settings.agentBehavior.skills.displayName=المهارات +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=تصفية المهارات +settings.agentBehavior.skills.empty=لم يتم العثور على مهارات. +settings.agentBehavior.skills.content.empty=لا يوجد محتوى مهارة متاح. +settings.agentBehavior.skills.load.timeout=انتهت مهلة تحميل المهارات. تم الاحتفاظ بالمهارات الحالية؛ أزل عناوين URL البطيئة أو غير المتاحة ثم حدّث. +settings.agentBehavior.skills.reload.deferred=تم حفظ مصدر المهارات. أعد تحميل Core بعد انتهاء الجلسات النشطة لتطبيق المهارات الجديدة. +settings.agentBehavior.skills.reload.blocked=تم حفظ إعدادات المهارات، لكن توجد جلسات نشطة. أعد تحميل Core بعد انتهاء تلك الجلسات لتطبيق المهارات الجديدة. +settings.agentBehavior.skills.saved.notification=تم حفظ إعدادات المهارات +settings.agentBehavior.skills.delete.title=حذف المهارة +settings.agentBehavior.skills.delete.message=هل تريد حذف المهارة {0}؟ سيؤدي ذلك إلى إزالة ملف المهارة ولا يمكن التراجع عنه. +settings.agentBehavior.skills.delete.failed=تعذر حذف المهارة. +settings.agentBehavior.skills.openInEditor=فتح في المحرر +settings.agentBehavior.skills.openInEditor.pending=سيتم فتح ملف المهارة بعد إغلاق الإعدادات. +settings.agentBehavior.skills.openInEditor.failed=تعذر فتح ملف المهارة في المحرر. +settings.agentBehavior.skills.sources.empty=لم يتم تكوين مصادر مهارات. +settings.agentBehavior.skills.sources.title=مصادر مهارات إضافية +settings.agentBehavior.skills.sources.add=إضافة +settings.agentBehavior.skills.sources.addPath=إضافة مسار +settings.agentBehavior.skills.sources.addPath.title=إضافة مسار مهارات +settings.agentBehavior.skills.sources.addPath.prompt=اختر مجلداً يحتوي على مهارات Kilo. +settings.agentBehavior.skills.sources.addUrl=إضافة URL +settings.agentBehavior.skills.sources.addUrl.title=إضافة URL لمصدر مهارات +settings.agentBehavior.skills.sources.addUrl.prompt=أدخل URL لمصدر مهارات. +settings.agentBehavior.skills.sources.editPath.title=تعديل مسار المهارات +settings.agentBehavior.skills.sources.editUrl.title=تعديل URL المهارات session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=تم التراجع عن رسالة واحدة @@ -399,3 +453,6 @@ settings.context.watcher.remove=إزالة الأنماط المحددة settings.context.watcher.empty=لم يتم تكوين أنماط تجاهل. settings.context.watcher.input.title=إضافة نمط تجاهل settings.context.watcher.input.prompt=أدخل نمط glob لتجاهله: + +# Auto-Approve settings +settings.autoApprove.edit=تحرير diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties index 85685d47df6..3c3fe1f71a9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -1,4 +1,6 @@ common.delete=Obriši +common.open=Otvori +common.save=Sačuvaj session.action.cancel=Otkaži session.connection.connecting=Učitavanje… session.connection.error.app=Greška pri spajanju @@ -28,6 +30,10 @@ session.permission.title=Zahtjev za dozvolu session.permission.meta=Alat: {0} • Uzorci: {1} session.permission.allow=Dozvoli session.permission.deny=Odbij +session.permission.ask=Pitaj +session.permission.rule.hint.default=Budući podudarni pozivi koristit će zadanu postavku dozvole: {0}. +session.permission.rule.hint.approve=Ovaj zahtjev i budući podudarni pozivi bit će dozvoljeni. +session.permission.rule.hint.deny=Ovaj zahtjev i budući podudarni pozivi bit će odbijeni. session.question.dismiss=Zatvori session.status.considering=Razmatranje sljedećih koraka… @@ -51,6 +57,7 @@ session.part.tool.copy=Kopiraj session.part.tool.error=Greška session.part.tool.pending=Na čekanju session.part.tool.read=Čita +session.part.tool.edit=Uredi session.part.tool.running=Pokrenuto session.part.tool.shell=Shell session.part.tool.truncated=Izlaz skraćen u pregledu. Potpuni izlaz ostaje u podacima sesije. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Da dodate MCP server, zamolite agenta da ga doda. +settings.agentBehavior.skills.displayName=Vještine +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Filtriraj vještine +settings.agentBehavior.skills.empty=Nema pronađenih vještina. +settings.agentBehavior.skills.content.empty=Nema dostupnog sadržaja vještine. +settings.agentBehavior.skills.load.timeout=Učitavanje vještina je isteklo. Postojeće vještine su zadržane; uklonite spore ili nedostupne URL-ove i osvježite. +settings.agentBehavior.skills.reload.deferred=Izvor vještina je sačuvan. Ponovo učitajte Core nakon što aktivne sesije završe da primijenite nove vještine. +settings.agentBehavior.skills.reload.blocked=Postavke vještina su sačuvane, ali postoje aktivne sesije. Ponovo učitajte Core nakon što te sesije završe da primijenite nove vještine. +settings.agentBehavior.skills.saved.notification=Postavke vještina su sačuvane +settings.agentBehavior.skills.delete.title=Izbriši vještinu +settings.agentBehavior.skills.delete.message=Izbrisati vještinu {0}? Ovo uklanja datoteku vještine i ne može se poništiti. +settings.agentBehavior.skills.delete.failed=Nije moguće izbrisati vještinu. +settings.agentBehavior.skills.openInEditor=Otvori u editoru +settings.agentBehavior.skills.openInEditor.pending=Datoteka vještine će se otvoriti nakon što zatvorite Postavke. +settings.agentBehavior.skills.openInEditor.failed=Nije moguće otvoriti datoteku vještine u editoru. +settings.agentBehavior.skills.sources.empty=Nema konfigurisanih izvora vještina. +settings.agentBehavior.skills.sources.title=Dodatni izvori vještina +settings.agentBehavior.skills.sources.add=Dodaj +settings.agentBehavior.skills.sources.addPath=Dodaj putanju +settings.agentBehavior.skills.sources.addPath.title=Dodaj putanju vještina +settings.agentBehavior.skills.sources.addPath.prompt=Odaberite folder koji sadrži Kilo vještine. +settings.agentBehavior.skills.sources.addUrl=Dodaj URL +settings.agentBehavior.skills.sources.addUrl.title=Dodaj URL izvora vještina +settings.agentBehavior.skills.sources.addUrl.prompt=Unesite URL izvora vještina. +settings.agentBehavior.skills.sources.editPath.title=Uredi putanju vještina +settings.agentBehavior.skills.sources.editUrl.title=Uredi URL vještina session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} poruka vraćena @@ -399,3 +453,6 @@ settings.context.watcher.remove=Ukloni odabrane uzorke settings.context.watcher.empty=Nema konfigurisanih uzoraka za ignoriranje. settings.context.watcher.input.title=Dodaj uzorak za ignoriranje settings.context.watcher.input.prompt=Unesite glob uzorak za ignoriranje: + +# Auto-Approve settings +settings.autoApprove.edit=Uredi diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties index 00f9ccc62c5..8d0d26a7223 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -1,4 +1,6 @@ common.delete=Slet +common.open=Åbn +common.save=Gem session.action.cancel=Annuller session.connection.connecting=Indlæser… session.connection.error.app=Forbindelsesfejl @@ -28,6 +30,10 @@ session.permission.title=Tilladelsesanmodning session.permission.meta=Værktøj: {0} • Mønstre: {1} session.permission.allow=Tillad session.permission.deny=Afvis +session.permission.ask=Spørg +session.permission.rule.hint.default=Fremtidige matchende kald bruger standardtilladelsen: {0}. +session.permission.rule.hint.approve=Denne anmodning og fremtidige matchende kald bliver tilladt. +session.permission.rule.hint.deny=Denne anmodning og fremtidige matchende kald bliver afvist. session.question.dismiss=Luk session.status.considering=Overvejer næste skridt… @@ -51,6 +57,7 @@ session.part.tool.copy=Kopiér session.part.tool.error=Fejl session.part.tool.pending=Afventer session.part.tool.read=Læs +session.part.tool.edit=Rediger session.part.tool.running=Kører session.part.tool.shell=Shell session.part.tool.truncated=Output afkortet i forhåndsvisning. Fuldt output forbliver i sessionsdata. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=For at tilføje en MCP-server skal du bede agenten om at gøre det. +settings.agentBehavior.skills.displayName=Færdigheder +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Filtrer færdigheder +settings.agentBehavior.skills.empty=Ingen færdigheder fundet. +settings.agentBehavior.skills.content.empty=Intet færdighedsindhold tilgængeligt. +settings.agentBehavior.skills.load.timeout=Indlæsning af færdigheder fik timeout. Eksisterende færdigheder blev bevaret; fjern langsomme eller utilgængelige URL'er og opdater. +settings.agentBehavior.skills.reload.deferred=Færdighedskilden blev gemt. Genindlæs Core, når aktive sessioner er afsluttet, for at anvende nye færdigheder. +settings.agentBehavior.skills.reload.blocked=Færdighedsindstillingerne blev gemt, men der er aktive sessioner. Genindlæs Core, når disse sessioner er afsluttet, for at anvende de nye færdigheder. +settings.agentBehavior.skills.saved.notification=Færdighedsindstillinger gemt +settings.agentBehavior.skills.delete.title=Slet færdighed +settings.agentBehavior.skills.delete.message=Slet færdigheden {0}? Dette fjerner færdighedsfilen og kan ikke fortrydes. +settings.agentBehavior.skills.delete.failed=Kunne ikke slette færdigheden. +settings.agentBehavior.skills.openInEditor=Åbn i editor +settings.agentBehavior.skills.openInEditor.pending=Færdighedsfilen åbnes, når du lukker Indstillinger. +settings.agentBehavior.skills.openInEditor.failed=Kunne ikke åbne færdighedsfilen i editoren. +settings.agentBehavior.skills.sources.empty=Ingen færdighedskilder konfigureret. +settings.agentBehavior.skills.sources.title=Yderligere færdighedskilder +settings.agentBehavior.skills.sources.add=Tilføj +settings.agentBehavior.skills.sources.addPath=Tilføj sti +settings.agentBehavior.skills.sources.addPath.title=Tilføj færdighedssti +settings.agentBehavior.skills.sources.addPath.prompt=Vælg en mappe, der indeholder Kilo-færdigheder. +settings.agentBehavior.skills.sources.addUrl=Tilføj URL +settings.agentBehavior.skills.sources.addUrl.title=Tilføj URL til færdighedskilde +settings.agentBehavior.skills.sources.addUrl.prompt=Indtast en URL til en færdighedskilde. +settings.agentBehavior.skills.sources.editPath.title=Rediger færdighedssti +settings.agentBehavior.skills.sources.editUrl.title=Rediger færdigheds-URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} besked rullet tilbage @@ -399,3 +453,6 @@ settings.context.watcher.remove=Fjern valgte mønstre settings.context.watcher.empty=Ingen ignormønstre konfigureret. settings.context.watcher.input.title=Tilføj ignormønster settings.context.watcher.input.prompt=Indtast et glob-mønster, der skal ignoreres: + +# Auto-Approve settings +settings.autoApprove.edit=Rediger diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties index 3f0d1cec7ad..181ac9f1654 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -1,4 +1,6 @@ common.delete=Löschen +common.open=Öffnen +common.save=Speichern session.action.cancel=Abbrechen session.connection.connecting=Wird geladen… session.connection.error.app=Verbindung fehlgeschlagen @@ -28,6 +30,10 @@ session.permission.title=Berechtigungsanfrage session.permission.meta=Werkzeug: {0} • Muster: {1} session.permission.allow=Erlauben session.permission.deny=Ablehnen +session.permission.ask=Nachfragen +session.permission.rule.hint.default=Zukünftige passende Aufrufe verwenden die Standard-Berechtigungseinstellung: {0}. +session.permission.rule.hint.approve=Diese Anfrage und zukünftige passende Aufrufe werden erlaubt. +session.permission.rule.hint.deny=Diese Anfrage und zukünftige passende Aufrufe werden abgelehnt. session.question.dismiss=Schließen session.status.considering=Nächste Schritte abwägen… @@ -51,6 +57,7 @@ session.part.tool.copy=Kopieren session.part.tool.error=Fehler session.part.tool.pending=Ausstehend session.part.tool.read=Lesen +session.part.tool.edit=Bearbeiten session.part.tool.running=Läuft session.part.tool.shell=Shell session.part.tool.truncated=Ausgabe in der Vorschau gekürzt. Vollständige Ausgabe verbleibt in den Sitzungsdaten. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Um einen MCP-Server hinzuzufügen, bitten Sie den Agenten darum. +settings.agentBehavior.skills.displayName=Skills +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Skills filtern +settings.agentBehavior.skills.empty=Keine Skills gefunden. +settings.agentBehavior.skills.content.empty=Keine Skill-Inhalte verfügbar. +settings.agentBehavior.skills.load.timeout=Das Laden der Skills ist abgelaufen. Vorhandene Skills wurden beibehalten; entfernen Sie langsame oder nicht erreichbare URLs und aktualisieren Sie. +settings.agentBehavior.skills.reload.deferred=Skill-Quelle gespeichert. Laden Sie Core neu, nachdem aktive Sitzungen beendet sind, um neue Skills anzuwenden. +settings.agentBehavior.skills.reload.blocked=Skill-Einstellungen gespeichert, aber es sind aktive Sitzungen vorhanden. Laden Sie Core neu, nachdem diese Sitzungen beendet sind, um neue Skills anzuwenden. +settings.agentBehavior.skills.saved.notification=Skill-Einstellungen gespeichert +settings.agentBehavior.skills.delete.title=Skill löschen +settings.agentBehavior.skills.delete.message=Skill {0} löschen? Dadurch wird die Skill-Datei entfernt und kann nicht rückgängig gemacht werden. +settings.agentBehavior.skills.delete.failed=Der Skill konnte nicht gelöscht werden. +settings.agentBehavior.skills.openInEditor=Im Editor öffnen +settings.agentBehavior.skills.openInEditor.pending=Die Skill-Datei wird geöffnet, nachdem Sie die Einstellungen geschlossen haben. +settings.agentBehavior.skills.openInEditor.failed=Die Skill-Datei konnte nicht im Editor geöffnet werden. +settings.agentBehavior.skills.sources.empty=Keine Skill-Quellen konfiguriert. +settings.agentBehavior.skills.sources.title=Zusätzliche Skill-Quellen +settings.agentBehavior.skills.sources.add=Hinzufügen +settings.agentBehavior.skills.sources.addPath=Pfad hinzufügen +settings.agentBehavior.skills.sources.addPath.title=Skill-Pfad hinzufügen +settings.agentBehavior.skills.sources.addPath.prompt=Wählen Sie einen Ordner mit Kilo-Skills aus. +settings.agentBehavior.skills.sources.addUrl=URL hinzufügen +settings.agentBehavior.skills.sources.addUrl.title=Skill-Quellen-URL hinzufügen +settings.agentBehavior.skills.sources.addUrl.prompt=Geben Sie eine Skill-Quellen-URL ein. +settings.agentBehavior.skills.sources.editPath.title=Skill-Pfad bearbeiten +settings.agentBehavior.skills.sources.editUrl.title=Skill-URL bearbeiten session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} Nachricht zurückgesetzt @@ -399,3 +453,6 @@ settings.context.watcher.remove=Ausgewählte Muster entfernen settings.context.watcher.empty=Keine Ignorierungsmuster konfiguriert. settings.context.watcher.input.title=Ignorierungsmuster hinzufügen settings.context.watcher.input.prompt=Glob-Muster zum Ignorieren eingeben: + +# Auto-Approve settings +settings.autoApprove.edit=Bearbeiten diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties index 071f529430a..fb67a0de551 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -1,4 +1,6 @@ common.delete=Eliminar +common.open=Abrir +common.save=Guardar session.action.cancel=Cancelar session.connection.connecting=Cargando… session.connection.error.app=Error de conexión @@ -28,6 +30,10 @@ session.permission.title=Solicitud de permiso session.permission.meta=Herramienta: {0} • Patrones: {1} session.permission.allow=Permitir session.permission.deny=Denegar +session.permission.ask=Preguntar +session.permission.rule.hint.default=Las llamadas coincidentes futuras usarán la configuración de permiso predeterminada: {0}. +session.permission.rule.hint.approve=Esta solicitud y las llamadas coincidentes futuras se permitirán. +session.permission.rule.hint.deny=Esta solicitud y las llamadas coincidentes futuras se rechazarán. session.question.dismiss=Descartar session.status.considering=Considerando los próximos pasos… @@ -51,6 +57,7 @@ session.part.tool.copy=Copiar session.part.tool.error=Error session.part.tool.pending=Pendiente session.part.tool.read=Leer +session.part.tool.edit=Editar session.part.tool.running=Ejecutando session.part.tool.shell=Shell session.part.tool.truncated=Salida truncada en la vista previa. La salida completa permanece en los datos de la sesión. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Para agregar un servidor MCP, pídele al agente que lo haga. +settings.agentBehavior.skills.displayName=Habilidades +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Filtrar habilidades +settings.agentBehavior.skills.empty=No se encontraron habilidades. +settings.agentBehavior.skills.content.empty=No hay contenido de habilidad disponible. +settings.agentBehavior.skills.load.timeout=Se agotó el tiempo de carga de habilidades. Se conservaron las habilidades existentes; elimina las URL lentas o inaccesibles y actualiza. +settings.agentBehavior.skills.reload.deferred=Fuente de habilidades guardada. Recarga Core cuando terminen las sesiones activas para aplicar nuevas habilidades. +settings.agentBehavior.skills.reload.blocked=Configuración de habilidades guardada, pero hay sesiones activas. Recarga Core cuando esas sesiones terminen para aplicar las nuevas habilidades. +settings.agentBehavior.skills.saved.notification=Configuración de habilidades guardada +settings.agentBehavior.skills.delete.title=Eliminar habilidad +settings.agentBehavior.skills.delete.message=¿Eliminar la habilidad {0}? Esto elimina el archivo de la habilidad y no se puede deshacer. +settings.agentBehavior.skills.delete.failed=No se pudo eliminar la habilidad. +settings.agentBehavior.skills.openInEditor=Abrir en el editor +settings.agentBehavior.skills.openInEditor.pending=El archivo de la habilidad se abrirá después de cerrar Configuración. +settings.agentBehavior.skills.openInEditor.failed=No se pudo abrir el archivo de la habilidad en el editor. +settings.agentBehavior.skills.sources.empty=No hay fuentes de habilidades configuradas. +settings.agentBehavior.skills.sources.title=Fuentes de habilidades adicionales +settings.agentBehavior.skills.sources.add=Agregar +settings.agentBehavior.skills.sources.addPath=Agregar ruta +settings.agentBehavior.skills.sources.addPath.title=Agregar ruta de habilidades +settings.agentBehavior.skills.sources.addPath.prompt=Elige una carpeta que contenga habilidades de Kilo. +settings.agentBehavior.skills.sources.addUrl=Agregar URL +settings.agentBehavior.skills.sources.addUrl.title=Agregar URL de fuente de habilidades +settings.agentBehavior.skills.sources.addUrl.prompt=Introduce una URL de fuente de habilidades. +settings.agentBehavior.skills.sources.editPath.title=Editar ruta de habilidades +settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} mensaje revertido @@ -399,3 +453,6 @@ settings.context.watcher.remove=Eliminar patrones seleccionados settings.context.watcher.empty=No hay patrones de ignorar configurados. settings.context.watcher.input.title=Agregar patrón de ignorar settings.context.watcher.input.prompt=Introduce un patrón glob para ignorar: + +# Auto-Approve settings +settings.autoApprove.edit=Editar diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties index 508507d2787..3848ae7ee98 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -1,4 +1,6 @@ common.delete=Supprimer +common.open=Ouvrir +common.save=Enregistrer session.action.cancel=Annuler session.connection.connecting=Chargement… session.connection.error.app=Échec de la connexion @@ -28,6 +30,10 @@ session.permission.title=Demande de permission session.permission.meta=Outil : {0} • Modèles : {1} session.permission.allow=Autoriser session.permission.deny=Refuser +session.permission.ask=Demander +session.permission.rule.hint.default=Les futurs appels correspondants utiliseront le paramètre de permission par défaut : {0}. +session.permission.rule.hint.approve=Cette demande et les futurs appels correspondants seront autorisés. +session.permission.rule.hint.deny=Cette demande et les futurs appels correspondants seront refusés. session.question.dismiss=Ignorer session.status.considering=Considération des prochaines étapes… @@ -51,6 +57,7 @@ session.part.tool.copy=Copier session.part.tool.error=Erreur session.part.tool.pending=En attente session.part.tool.read=Lire +session.part.tool.edit=Modifier session.part.tool.running=En cours session.part.tool.shell=Shell session.part.tool.truncated=Sortie tronquée dans l'aperçu. La sortie complète reste dans les données de session. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Pour ajouter un serveur MCP, demandez à l’agent de le faire. +settings.agentBehavior.skills.displayName=Compétences +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Filtrer les compétences +settings.agentBehavior.skills.empty=Aucune compétence trouvée. +settings.agentBehavior.skills.content.empty=Aucun contenu de compétence disponible. +settings.agentBehavior.skills.load.timeout=Le chargement des compétences a expiré. Les compétences existantes ont été conservées ; supprimez les URL lentes ou inaccessibles puis actualisez. +settings.agentBehavior.skills.reload.deferred=Source de compétences enregistrée. Rechargez Core après la fin des sessions actives pour appliquer les nouvelles compétences. +settings.agentBehavior.skills.reload.blocked=Paramètres des compétences enregistrés, mais des sessions sont actives. Rechargez Core après la fin de ces sessions pour appliquer les nouvelles compétences. +settings.agentBehavior.skills.saved.notification=Paramètres des compétences enregistrés +settings.agentBehavior.skills.delete.title=Supprimer la compétence +settings.agentBehavior.skills.delete.message=Supprimer la compétence {0} ? Cela supprime le fichier de compétence et ne peut pas être annulé. +settings.agentBehavior.skills.delete.failed=Impossible de supprimer la compétence. +settings.agentBehavior.skills.openInEditor=Ouvrir dans l’éditeur +settings.agentBehavior.skills.openInEditor.pending=Le fichier de compétence s’ouvrira après la fermeture des paramètres. +settings.agentBehavior.skills.openInEditor.failed=Impossible d’ouvrir le fichier de compétence dans l’éditeur. +settings.agentBehavior.skills.sources.empty=Aucune source de compétences configurée. +settings.agentBehavior.skills.sources.title=Sources de compétences supplémentaires +settings.agentBehavior.skills.sources.add=Ajouter +settings.agentBehavior.skills.sources.addPath=Ajouter un chemin +settings.agentBehavior.skills.sources.addPath.title=Ajouter un chemin de compétences +settings.agentBehavior.skills.sources.addPath.prompt=Choisissez un dossier contenant des compétences Kilo. +settings.agentBehavior.skills.sources.addUrl=Ajouter une URL +settings.agentBehavior.skills.sources.addUrl.title=Ajouter une URL de source de compétences +settings.agentBehavior.skills.sources.addUrl.prompt=Saisissez une URL de source de compétences. +settings.agentBehavior.skills.sources.editPath.title=Modifier le chemin des compétences +settings.agentBehavior.skills.sources.editUrl.title=Modifier l’URL des compétences session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} message annulé @@ -399,3 +453,6 @@ settings.context.watcher.remove=Supprimer les motifs sélectionnés settings.context.watcher.empty=Aucun motif d’ignorance configuré. settings.context.watcher.input.title=Ajouter un motif d’ignorance settings.context.watcher.input.prompt=Saisissez un motif glob à ignorer : + +# Auto-Approve settings +settings.autoApprove.edit=Modifier diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties index 6f6424b4b4a..8c9e3d12c06 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -1,4 +1,6 @@ common.delete=削除 +common.open=開く +common.save=保存 session.action.cancel=キャンセル session.connection.connecting=読み込み中… session.connection.error.app=接続に失敗しました @@ -28,6 +30,10 @@ session.permission.title=権限リクエスト session.permission.meta=ツール:{0} • パターン:{1} session.permission.allow=許可 session.permission.deny=拒否 +session.permission.ask=確認 +session.permission.rule.hint.default=今後一致する呼び出しにはデフォルトの権限設定が使用されます: {0}。 +session.permission.rule.hint.approve=このリクエストと今後一致する呼び出しは許可されます。 +session.permission.rule.hint.deny=このリクエストと今後一致する呼び出しは拒否されます。 session.question.dismiss=閉じる session.status.considering=次のステップを検討中… @@ -51,6 +57,7 @@ session.part.tool.copy=コピー session.part.tool.error=エラー session.part.tool.pending=保留中 session.part.tool.read=読み取り +session.part.tool.edit=編集 session.part.tool.running=実行中 session.part.tool.shell=シェル session.part.tool.truncated=プレビューでは出力が切り詰められています。完全な出力はセッションデータに残っています。 @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=MCPサーバーを追加するには、エージェントに依頼してください。 +settings.agentBehavior.skills.displayName=スキル +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=スキルを絞り込み +settings.agentBehavior.skills.empty=スキルが見つかりません。 +settings.agentBehavior.skills.content.empty=利用可能なスキル内容がありません。 +settings.agentBehavior.skills.load.timeout=スキルの読み込みがタイムアウトしました。既存のスキルは保持されました。遅い、または到達不能な URL を削除して更新してください。 +settings.agentBehavior.skills.reload.deferred=スキルソースを保存しました。新しいスキルを適用するには、アクティブなセッションが終了した後に Core を再読み込みしてください。 +settings.agentBehavior.skills.reload.blocked=スキル設定を保存しましたが、アクティブなセッションがあります。新しいスキルを適用するには、それらのセッションが終了した後に Core を再読み込みしてください。 +settings.agentBehavior.skills.saved.notification=スキル設定を保存しました +settings.agentBehavior.skills.delete.title=スキルを削除 +settings.agentBehavior.skills.delete.message=スキル {0} を削除しますか?スキルファイルが削除され、この操作は元に戻せません。 +settings.agentBehavior.skills.delete.failed=スキルを削除できませんでした。 +settings.agentBehavior.skills.openInEditor=エディターで開く +settings.agentBehavior.skills.openInEditor.pending=設定を閉じるとスキルファイルが開きます。 +settings.agentBehavior.skills.openInEditor.failed=エディターでスキルファイルを開けませんでした。 +settings.agentBehavior.skills.sources.empty=スキルソースが設定されていません。 +settings.agentBehavior.skills.sources.title=追加のスキルソース +settings.agentBehavior.skills.sources.add=追加 +settings.agentBehavior.skills.sources.addPath=パスを追加 +settings.agentBehavior.skills.sources.addPath.title=スキルパスを追加 +settings.agentBehavior.skills.sources.addPath.prompt=Kilo スキルを含むフォルダーを選択してください。 +settings.agentBehavior.skills.sources.addUrl=URL を追加 +settings.agentBehavior.skills.sources.addUrl.title=スキルソース URL を追加 +settings.agentBehavior.skills.sources.addUrl.prompt=スキルソース URL を入力してください。 +settings.agentBehavior.skills.sources.editPath.title=スキルパスを編集 +settings.agentBehavior.skills.sources.editUrl.title=スキル URL を編集 session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} 件のメッセージをロールバックしました @@ -399,3 +453,6 @@ settings.context.watcher.remove=選択したパターンを削除 settings.context.watcher.empty=無視パターンは設定されていません。 settings.context.watcher.input.title=無視パターンを追加 settings.context.watcher.input.prompt=無視するglobパターンを入力してください: + +# Auto-Approve settings +settings.autoApprove.edit=編集 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties index 9f33c300ad9..b463a9b1951 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -1,4 +1,6 @@ common.delete=삭제 +common.open=열기 +common.save=저장 session.action.cancel=취소 session.connection.connecting=로딩 중… session.connection.error.app=연결 실패 @@ -28,6 +30,10 @@ session.permission.title=권한 요청 session.permission.meta=도구: {0} • 패턴: {1} session.permission.allow=허용 session.permission.deny=거부 +session.permission.ask=묻기 +session.permission.rule.hint.default=향후 일치하는 호출은 기본 권한 설정을 사용합니다: {0}. +session.permission.rule.hint.approve=이 요청과 향후 일치하는 호출은 허용됩니다. +session.permission.rule.hint.deny=이 요청과 향후 일치하는 호출은 거부됩니다. session.question.dismiss=닫기 session.status.considering=다음 단계 고려 중… @@ -51,6 +57,7 @@ session.part.tool.copy=복사 session.part.tool.error=오류 session.part.tool.pending=대기 중 session.part.tool.read=읽기 +session.part.tool.edit=편집 session.part.tool.running=실행 중 session.part.tool.shell=셸 session.part.tool.truncated=미리보기에서 출력이 잘렸습니다. 전체 출력은 세션 데이터에 남아 있습니다. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=MCP 서버를 추가하려면 에이전트에게 요청하세요. +settings.agentBehavior.skills.displayName=스킬 +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=스킬 필터링 +settings.agentBehavior.skills.empty=스킬을 찾을 수 없습니다. +settings.agentBehavior.skills.content.empty=사용 가능한 스킬 내용이 없습니다. +settings.agentBehavior.skills.load.timeout=스킬 로드 시간이 초과되었습니다. 기존 스킬은 유지되었습니다. 느리거나 연결할 수 없는 URL을 제거한 뒤 새로 고치세요. +settings.agentBehavior.skills.reload.deferred=스킬 소스가 저장되었습니다. 새 스킬을 적용하려면 활성 세션이 끝난 뒤 Core를 다시 로드하세요. +settings.agentBehavior.skills.reload.blocked=스킬 설정이 저장되었지만 활성 세션이 있습니다. 새 스킬을 적용하려면 해당 세션이 끝난 뒤 Core를 다시 로드하세요. +settings.agentBehavior.skills.saved.notification=스킬 설정이 저장되었습니다 +settings.agentBehavior.skills.delete.title=스킬 삭제 +settings.agentBehavior.skills.delete.message=스킬 {0}을 삭제할까요? 스킬 파일이 제거되며 되돌릴 수 없습니다. +settings.agentBehavior.skills.delete.failed=스킬을 삭제할 수 없습니다. +settings.agentBehavior.skills.openInEditor=에디터에서 열기 +settings.agentBehavior.skills.openInEditor.pending=설정을 닫으면 스킬 파일이 열립니다. +settings.agentBehavior.skills.openInEditor.failed=에디터에서 스킬 파일을 열 수 없습니다. +settings.agentBehavior.skills.sources.empty=구성된 스킬 소스가 없습니다. +settings.agentBehavior.skills.sources.title=추가 스킬 소스 +settings.agentBehavior.skills.sources.add=추가 +settings.agentBehavior.skills.sources.addPath=경로 추가 +settings.agentBehavior.skills.sources.addPath.title=스킬 경로 추가 +settings.agentBehavior.skills.sources.addPath.prompt=Kilo 스킬이 포함된 폴더를 선택하세요. +settings.agentBehavior.skills.sources.addUrl=URL 추가 +settings.agentBehavior.skills.sources.addUrl.title=스킬 소스 URL 추가 +settings.agentBehavior.skills.sources.addUrl.prompt=스킬 소스 URL을 입력하세요. +settings.agentBehavior.skills.sources.editPath.title=스킬 경로 편집 +settings.agentBehavior.skills.sources.editUrl.title=스킬 URL 편집 session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0}개 메시지가 롤백됨 @@ -399,3 +453,6 @@ settings.context.watcher.remove=선택한 패턴 제거 settings.context.watcher.empty=구성된 무시 패턴이 없습니다. settings.context.watcher.input.title=무시 패턴 추가 settings.context.watcher.input.prompt=무시할 glob 패턴을 입력하세요: + +# Auto-Approve settings +settings.autoApprove.edit=편집 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties index 0b981fee3bd..b29e1b9dc21 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -1,4 +1,6 @@ common.delete=Verwijderen +common.open=Openen +common.save=Opslaan session.action.cancel=Annuleren session.connection.connecting=Laden… session.connection.error.app=Verbinding mislukt @@ -28,6 +30,10 @@ session.permission.title=Toestemmingsverzoek session.permission.meta=Hulpmiddel: {0} • Patronen: {1} session.permission.allow=Toestaan session.permission.deny=Weigeren +session.permission.ask=Vragen +session.permission.rule.hint.default=Toekomstige overeenkomende aanroepen gebruiken de standaardmachtigingsinstelling: {0}. +session.permission.rule.hint.approve=Dit verzoek en toekomstige overeenkomende aanroepen worden toegestaan. +session.permission.rule.hint.deny=Dit verzoek en toekomstige overeenkomende aanroepen worden geweigerd. session.question.dismiss=Sluiten session.status.considering=Volgende stappen overwegen… @@ -51,6 +57,7 @@ session.part.tool.copy=Kopiëren session.part.tool.error=Fout session.part.tool.pending=In afwachting session.part.tool.read=Lezen +session.part.tool.edit=Bewerken session.part.tool.running=Actief session.part.tool.shell=Shell session.part.tool.truncated=Uitvoer ingekort in voorvertoning. Volledige uitvoer blijft beschikbaar in sessiegegevens. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Vraag de agent om een MCP-server toe te voegen. +settings.agentBehavior.skills.displayName=Vaardigheden +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Vaardigheden filteren +settings.agentBehavior.skills.empty=Geen vaardigheden gevonden. +settings.agentBehavior.skills.content.empty=Geen vaardigheidsinhoud beschikbaar. +settings.agentBehavior.skills.load.timeout=Het laden van vaardigheden is verlopen. Bestaande vaardigheden zijn behouden; verwijder trage of onbereikbare URL’s en vernieuw. +settings.agentBehavior.skills.reload.deferred=Vaardigheidsbron opgeslagen. Laad Core opnieuw nadat actieve sessies zijn voltooid om nieuwe vaardigheden toe te passen. +settings.agentBehavior.skills.reload.blocked=Vaardigheidsinstellingen opgeslagen, maar er zijn actieve sessies. Laad Core opnieuw nadat die sessies zijn voltooid om de nieuwe vaardigheden toe te passen. +settings.agentBehavior.skills.saved.notification=Vaardigheidsinstellingen opgeslagen +settings.agentBehavior.skills.delete.title=Vaardigheid verwijderen +settings.agentBehavior.skills.delete.message=Vaardigheid {0} verwijderen? Dit verwijdert het vaardigheidsbestand en kan niet ongedaan worden gemaakt. +settings.agentBehavior.skills.delete.failed=Kon de vaardigheid niet verwijderen. +settings.agentBehavior.skills.openInEditor=Openen in editor +settings.agentBehavior.skills.openInEditor.pending=Het vaardigheidsbestand wordt geopend nadat u Instellingen sluit. +settings.agentBehavior.skills.openInEditor.failed=Kon het vaardigheidsbestand niet openen in de editor. +settings.agentBehavior.skills.sources.empty=Geen vaardigheidsbronnen geconfigureerd. +settings.agentBehavior.skills.sources.title=Extra vaardigheidsbronnen +settings.agentBehavior.skills.sources.add=Toevoegen +settings.agentBehavior.skills.sources.addPath=Pad toevoegen +settings.agentBehavior.skills.sources.addPath.title=Vaardigheidspad toevoegen +settings.agentBehavior.skills.sources.addPath.prompt=Kies een map met Kilo-vaardigheden. +settings.agentBehavior.skills.sources.addUrl=URL toevoegen +settings.agentBehavior.skills.sources.addUrl.title=URL van vaardigheidsbron toevoegen +settings.agentBehavior.skills.sources.addUrl.prompt=Voer een URL van een vaardigheidsbron in. +settings.agentBehavior.skills.sources.editPath.title=Vaardigheidspad bewerken +settings.agentBehavior.skills.sources.editUrl.title=Vaardigheids-URL bewerken session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} bericht teruggedraaid @@ -399,3 +453,6 @@ settings.context.watcher.remove=Geselecteerde patronen verwijderen settings.context.watcher.empty=Geen negeerpatronen geconfigureerd. settings.context.watcher.input.title=Negeerpatroon toevoegen settings.context.watcher.input.prompt=Voer een glob-patroon in om te negeren: + +# Auto-Approve settings +settings.autoApprove.edit=Bewerken diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties index 946ebb04d27..c577c420520 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -1,4 +1,6 @@ common.delete=Slett +common.open=Åpne +common.save=Lagre session.action.cancel=Avbryt session.connection.connecting=Laster… session.connection.error.app=Tilkoblingsfeil @@ -28,6 +30,10 @@ session.permission.title=Tillatelsesforespørsel session.permission.meta=Verktøy: {0} • Mønstre: {1} session.permission.allow=Tillat session.permission.deny=Avslå +session.permission.ask=Spør +session.permission.rule.hint.default=Fremtidige samsvarende kall bruker standard tillatelsesinnstilling: {0}. +session.permission.rule.hint.approve=Denne forespørselen og fremtidige samsvarende kall blir tillatt. +session.permission.rule.hint.deny=Denne forespørselen og fremtidige samsvarende kall blir avvist. session.question.dismiss=Lukk session.status.considering=Vurderer neste steg… @@ -51,6 +57,7 @@ session.part.tool.copy=Kopier session.part.tool.error=Feil session.part.tool.pending=Venter session.part.tool.read=Les +session.part.tool.edit=Rediger session.part.tool.running=Kjører session.part.tool.shell=Shell session.part.tool.truncated=Utdata avkortet i forhåndsvisning. Fullstendig utdata finnes fortsatt i øktdata. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Be agenten om å legge til en MCP-server. +settings.agentBehavior.skills.displayName=Ferdigheter +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Filtrer ferdigheter +settings.agentBehavior.skills.empty=Ingen ferdigheter funnet. +settings.agentBehavior.skills.content.empty=Ingen ferdighetsinnhold tilgjengelig. +settings.agentBehavior.skills.load.timeout=Innlasting av ferdigheter tidsavbrutt. Eksisterende ferdigheter ble beholdt; fjern trege eller utilgjengelige URL-er og oppdater. +settings.agentBehavior.skills.reload.deferred=Ferdighetskilden ble lagret. Last Core på nytt etter at aktive økter er ferdige for å bruke nye ferdigheter. +settings.agentBehavior.skills.reload.blocked=Ferdighetsinnstillinger ble lagret, men det finnes aktive økter. Last Core på nytt etter at disse øktene er ferdige for å bruke de nye ferdighetene. +settings.agentBehavior.skills.saved.notification=Ferdighetsinnstillinger lagret +settings.agentBehavior.skills.delete.title=Slett ferdighet +settings.agentBehavior.skills.delete.message=Slette ferdigheten {0}? Dette fjerner ferdighetsfilen og kan ikke angres. +settings.agentBehavior.skills.delete.failed=Kunne ikke slette ferdigheten. +settings.agentBehavior.skills.openInEditor=Åpne i editor +settings.agentBehavior.skills.openInEditor.pending=Ferdighetsfilen åpnes etter at du lukker Innstillinger. +settings.agentBehavior.skills.openInEditor.failed=Kunne ikke åpne ferdighetsfilen i editoren. +settings.agentBehavior.skills.sources.empty=Ingen ferdighetskilder konfigurert. +settings.agentBehavior.skills.sources.title=Flere ferdighetskilder +settings.agentBehavior.skills.sources.add=Legg til +settings.agentBehavior.skills.sources.addPath=Legg til sti +settings.agentBehavior.skills.sources.addPath.title=Legg til ferdighetssti +settings.agentBehavior.skills.sources.addPath.prompt=Velg en mappe som inneholder Kilo-ferdigheter. +settings.agentBehavior.skills.sources.addUrl=Legg til URL +settings.agentBehavior.skills.sources.addUrl.title=Legg til URL for ferdighetskilde +settings.agentBehavior.skills.sources.addUrl.prompt=Skriv inn en URL for ferdighetskilde. +settings.agentBehavior.skills.sources.editPath.title=Rediger ferdighetssti +settings.agentBehavior.skills.sources.editUrl.title=Rediger ferdighets-URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} melding rullet tilbake @@ -399,3 +453,6 @@ settings.context.watcher.remove=Fjern valgte mønstre settings.context.watcher.empty=Ingen ignormønstre konfigurert. settings.context.watcher.input.title=Legg til ignormønster settings.context.watcher.input.prompt=Skriv inn et glob-mønster som skal ignoreres: + +# Auto-Approve settings +settings.autoApprove.edit=Rediger diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties index 9b4e6eed922..d57e58ac718 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -1,4 +1,6 @@ common.delete=Usuń +common.open=Otwórz +common.save=Zapisz session.action.cancel=Anuluj session.connection.connecting=Ładowanie… session.connection.error.app=Błąd połączenia @@ -28,6 +30,10 @@ session.permission.title=Prośba o uprawnienia session.permission.meta=Narzędzie: {0} • Wzorce: {1} session.permission.allow=Zezwól session.permission.deny=Odmów +session.permission.ask=Pytaj +session.permission.rule.hint.default=Przyszłe pasujące wywołania użyją domyślnego ustawienia uprawnień: {0}. +session.permission.rule.hint.approve=To żądanie i przyszłe pasujące wywołania zostaną dozwolone. +session.permission.rule.hint.deny=To żądanie i przyszłe pasujące wywołania zostaną odrzucone. session.question.dismiss=Odrzuć session.status.considering=Rozważanie następnych kroków… @@ -51,6 +57,7 @@ session.part.tool.copy=Kopiuj session.part.tool.error=Błąd session.part.tool.pending=Oczekuje session.part.tool.read=Odczyt +session.part.tool.edit=Edycja session.part.tool.running=Uruchomione session.part.tool.shell=Powłoka session.part.tool.truncated=Wyjście skrócone w podglądzie. Pełne wyjście pozostaje w danych sesji. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Aby dodać serwer MCP, poproś agenta, aby to zrobił. +settings.agentBehavior.skills.displayName=Umiejętności +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Filtruj umiejętności +settings.agentBehavior.skills.empty=Nie znaleziono umiejętności. +settings.agentBehavior.skills.content.empty=Brak dostępnej treści umiejętności. +settings.agentBehavior.skills.load.timeout=Przekroczono czas ładowania umiejętności. Istniejące umiejętności zostały zachowane; usuń wolne lub niedostępne adresy URL i odśwież. +settings.agentBehavior.skills.reload.deferred=Źródło umiejętności zapisane. Przeładuj Core po zakończeniu aktywnych sesji, aby zastosować nowe umiejętności. +settings.agentBehavior.skills.reload.blocked=Ustawienia umiejętności zapisane, ale są aktywne sesje. Przeładuj Core po zakończeniu tych sesji, aby zastosować nowe umiejętności. +settings.agentBehavior.skills.saved.notification=Ustawienia umiejętności zapisane +settings.agentBehavior.skills.delete.title=Usuń umiejętność +settings.agentBehavior.skills.delete.message=Usunąć umiejętność {0}? Spowoduje to usunięcie pliku umiejętności i nie można tego cofnąć. +settings.agentBehavior.skills.delete.failed=Nie można usunąć umiejętności. +settings.agentBehavior.skills.openInEditor=Otwórz w edytorze +settings.agentBehavior.skills.openInEditor.pending=Plik umiejętności otworzy się po zamknięciu Ustawień. +settings.agentBehavior.skills.openInEditor.failed=Nie można otworzyć pliku umiejętności w edytorze. +settings.agentBehavior.skills.sources.empty=Nie skonfigurowano źródeł umiejętności. +settings.agentBehavior.skills.sources.title=Dodatkowe źródła umiejętności +settings.agentBehavior.skills.sources.add=Dodaj +settings.agentBehavior.skills.sources.addPath=Dodaj ścieżkę +settings.agentBehavior.skills.sources.addPath.title=Dodaj ścieżkę umiejętności +settings.agentBehavior.skills.sources.addPath.prompt=Wybierz folder zawierający umiejętności Kilo. +settings.agentBehavior.skills.sources.addUrl=Dodaj URL +settings.agentBehavior.skills.sources.addUrl.title=Dodaj URL źródła umiejętności +settings.agentBehavior.skills.sources.addUrl.prompt=Wpisz URL źródła umiejętności. +settings.agentBehavior.skills.sources.editPath.title=Edytuj ścieżkę umiejętności +settings.agentBehavior.skills.sources.editUrl.title=Edytuj URL umiejętności session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=Cofnięto {0} wiadomość @@ -399,3 +453,6 @@ settings.context.watcher.remove=Usuń wybrane wzorce settings.context.watcher.empty=Nie skonfigurowano wzorców ignorowania. settings.context.watcher.input.title=Dodaj wzorzec ignorowania settings.context.watcher.input.prompt=Wpisz wzorzec glob do ignorowania: + +# Auto-Approve settings +settings.autoApprove.edit=Edytuj diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties index 0bc0247b971..6655ce84b0f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -1,4 +1,6 @@ common.delete=Excluir +common.open=Abrir +common.save=Salvar session.action.cancel=Cancelar session.connection.connecting=Carregando… session.connection.error.app=Falha na conexão @@ -28,6 +30,10 @@ session.permission.title=Solicitação de permissão session.permission.meta=Ferramenta: {0} • Padrões: {1} session.permission.allow=Permitir session.permission.deny=Negar +session.permission.ask=Perguntar +session.permission.rule.hint.default=Chamadas correspondentes futuras usarão a configuração de permissão padrão: {0}. +session.permission.rule.hint.approve=Esta solicitação e chamadas correspondentes futuras serão permitidas. +session.permission.rule.hint.deny=Esta solicitação e chamadas correspondentes futuras serão rejeitadas. session.question.dismiss=Dispensar session.status.considering=Considerando os próximos passos… @@ -51,6 +57,7 @@ session.part.tool.copy=Copiar session.part.tool.error=Erro session.part.tool.pending=Pendente session.part.tool.read=Ler +session.part.tool.edit=Editar session.part.tool.running=Executando session.part.tool.shell=Shell session.part.tool.truncated=Saída truncada na pré-visualização. A saída completa permanece nos dados da sessão. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Para adicionar um servidor MCP, peça ao agente para fazer isso. +settings.agentBehavior.skills.displayName=Habilidades +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Filtrar habilidades +settings.agentBehavior.skills.empty=Nenhuma habilidade encontrada. +settings.agentBehavior.skills.content.empty=Nenhum conteúdo de habilidade disponível. +settings.agentBehavior.skills.load.timeout=O carregamento de habilidades atingiu o tempo limite. As habilidades existentes foram mantidas; remova URLs lentas ou inacessíveis e atualize. +settings.agentBehavior.skills.reload.deferred=Fonte de habilidades salva. Recarregue o Core depois que as sessões ativas terminarem para aplicar novas habilidades. +settings.agentBehavior.skills.reload.blocked=Configurações de habilidades salvas, mas há sessões ativas. Recarregue o Core depois que essas sessões terminarem para aplicar as novas habilidades. +settings.agentBehavior.skills.saved.notification=Configurações de habilidades salvas +settings.agentBehavior.skills.delete.title=Excluir habilidade +settings.agentBehavior.skills.delete.message=Excluir a habilidade {0}? Isso remove o arquivo da habilidade e não pode ser desfeito. +settings.agentBehavior.skills.delete.failed=Não foi possível excluir a habilidade. +settings.agentBehavior.skills.openInEditor=Abrir no editor +settings.agentBehavior.skills.openInEditor.pending=O arquivo da habilidade será aberto depois que você fechar as Configurações. +settings.agentBehavior.skills.openInEditor.failed=Não foi possível abrir o arquivo da habilidade no editor. +settings.agentBehavior.skills.sources.empty=Nenhuma fonte de habilidades configurada. +settings.agentBehavior.skills.sources.title=Fontes de habilidades adicionais +settings.agentBehavior.skills.sources.add=Adicionar +settings.agentBehavior.skills.sources.addPath=Adicionar caminho +settings.agentBehavior.skills.sources.addPath.title=Adicionar caminho de habilidades +settings.agentBehavior.skills.sources.addPath.prompt=Escolha uma pasta contendo habilidades do Kilo. +settings.agentBehavior.skills.sources.addUrl=Adicionar URL +settings.agentBehavior.skills.sources.addUrl.title=Adicionar URL de fonte de habilidades +settings.agentBehavior.skills.sources.addUrl.prompt=Insira uma URL de fonte de habilidades. +settings.agentBehavior.skills.sources.editPath.title=Editar caminho de habilidades +settings.agentBehavior.skills.sources.editUrl.title=Editar URL de habilidades session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} mensagem revertida @@ -399,3 +453,6 @@ settings.context.watcher.remove=Remover padrões selecionados settings.context.watcher.empty=Nenhum padrão de ignorar configurado. settings.context.watcher.input.title=Adicionar padrão de ignorar settings.context.watcher.input.prompt=Digite um padrão glob para ignorar: + +# Auto-Approve settings +settings.autoApprove.edit=Editar diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties index f5993cf0007..c1eaee006ad 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -1,4 +1,6 @@ common.delete=Удалить +common.open=Открыть +common.save=Сохранить session.action.cancel=Отмена session.connection.connecting=Загрузка… session.connection.error.app=Ошибка подключения @@ -28,6 +30,10 @@ session.permission.title=Запрос разрешения session.permission.meta=Инструмент: {0} • Шаблоны: {1} session.permission.allow=Разрешить session.permission.deny=Отказать +session.permission.ask=Спрашивать +session.permission.rule.hint.default=Будущие совпадающие вызовы будут использовать настройку разрешений по умолчанию: {0}. +session.permission.rule.hint.approve=Этот запрос и будущие совпадающие вызовы будут разрешены. +session.permission.rule.hint.deny=Этот запрос и будущие совпадающие вызовы будут отклонены. session.question.dismiss=Отклонить session.status.considering=Обдумываю следующие шаги… @@ -51,6 +57,7 @@ session.part.tool.copy=Копировать session.part.tool.error=Ошибка session.part.tool.pending=Ожидание session.part.tool.read=Чтение +session.part.tool.edit=Редактирование session.part.tool.running=Выполняется session.part.tool.shell=Shell session.part.tool.truncated=Вывод усечён в предпросмотре. Полный вывод сохраняется в данных сессии. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Чтобы добавить MCP-сервер, попросите агента сделать это. +settings.agentBehavior.skills.displayName=Навыки +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Фильтр навыков +settings.agentBehavior.skills.empty=Навыки не найдены. +settings.agentBehavior.skills.content.empty=Нет доступного содержимого навыка. +settings.agentBehavior.skills.load.timeout=Время загрузки навыков истекло. Существующие навыки сохранены; удалите медленные или недоступные URL и обновите. +settings.agentBehavior.skills.reload.deferred=Источник навыков сохранён. Перезагрузите Core после завершения активных сессий, чтобы применить новые навыки. +settings.agentBehavior.skills.reload.blocked=Настройки навыков сохранены, но есть активные сессии. Перезагрузите Core после завершения этих сессий, чтобы применить новые навыки. +settings.agentBehavior.skills.saved.notification=Настройки навыков сохранены +settings.agentBehavior.skills.delete.title=Удалить навык +settings.agentBehavior.skills.delete.message=Удалить навык {0}? Это удалит файл навыка, и действие нельзя будет отменить. +settings.agentBehavior.skills.delete.failed=Не удалось удалить навык. +settings.agentBehavior.skills.openInEditor=Открыть в редакторе +settings.agentBehavior.skills.openInEditor.pending=Файл навыка откроется после закрытия настроек. +settings.agentBehavior.skills.openInEditor.failed=Не удалось открыть файл навыка в редакторе. +settings.agentBehavior.skills.sources.empty=Источники навыков не настроены. +settings.agentBehavior.skills.sources.title=Дополнительные источники навыков +settings.agentBehavior.skills.sources.add=Добавить +settings.agentBehavior.skills.sources.addPath=Добавить путь +settings.agentBehavior.skills.sources.addPath.title=Добавить путь к навыкам +settings.agentBehavior.skills.sources.addPath.prompt=Выберите папку, содержащую навыки Kilo. +settings.agentBehavior.skills.sources.addUrl=Добавить URL +settings.agentBehavior.skills.sources.addUrl.title=Добавить URL источника навыков +settings.agentBehavior.skills.sources.addUrl.prompt=Введите URL источника навыков. +settings.agentBehavior.skills.sources.editPath.title=Изменить путь к навыкам +settings.agentBehavior.skills.sources.editUrl.title=Изменить URL навыков session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=Отменено сообщений: {0} @@ -399,3 +453,6 @@ settings.context.watcher.remove=Удалить выбранные шаблоны settings.context.watcher.empty=Шаблоны игнорирования не настроены. settings.context.watcher.input.title=Добавить шаблон игнорирования settings.context.watcher.input.prompt=Введите glob-шаблон для игнорирования: + +# Auto-Approve settings +settings.autoApprove.edit=Изменить diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties index bf99b6c0b6a..89e6d9a8df9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -1,4 +1,6 @@ common.delete=ลบ +common.open=เปิด +common.save=บันทึก session.action.cancel=ยกเลิก session.connection.connecting=กำลังโหลด… session.connection.error.app=การเชื่อมต่อล้มเหลว @@ -28,6 +30,10 @@ session.permission.title=ขอสิทธิ์ session.permission.meta=เครื่องมือ: {0} • รูปแบบ: {1} session.permission.allow=อนุญาต session.permission.deny=ปฏิเสธ +session.permission.ask=ถาม +session.permission.rule.hint.default=การเรียกที่ตรงกันในอนาคตจะใช้การตั้งค่าสิทธิ์เริ่มต้น: {0} +session.permission.rule.hint.approve=คำขอนี้และการเรียกที่ตรงกันในอนาคตจะได้รับอนุญาต +session.permission.rule.hint.deny=คำขอนี้และการเรียกที่ตรงกันในอนาคตจะถูกปฏิเสธ session.question.dismiss=ปิด session.status.considering=กำลังพิจารณาขั้นตอนถัดไป… @@ -51,6 +57,7 @@ session.part.tool.copy=คัดลอก session.part.tool.error=ข้อผิดพลาด session.part.tool.pending=รอดำเนินการ session.part.tool.read=อ่าน +session.part.tool.edit=แก้ไข session.part.tool.running=กำลังทำงาน session.part.tool.shell=Shell session.part.tool.truncated=ผลลัพธ์ถูกตัดทอนในส่วนตัวอย่าง ผลลัพธ์ทั้งหมดยังคงอยู่ในข้อมูลเซสชัน @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=หากต้องการเพิ่มเซิร์ฟเวอร์ MCP ให้ขอให้เอเจนต์เพิ่มให้ +settings.agentBehavior.skills.displayName=ทักษะ +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=กรองทักษะ +settings.agentBehavior.skills.empty=ไม่พบทักษะ +settings.agentBehavior.skills.content.empty=ไม่มีเนื้อหาทักษะที่พร้อมใช้งาน +settings.agentBehavior.skills.load.timeout=การโหลดทักษะหมดเวลา ระบบเก็บทักษะเดิมไว้แล้ว โปรดลบ URL ที่ช้าหรือเข้าถึงไม่ได้แล้วรีเฟรช +settings.agentBehavior.skills.reload.deferred=บันทึกแหล่งที่มาทักษะแล้ว โหลด Core ใหม่หลังจากเซสชันที่ใช้งานอยู่สิ้นสุดเพื่อใช้ทักษะใหม่ +settings.agentBehavior.skills.reload.blocked=บันทึกการตั้งค่าทักษะแล้ว แต่ยังมีเซสชันที่ใช้งานอยู่ โหลด Core ใหม่หลังจากเซสชันเหล่านั้นสิ้นสุดเพื่อใช้ทักษะใหม่ +settings.agentBehavior.skills.saved.notification=บันทึกการตั้งค่าทักษะแล้ว +settings.agentBehavior.skills.delete.title=ลบทักษะ +settings.agentBehavior.skills.delete.message=ลบทักษะ {0} หรือไม่ การดำเนินการนี้จะลบไฟล์ทักษะและไม่สามารถย้อนกลับได้ +settings.agentBehavior.skills.delete.failed=ไม่สามารถลบทักษะได้ +settings.agentBehavior.skills.openInEditor=เปิดในตัวแก้ไข +settings.agentBehavior.skills.openInEditor.pending=ไฟล์ทักษะจะเปิดหลังจากคุณปิดการตั้งค่า +settings.agentBehavior.skills.openInEditor.failed=ไม่สามารถเปิดไฟล์ทักษะในตัวแก้ไขได้ +settings.agentBehavior.skills.sources.empty=ไม่ได้กำหนดค่าแหล่งที่มาทักษะ +settings.agentBehavior.skills.sources.title=แหล่งที่มาทักษะเพิ่มเติม +settings.agentBehavior.skills.sources.add=เพิ่ม +settings.agentBehavior.skills.sources.addPath=เพิ่มเส้นทาง +settings.agentBehavior.skills.sources.addPath.title=เพิ่มเส้นทางทักษะ +settings.agentBehavior.skills.sources.addPath.prompt=เลือกโฟลเดอร์ที่มีทักษะ Kilo +settings.agentBehavior.skills.sources.addUrl=เพิ่ม URL +settings.agentBehavior.skills.sources.addUrl.title=เพิ่ม URL แหล่งที่มาทักษะ +settings.agentBehavior.skills.sources.addUrl.prompt=ป้อน URL แหล่งที่มาทักษะ +settings.agentBehavior.skills.sources.editPath.title=แก้ไขเส้นทางทักษะ +settings.agentBehavior.skills.sources.editUrl.title=แก้ไข URL ทักษะ session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=ย้อนกลับข้อความ {0} รายการแล้ว @@ -399,3 +453,6 @@ settings.context.watcher.remove=ลบรูปแบบที่เลือก settings.context.watcher.empty=ยังไม่ได้กำหนดค่ารูปแบบการละเว้น settings.context.watcher.input.title=เพิ่มรูปแบบการละเว้น settings.context.watcher.input.prompt=ป้อนรูปแบบ glob ที่จะละเว้น: + +# Auto-Approve settings +settings.autoApprove.edit=แก้ไข diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties index c1d1d2a855c..d6ee1e84d97 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -1,4 +1,6 @@ common.delete=Sil +common.open=Aç +common.save=Kaydet session.action.cancel=İptal session.connection.connecting=Yükleniyor… session.connection.error.app=Bağlantı hatası @@ -28,6 +30,10 @@ session.permission.title=İzin isteği session.permission.meta=Araç: {0} • Desenler: {1} session.permission.allow=İzin ver session.permission.deny=Reddet +session.permission.ask=Sor +session.permission.rule.hint.default=Gelecekteki eşleşen çağrılar varsayılan izin ayarını kullanacak: {0}. +session.permission.rule.hint.approve=Bu istek ve gelecekteki eşleşen çağrılara izin verilecek. +session.permission.rule.hint.deny=Bu istek ve gelecekteki eşleşen çağrılar reddedilecek. session.question.dismiss=Kapat session.status.considering=Sonraki adımlar düşünülüyor… @@ -51,6 +57,7 @@ session.part.tool.copy=Kopyala session.part.tool.error=Hata session.part.tool.pending=Bekliyor session.part.tool.read=Oku +session.part.tool.edit=Düzenle session.part.tool.running=Çalışıyor session.part.tool.shell=Kabuk session.part.tool.truncated=Önizlemede çıktı kısaltıldı. Tam çıktı oturum verilerinde kalıyor. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=MCP sunucusu eklemek için ajandan bunu yapmasını isteyin. +settings.agentBehavior.skills.displayName=Beceriler +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Becerileri filtrele +settings.agentBehavior.skills.empty=Beceri bulunamadı. +settings.agentBehavior.skills.content.empty=Kullanılabilir beceri içeriği yok. +settings.agentBehavior.skills.load.timeout=Beceriler yüklenirken zaman aşımına uğradı. Mevcut beceriler korundu; yavaş veya ulaşılamayan URL'leri kaldırıp yenileyin. +settings.agentBehavior.skills.reload.deferred=Beceri kaynağı kaydedildi. Yeni becerileri uygulamak için etkin oturumlar bittikten sonra Core'u yeniden yükleyin. +settings.agentBehavior.skills.reload.blocked=Beceri ayarları kaydedildi, ancak etkin oturumlar var. Yeni becerileri uygulamak için bu oturumlar bittikten sonra Core'u yeniden yükleyin. +settings.agentBehavior.skills.saved.notification=Beceri ayarları kaydedildi +settings.agentBehavior.skills.delete.title=Beceriyi Sil +settings.agentBehavior.skills.delete.message={0} becerisi silinsin mi? Bu işlem beceri dosyasını kaldırır ve geri alınamaz. +settings.agentBehavior.skills.delete.failed=Beceri silinemedi. +settings.agentBehavior.skills.openInEditor=Düzenleyicide Aç +settings.agentBehavior.skills.openInEditor.pending=Beceri dosyası Ayarlar kapatıldıktan sonra açılacak. +settings.agentBehavior.skills.openInEditor.failed=Beceri dosyası düzenleyicide açılamadı. +settings.agentBehavior.skills.sources.empty=Yapılandırılmış beceri kaynağı yok. +settings.agentBehavior.skills.sources.title=Ek Beceri Kaynakları +settings.agentBehavior.skills.sources.add=Ekle +settings.agentBehavior.skills.sources.addPath=Yol ekle +settings.agentBehavior.skills.sources.addPath.title=Beceri Yolu Ekle +settings.agentBehavior.skills.sources.addPath.prompt=Kilo becerilerini içeren bir klasör seçin. +settings.agentBehavior.skills.sources.addUrl=URL ekle +settings.agentBehavior.skills.sources.addUrl.title=Beceri Kaynağı URL'si Ekle +settings.agentBehavior.skills.sources.addUrl.prompt=Bir beceri kaynağı URL'si girin. +settings.agentBehavior.skills.sources.editPath.title=Beceri Yolunu Düzenle +settings.agentBehavior.skills.sources.editUrl.title=Beceri URL'sini Düzenle session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one={0} mesaj geri alındı @@ -399,3 +453,6 @@ settings.context.watcher.remove=Seçili kalıpları kaldır settings.context.watcher.empty=Yok sayma kalıbı yapılandırılmadı. settings.context.watcher.input.title=Yok sayma kalıbı ekle settings.context.watcher.input.prompt=Yok sayılacak bir glob kalıbı girin: + +# Auto-Approve settings +settings.autoApprove.edit=Düzenle diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties index f54be76f634..f385d236f4e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -1,4 +1,6 @@ common.delete=Видалити +common.open=Відкрити +common.save=Зберегти session.action.cancel=Скасувати session.connection.connecting=Завантаження… session.connection.error.app=Помилка з'єднання @@ -28,6 +30,10 @@ session.permission.title=Запит дозволу session.permission.meta=Інструмент: {0} • Шаблони: {1} session.permission.allow=Дозволити session.permission.deny=Заборонити +session.permission.ask=Запитувати +session.permission.rule.hint.default=Майбутні відповідні виклики використовуватимуть стандартне налаштування дозволів: {0}. +session.permission.rule.hint.approve=Цей запит і майбутні відповідні виклики буде дозволено. +session.permission.rule.hint.deny=Цей запит і майбутні відповідні виклики буде відхилено. session.question.dismiss=Закрити session.status.considering=Обмірковую наступні кроки… @@ -51,6 +57,7 @@ session.part.tool.copy=Копіювати session.part.tool.error=Помилка session.part.tool.pending=Очікується session.part.tool.read=Читання +session.part.tool.edit=Редагування session.part.tool.running=Виконується session.part.tool.shell=Shell session.part.tool.truncated=Вивід у попередньому перегляді усічено. Повний вивід зберігається в даних сесії. @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=Щоб додати сервер MCP, попросіть агента зробити це. +settings.agentBehavior.skills.displayName=Навички +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=Фільтрувати навички +settings.agentBehavior.skills.empty=Навички не знайдено. +settings.agentBehavior.skills.content.empty=Немає доступного вмісту навички. +settings.agentBehavior.skills.load.timeout=Час завантаження навичок минув. Наявні навички збережено; видаліть повільні або недоступні URL-адреси й оновіть. +settings.agentBehavior.skills.reload.deferred=Джерело навичок збережено. Перезавантажте Core після завершення активних сеансів, щоб застосувати нові навички. +settings.agentBehavior.skills.reload.blocked=Налаштування навичок збережено, але є активні сеанси. Перезавантажте Core після завершення цих сеансів, щоб застосувати нові навички. +settings.agentBehavior.skills.saved.notification=Налаштування навичок збережено +settings.agentBehavior.skills.delete.title=Видалити навичку +settings.agentBehavior.skills.delete.message=Видалити навичку {0}? Це видалить файл навички, і дію не можна буде скасувати. +settings.agentBehavior.skills.delete.failed=Не вдалося видалити навичку. +settings.agentBehavior.skills.openInEditor=Відкрити в редакторі +settings.agentBehavior.skills.openInEditor.pending=Файл навички відкриється після закриття Налаштувань. +settings.agentBehavior.skills.openInEditor.failed=Не вдалося відкрити файл навички в редакторі. +settings.agentBehavior.skills.sources.empty=Джерела навичок не налаштовано. +settings.agentBehavior.skills.sources.title=Додаткові джерела навичок +settings.agentBehavior.skills.sources.add=Додати +settings.agentBehavior.skills.sources.addPath=Додати шлях +settings.agentBehavior.skills.sources.addPath.title=Додати шлях до навичок +settings.agentBehavior.skills.sources.addPath.prompt=Виберіть папку з навичками Kilo. +settings.agentBehavior.skills.sources.addUrl=Додати URL +settings.agentBehavior.skills.sources.addUrl.title=Додати URL джерела навичок +settings.agentBehavior.skills.sources.addUrl.prompt=Введіть URL джерела навичок. +settings.agentBehavior.skills.sources.editPath.title=Редагувати шлях до навичок +settings.agentBehavior.skills.sources.editUrl.title=Редагувати URL навичок session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=Відкочено повідомлень: {0} @@ -399,3 +453,6 @@ settings.context.watcher.remove=Видалити вибрані шаблони settings.context.watcher.empty=Шаблони ігнорування не налаштовано. settings.context.watcher.input.title=Додати шаблон ігнорування settings.context.watcher.input.prompt=Введіть glob-шаблон для ігнорування: + +# Auto-Approve settings +settings.autoApprove.edit=Редагувати diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties index 817042a84f9..3f4b3fbc3db 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -1,4 +1,6 @@ common.delete=删除 +common.open=打开 +common.save=保存 session.action.cancel=取消 session.connection.connecting=加载中… session.connection.error.app=连接失败 @@ -28,6 +30,10 @@ session.permission.title=权限请求 session.permission.meta=工具:{0} • 模式:{1} session.permission.allow=允许 session.permission.deny=拒绝 +session.permission.ask=询问 +session.permission.rule.hint.default=未来匹配的调用将使用默认权限设置:{0}。 +session.permission.rule.hint.approve=此请求以及未来匹配的调用将被允许。 +session.permission.rule.hint.deny=此请求以及未来匹配的调用将被拒绝。 session.question.dismiss=关闭 session.status.considering=正在考虑下一步… @@ -51,6 +57,7 @@ session.part.tool.copy=复制 session.part.tool.error=错误 session.part.tool.pending=待处理 session.part.tool.read=读取 +session.part.tool.edit=编辑 session.part.tool.running=运行中 session.part.tool.shell=Shell session.part.tool.truncated=预览中的输出已截断。完整输出仍保留在会话数据中。 @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=要添加 MCP 服务器,请让代理为你添加。 +settings.agentBehavior.skills.displayName=技能 +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=筛选技能 +settings.agentBehavior.skills.empty=未找到技能。 +settings.agentBehavior.skills.content.empty=没有可用的技能内容。 +settings.agentBehavior.skills.load.timeout=技能加载超时。已保留现有技能;请移除缓慢或无法访问的 URL 后刷新。 +settings.agentBehavior.skills.reload.deferred=技能源已保存。请在活动会话结束后重新加载 Core,以应用新技能。 +settings.agentBehavior.skills.reload.blocked=技能设置已保存,但仍有活动会话。请在这些会话结束后重新加载 Core,以应用新技能。 +settings.agentBehavior.skills.saved.notification=技能设置已保存 +settings.agentBehavior.skills.delete.title=删除技能 +settings.agentBehavior.skills.delete.message=要删除技能 {0} 吗?这会移除技能文件,且无法撤销。 +settings.agentBehavior.skills.delete.failed=无法删除该技能。 +settings.agentBehavior.skills.openInEditor=在编辑器中打开 +settings.agentBehavior.skills.openInEditor.pending=关闭设置后将打开技能文件。 +settings.agentBehavior.skills.openInEditor.failed=无法在编辑器中打开技能文件。 +settings.agentBehavior.skills.sources.empty=未配置技能源。 +settings.agentBehavior.skills.sources.title=其他技能源 +settings.agentBehavior.skills.sources.add=添加 +settings.agentBehavior.skills.sources.addPath=添加路径 +settings.agentBehavior.skills.sources.addPath.title=添加技能路径 +settings.agentBehavior.skills.sources.addPath.prompt=选择一个包含 Kilo 技能的文件夹。 +settings.agentBehavior.skills.sources.addUrl=添加 URL +settings.agentBehavior.skills.sources.addUrl.title=添加技能源 URL +settings.agentBehavior.skills.sources.addUrl.prompt=输入技能源 URL。 +settings.agentBehavior.skills.sources.editPath.title=编辑技能路径 +settings.agentBehavior.skills.sources.editUrl.title=编辑技能 URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=已回滚 {0} 条消息 @@ -399,3 +453,6 @@ settings.context.watcher.remove=移除所选模式 settings.context.watcher.empty=未配置忽略模式。 settings.context.watcher.input.title=添加忽略模式 settings.context.watcher.input.prompt=输入要忽略的 glob 模式: + +# Auto-Approve settings +settings.autoApprove.edit=编辑 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties index ac6adcf29bd..dd2898d09a8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -1,4 +1,6 @@ common.delete=刪除 +common.open=開啟 +common.save=儲存 session.action.cancel=取消 session.connection.connecting=載入中… session.connection.error.app=連線失敗 @@ -28,6 +30,10 @@ session.permission.title=權限請求 session.permission.meta=工具:{0} • 模式:{1} session.permission.allow=允許 session.permission.deny=拒絕 +session.permission.ask=詢問 +session.permission.rule.hint.default=未來相符的呼叫將使用預設權限設定:{0}。 +session.permission.rule.hint.approve=此請求以及未來相符的呼叫將被允許。 +session.permission.rule.hint.deny=此請求以及未來相符的呼叫將被拒絕。 session.question.dismiss=關閉 session.status.considering=考慮下一步… @@ -51,6 +57,7 @@ session.part.tool.copy=複製 session.part.tool.error=錯誤 session.part.tool.pending=待處理 session.part.tool.read=讀取 +session.part.tool.edit=編輯 session.part.tool.running=執行中 session.part.tool.shell=Shell session.part.tool.truncated=預覽中的輸出已截斷。完整輸出仍保留在工作階段資料中。 @@ -368,6 +375,53 @@ settings.agentBehavior.undo=Undo settings.agentBehavior.agents.create.failed=Could not create the agent. settings.agentBehavior.mcp.addHint=若要新增 MCP 伺服器,請請代理為你新增。 +settings.agentBehavior.skills.displayName=技能 +settings.agentBehavior.rules.displayName=Rules +settings.rules.files.title=Additional Instruction Files +settings.rules.files.description=Paths to additional instruction files that are included in the system prompt +settings.rules.files.add=Add file +settings.rules.files.add.description=Add instruction file +settings.rules.files.search=Filter instruction files +settings.rules.files.edit.title=Edit Instruction File +settings.rules.files.delete.title=Delete Instruction File +settings.rules.files.delete.message=Delete instruction file {0}? This removes it from your config. +settings.rules.files.openInEditor=Open in Editor +settings.rules.files.openInEditor.pending=The instruction file will open after you close Settings. +settings.rules.files.openInEditor.failed=Could not open the instruction file. +settings.rules.files.cannotEdit=This instruction path is not a local file and can't be edited here. +settings.rules.saved.notification=Rules settings saved +settings.rules.files.empty=No additional instruction files configured. +settings.rules.files.input.title=Add Instruction File +settings.rules.files.input.prompt=Enter an instruction file path, glob, or URL: +settings.rules.claude.heading=Claude Code Compatibility +settings.rules.claude.title=Load Claude Code Files +settings.rules.claude.description=Load CLAUDE.md instructions and skills from your Claude Code configuration directory into sessions. Enable this if you want Kilo to use your Claude Code instructions and skills. Requires restart. +settings.rules.save.pending=Saving rules settings... +settings.rules.save.failed=Failed to save rules settings. +settings.agentBehavior.skills.search=篩選技能 +settings.agentBehavior.skills.empty=找不到技能。 +settings.agentBehavior.skills.content.empty=沒有可用的技能內容。 +settings.agentBehavior.skills.load.timeout=技能載入逾時。已保留現有技能;請移除緩慢或無法連線的 URL 後重新整理。 +settings.agentBehavior.skills.reload.deferred=技能來源已儲存。請在作用中工作階段結束後重新載入 Core,以套用新技能。 +settings.agentBehavior.skills.reload.blocked=技能設定已儲存,但仍有作用中工作階段。請在這些工作階段結束後重新載入 Core,以套用新技能。 +settings.agentBehavior.skills.saved.notification=技能設定已儲存 +settings.agentBehavior.skills.delete.title=刪除技能 +settings.agentBehavior.skills.delete.message=要刪除技能 {0} 嗎?這會移除技能檔案,且無法復原。 +settings.agentBehavior.skills.delete.failed=無法刪除該技能。 +settings.agentBehavior.skills.openInEditor=在編輯器中開啟 +settings.agentBehavior.skills.openInEditor.pending=關閉設定後將開啟技能檔案。 +settings.agentBehavior.skills.openInEditor.failed=無法在編輯器中開啟技能檔案。 +settings.agentBehavior.skills.sources.empty=未設定技能來源。 +settings.agentBehavior.skills.sources.title=其他技能來源 +settings.agentBehavior.skills.sources.add=新增 +settings.agentBehavior.skills.sources.addPath=新增路徑 +settings.agentBehavior.skills.sources.addPath.title=新增技能路徑 +settings.agentBehavior.skills.sources.addPath.prompt=選擇包含 Kilo 技能的資料夾。 +settings.agentBehavior.skills.sources.addUrl=新增 URL +settings.agentBehavior.skills.sources.addUrl.title=新增技能來源 URL +settings.agentBehavior.skills.sources.addUrl.prompt=輸入技能來源 URL。 +settings.agentBehavior.skills.sources.editPath.title=編輯技能路徑 +settings.agentBehavior.skills.sources.editUrl.title=編輯技能 URL session.file.missing=Couldn''t find ''{0}'' in this repository. revert.banner.filesNotRestored=Snapshots are off - only the conversation was reverted; your files were not changed. revert.banner.count.one=已回復 {0} 則訊息 @@ -399,3 +453,6 @@ settings.context.watcher.remove=移除所選模式 settings.context.watcher.empty=尚未設定忽略模式。 settings.context.watcher.input.title=新增忽略模式 settings.context.watcher.input.prompt=輸入要忽略的 glob 模式: + +# Auto-Approve settings +settings.autoApprove.edit=編輯 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt index 8629fa1c762..477a78333ca 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/KiloRecoveryActionsTest.kt @@ -360,6 +360,7 @@ class KiloRecoveryActionsTest : BasePlatformTestCase() { dir, MutableStateFlow(KiloWorkspaceStateDto(KiloWorkspaceStatusDto.READY)), reload = {}, + refreshConfigFiles = {}, ) } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt index 5dd31cb642b..fefcde20dd9 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloAgentBehaviorServiceTest.kt @@ -3,6 +3,7 @@ package ai.kilocode.client.app import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi import ai.kilocode.rpc.dto.AgentCreateDto import ai.kilocode.rpc.dto.McpStatusDto +import ai.kilocode.rpc.dto.SkillDto import com.intellij.testFramework.fixtures.BasePlatformTestCase import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -10,6 +11,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import kotlin.test.assertFailsWith class KiloAgentBehaviorServiceTest : BasePlatformTestCase() { private lateinit var scope: CoroutineScope @@ -67,6 +69,35 @@ class KiloAgentBehaviorServiceTest : BasePlatformTestCase() { assertTrue(rpc.removals.isEmpty()) } + fun `test loadSkills propagates rpc failure`() = runBlocking { + rpc.skillsError = RuntimeException("boom") + + assertFailsWith { + withContext(Dispatchers.Default) { service.loadSkills("/test") } + } + } + + fun `test refreshSkills returns previous rows on rpc failure`() = runBlocking { + val fallback = listOf(SkillDto("plan", location = "/test/SKILL.md")) + rpc.skillsError = RuntimeException("boom") + + val items = withContext(Dispatchers.Default) { service.refreshSkills("/test", fallback) } + + assertEquals(fallback, items) + } + + fun `test saveSkills forwards all edits`() = runBlocking { + rpc.skills = listOf(SkillDto("plan", location = "/test/plan/SKILL.md")) + + val ok = withContext(Dispatchers.Default) { + service.saveSkills("/test", mapOf("/test/plan/SKILL.md" to "# Saved")) + } + + assertTrue(ok) + assertEquals(listOf(Triple("/test", "/test/plan/SKILL.md", "# Saved")), rpc.skillSaves) + assertEquals("# Saved", rpc.skills.single().content) + } + fun `test mcpStatus forwards directory`() = runBlocking { rpc.mcps = listOf(McpStatusDto("filesystem", "connected")) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloWorkspaceServiceTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloWorkspaceServiceTest.kt index 2691c32705f..6b437061033 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloWorkspaceServiceTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloWorkspaceServiceTest.kt @@ -98,6 +98,18 @@ class KiloWorkspaceServiceTest : BasePlatformTestCase() { assertEquals(listOf("dep"), rpc.searchQueries) } + fun `test refreshConfigFiles logs backend failure and completes`() = runBlocking { + rpc.refreshConfigThrows = IllegalStateException("backend unavailable") + + val job = service.refreshConfigFiles("/test") + job.join() + + assertTrue(job.isCompleted) + assertEquals(listOf("/test"), rpc.refreshedConfigs.toList()) + assertEquals(0, rpc.localConfigPathCalls) + assertEquals(0, rpc.globalConfigPathCalls) + } + fun `test searchFiles sends query to RPC`() = runBlocking { withContext(Dispatchers.Default) { service.searchFiles("/test", "src") diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt index 8449703a982..b84cdb5da6f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.plugin.KiloPluginSettings import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.model.PermissionFileDiff import ai.kilocode.client.session.model.PermissionMeta +import ai.kilocode.client.session.model.PermissionRuleDecision import ai.kilocode.client.session.model.SessionState import ai.kilocode.client.session.model.Tool import ai.kilocode.rpc.dto.AgentDto @@ -20,6 +21,7 @@ import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto import ai.kilocode.rpc.dto.PermissionFileDiffDto import ai.kilocode.rpc.dto.PermissionReplyDto import ai.kilocode.rpc.dto.PermissionRequestDto +import ai.kilocode.rpc.dto.PermissionRuleDecisionDto import ai.kilocode.rpc.dto.ProviderDto import ai.kilocode.rpc.dto.PromptPartDto import ai.kilocode.rpc.dto.QuestionInfoDto @@ -335,6 +337,32 @@ class PromptLifecycleTest : SessionControllerTestBase() { assertEquals(1, rpc.permissionRulesSaved.size) assertEquals("perm1", rpc.permissionRulesSaved[0].first) assertEquals(1, rpc.permissionReplies.size) + assertEquals(listOf("/test"), projectRpc.refreshedConfigs.toList()) + } + + fun `test permission request maps rule decisions into model`() { + val (m, _, _) = prompted() + val request = permission("perm1").copy( + rules = listOf("git *", "git add *", "git add ."), + ruleDecisions = listOf( + PermissionRuleDecisionDto("git *", "approved", "ask"), + PermissionRuleDecisionDto("git add *", "denied", "allow"), + PermissionRuleDecisionDto("git add ."), + ), + ) + + emit(ChatEventDto.PermissionAsked("ses_test", request)) + + val state = m.model.state as? SessionState.AwaitingPermission ?: error("Expected AwaitingPermission") + assertEquals(listOf("git *", "git add *", "git add ."), state.permission.meta.rules) + assertEquals( + listOf(PermissionRuleDecision.APPROVED, PermissionRuleDecision.DENIED, PermissionRuleDecision.PENDING), + state.permission.meta.ruleDecisions.map { it.decision }, + ) + assertEquals( + listOf(PermissionRuleDecision.PENDING, PermissionRuleDecision.APPROVED, PermissionRuleDecision.PENDING), + state.permission.meta.ruleDecisions.map { it.defaultDecision }, + ) } fun `test replyQuestion calls RPC`() { @@ -535,6 +563,7 @@ class PromptLifecycleTest : SessionControllerTestBase() { flush() assertTrue(rpc.permissionRulesSaved.isEmpty()) + assertTrue(projectRpc.refreshedConfigs.isEmpty()) assertEquals(1, rpc.permissionReplies.size) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt index 0a5eaf824f0..ed069137883 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionLayoutTest.kt @@ -7,6 +7,7 @@ import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.Dimension import java.awt.Insets +import javax.swing.JPanel import javax.swing.JLabel /** @@ -281,6 +282,66 @@ class SessionLayoutTest : BasePlatformTestCase() { assertEquals(20 + JBUI.scale(8), c2.y) } + fun `test valid child reuses cached preferred height`() { + val p = panel(width = 300) + val child = probe(height = 20) + p.add(child) + p.doLayout() + child.markValid() + val count = child.count + + p.doLayout() + + assertEquals(count, child.count) + assertEquals(20, child.height) + } + + fun `test invalid child is measured again`() { + val p = panel(width = 300) + val child = probe(height = 20) + p.add(child) + p.doLayout() + child.markValid() + val count = child.count + + child.invalidate() + p.doLayout() + + assertEquals(count + 1, child.count) + } + + fun `test width change forces cached child remeasure`() { + val p = panel(width = 300) + val child = probe(height = 20) + p.add(child) + p.doLayout() + child.markValid() + val count = child.count + + p.setSize(320, 2000) + p.doLayout() + + assertEquals(count + 1, child.count) + assertEquals(320, child.width) + } + + fun `test forget re-measures a valid child`() { + val p = panel(width = 300) + val child = probe(height = 20) + p.add(child) + p.doLayout() + child.markValid() + val count = child.count + + // A settled turn is its own validate root, so it can be re-validated independently and its + // isValid flag flips back to true even after its content (and height) changed. forget() + // drops the stale cached height so the next layout pass re-measures the child. + (p.layout as SessionLayout).forget(child) + p.doLayout() + + assertEquals(count + 1, child.count) + } + // ---- helpers ------ /** A fixed-height JLabel. The width is reported as 0 until layout sets it. */ @@ -293,4 +354,25 @@ class SessionLayoutTest : BasePlatformTestCase() { override fun getPreferredSize(): Dimension = Dimension(0, height) } + + private fun probe(height: Int) = object : JPanel() { + var count = 0 + private var valid = false + + override fun isValid() = valid + + override fun invalidate() { + valid = false + super.invalidate() + } + + fun markValid() { + valid = true + } + + override fun getPreferredSize(): Dimension { + count++ + return Dimension(0, height) + } + } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt index e9a1c58725a..d5e72cfc081 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/SessionMessageListPanelTest.kt @@ -39,6 +39,8 @@ import ai.kilocode.rpc.dto.TodoDto import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.registry.Registry +import com.intellij.openapi.util.registry.RegistryKeyDescriptor import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.ui.components.ActionLink import com.intellij.ui.components.JBLabel @@ -53,7 +55,9 @@ import java.awt.Point import java.awt.event.MouseEvent import java.awt.image.BufferedImage import javax.swing.JButton +import javax.swing.JComponent import javax.swing.JPanel +import javax.swing.RepaintManager import javax.swing.SwingUtilities import javax.swing.border.Border @@ -401,6 +405,151 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { assertEquals("hello world", tv.markdown()) } + fun `test empty ContentDelta does not refresh panel`() { + model.upsertMessage(msg("a1", "assistant")) + model.updateContent("a1", part("p1", "a1", "text", text = "hello")) + val mv = panel.findMessage("a1")!! + val tv = mv.part("p1") as TextView + val repaint = TrackingRepaintManager(setOf(panel, mv, tv)) + val old = RepaintManager.currentManager(panel) + + try { + RepaintManager.setCurrentManager(repaint) + + model.appendDelta("a1", "p1", "") + + assertEquals("hello", tv.markdown()) + assertTrue(repaint.dirty.isEmpty()) + assertTrue(repaint.invalid.isEmpty()) + } finally { + RepaintManager.setCurrentManager(old) + } + } + + fun `test identical ContentUpdated does not refresh panel`() { + model.upsertMessage(msg("a1", "assistant")) + model.updateContent("a1", part("p1", "a1", "text", text = "hello")) + val mv = panel.findMessage("a1")!! + val tv = mv.part("p1") as TextView + val comp = tv.md.component + val repaint = TrackingRepaintManager(setOf(panel, mv, tv)) + val old = RepaintManager.currentManager(panel) + + try { + RepaintManager.setCurrentManager(repaint) + + model.updateContent("a1", part("p1", "a1", "text", text = "hello")) + + assertSame(tv, mv.part("p1")) + assertSame(comp, tv.md.component) + assertTrue(repaint.dirty.isEmpty()) + assertTrue(repaint.invalid.isEmpty()) + } finally { + RepaintManager.setCurrentManager(old) + } + } + + // ------ settled turns / validate roots (B) ------ + + fun `test turns are validate roots when idle`() { + model.upsertMessage(msg("u1", "user")) + model.upsertMessage(msg("a1", "assistant")) + model.upsertMessage(msg("u2", "user")) + + assertTrue(panel.findTurn("u1")!!.isValidateRoot()) + assertTrue(panel.findTurn("u2")!!.isValidateRoot()) + } + + fun `test streaming turn is not a validate root while busy`() { + model.upsertMessage(msg("u1", "user")) + model.upsertMessage(msg("a1", "assistant")) + model.upsertMessage(msg("u2", "user")) + model.upsertMessage(msg("a2", "assistant")) + + model.setState(SessionState.Busy("thinking")) + + assertTrue("prior turn stays a validate root", panel.findTurn("u1")!!.isValidateRoot()) + assertFalse("streaming turn must not be a validate root", panel.findTurn("u2")!!.isValidateRoot()) + } + + fun `test turns settle again when idle`() { + model.upsertMessage(msg("u1", "user")) + model.upsertMessage(msg("u2", "user")) + model.setState(SessionState.Busy("thinking")) + + model.setState(SessionState.Idle) + + assertTrue(panel.findTurn("u1")!!.isValidateRoot()) + assertTrue(panel.findTurn("u2")!!.isValidateRoot()) + } + + fun `test turn added while busy becomes the active non-root turn`() { + model.upsertMessage(msg("u1", "user")) + model.setState(SessionState.Busy("thinking")) + assertFalse(panel.findTurn("u1")!!.isValidateRoot()) + + model.upsertMessage(msg("u2", "user")) + + assertTrue("previous turn settles once a newer turn is active", panel.findTurn("u1")!!.isValidateRoot()) + assertFalse("newest turn is the active streaming turn", panel.findTurn("u2")!!.isValidateRoot()) + } + + fun `test validate roots flag disables turn isolation`() { + disableValidateRoots() + model.upsertMessage(msg("u1", "user")) + + assertFalse(panel.findTurn("u1")!!.isValidateRoot()) + } + + fun `test settled turns still follow panel width top down`() { + model.upsertMessage(msg("a1", "assistant")) + model.updateContent("a1", part("p1", "a1", "text", text = "answer")) + val turn = panel.findTurn("a1")!! + assertTrue("idle turn is a validate root", turn.isValidateRoot()) + + panel.setSize(600, 2000) + layout(panel) + val wide = turn.width + + panel.setSize(500, 2000) + layout(panel) + + assertTrue("validate-root turns must still relayout top-down", turn.width < wide) + assertTrue(turn.isValidateRoot()) + } + + // ------ streaming stress / teardown ------ + + fun `test many streamed turns stay bounded and fully tear down`() { + val empty = count(panel) + + repeat(40) { i -> + model.upsertMessage(msg("u$i", "user")) + model.updateContent("u$i", part("up$i", "u$i", "text", text = "q$i")) + model.upsertMessage(msg("a$i", "assistant")) + model.updateContent("a$i", part("ap$i", "a$i", "text", text = "```kotlin\nval x = $i\n```")) + repeat(20) { j -> model.appendDelta("a$i", "ap$i", " tok$j") } + } + assertEquals(40, panel.turnCount()) + + // Retained instances stay identical while streaming into an earlier message, + // and streaming deltas must not grow the component tree. + val tv = panel.findMessage("a0")!!.part("ap0") as TextView + val comp = tv.md.component + val count = count(panel) + repeat(50) { model.appendDelta("a0", "ap0", " x$it") } + + assertSame(tv, panel.findMessage("a0")!!.part("ap0")) + assertSame(comp, tv.md.component) + assertEquals(count, count(panel)) + + model.clear() + + assertEquals(0, panel.turnCount()) + assertTrue("transcript turns must be removed on clear", panel.components.none { it is TurnView }) + assertEquals("clear must return the transcript to its empty component tree", empty, count(panel)) + } + fun `test ContentDelta preserves TextView and markdown component`() { model.upsertMessage(msg("a1", "assistant")) model.updateContent("a1", part("p1", "a1", "text", text = "first\n\nsecond")) @@ -1037,7 +1186,7 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { reject = { _ -> }, ) val p = PermissionView( - reply = { _, _ -> }, + reply = { _, _, _ -> }, ) val l = LoginRequiredView(openProfile = {}, dismiss = {}) return SessionMessageListPanel(model, parent, q, p, l, openFile) @@ -1152,6 +1301,18 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { for (child in root.components) if (child is Container) layout(child) } + /** The plugin's `` extensions are not loaded in tests, so contribute the key here. */ + private fun disableValidateRoots() { + val key = "kilo.session.validateRoots" + Registry.mutateContributedKeys { + it + (key to RegistryKeyDescriptor(key, "test", "true", false, false, null, null)) + } + Disposer.register(testRootDisposable) { + Registry.mutateContributedKeys { it - key } + } + Registry.get(key).setValue(false, testRootDisposable) + } + private fun promptBox(root: MessageView): Component { return components(root).first { it.parent != root && it is JPanel && it.componentCount == 1 && it.components.single() is TextView } } @@ -1175,4 +1336,19 @@ class SessionMessageListPanelTest : BasePlatformTestCase() { .joinToString(" ") } } + + private class TrackingRepaintManager(private val watched: Set) : RepaintManager() { + val dirty = mutableListOf() + val invalid = mutableListOf() + + override fun addDirtyRegion(c: JComponent, x: Int, y: Int, w: Int, h: Int) { + if (c in watched) dirty.add(c) + super.addDirtyRegion(c, x, y, w, h) + } + + override fun addInvalidComponent(invalidComponent: JComponent) { + if (invalidComponent in watched) invalid.add(invalidComponent) + super.addInvalidComponent(invalidComponent) + } + } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/EditToolViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/EditToolViewTest.kt new file mode 100644 index 00000000000..4cfaf8d2c61 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/EditToolViewTest.kt @@ -0,0 +1,476 @@ +package ai.kilocode.client.session.views + +import ai.kilocode.client.session.model.Tool +import ai.kilocode.client.session.model.ToolExecState +import ai.kilocode.client.session.model.toolKind +import ai.kilocode.client.session.ui.style.SessionUiStyle +import ai.kilocode.client.session.views.base.SecondarySessionPartView +import ai.kilocode.client.session.views.tool.EditToolView +import ai.kilocode.client.session.views.tool.ReadToolView +import ai.kilocode.client.session.views.tool.ToolView +import ai.kilocode.client.ui.DiffStatBadge +import com.intellij.openapi.diff.DiffColors +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.util.Disposer +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import kotlinx.serialization.json.addJsonObject +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put +import java.awt.Component +import java.awt.Container +import java.awt.event.MouseEvent + +@Suppress("UnstableApiUsage") +class EditToolViewTest : BasePlatformTestCase() { + + private val views = mutableListOf() + + override fun tearDown() { + views.forEach { Disposer.dispose(it) } + views.clear() + super.tearDown() + } + + fun `test edit tool shows Edit title and clickable file link`() { + val opened = mutableListOf() + val view = track(EditToolView(tool(), openFile = { href, _ -> opened.add(href) })) + val base: Any = view + + assertTrue(base is SecondarySessionPartView) + assertTrue(view.labelText().contains("Edit")) + assertTrue(view.linkVisible()) + assertEquals("App.kt", view.linkLabel()) + assertEquals("/repo/src/App.kt", view.linkHref()) + assertEquals("/repo/src/App.kt", view.linkTooltip()) + assertTrue(view.labelText().contains("App.kt")) + + view.openLink() + + assertEquals(listOf("/repo/src/App.kt"), opened) + } + + fun `test edit link uses metadata path when input is only filename`() { + val opened = mutableListOf() + val path = "backend/src/com/kirillk/watcher/dao/GameApi.java" + val view = track(EditToolView(tool().also { + it.title = "GameApi.java" + it.input = mapOf("filePath" to "GameApi.java") + it.metadata = mapOf("filediff" to fileDiff(1, 0, PATCH, path)) + }, openFile = { href, _ -> opened.add(href) })) + + assertEquals("GameApi.java", view.linkLabel()) + assertEquals(path, view.linkHref()) + + view.openLink() + + assertEquals(listOf(path), opened) + } + + fun `test changes tag shows additions and deletions`() { + val view = track(EditToolView(tool())) + + assertTrue(view.badgeVisible()) + assertEquals(2 to 1, view.diffStat()) + } + + fun `test changes tag hidden without diff`() { + val view = track(EditToolView(tool().also { it.metadata = emptyMap() })) + + assertFalse(view.badgeVisible()) + assertEquals(0 to 0, view.diffStat()) + } + + fun `test multi file apply_patch shows file count tag and aggregated changes`() { + val view = track(EditToolView(tool().also { + it.input = emptyMap() + it.metadata = mapOf("files" to filesMeta( + FileChange("src/A.kt", 2, 0, ADD_HUNK), + FileChange("src/B.kt", 1, 1, UPDATE_HUNK), + )) + })) + + assertTrue(view.labelText().contains("Patch")) + assertFalse(view.labelText().contains("Edit")) + assertTrue(view.filesTagVisible()) + assertTrue(view.filesTagText()!!.contains("2 files")) + assertFalse(view.linkVisible()) + assertTrue(view.badgeVisible()) + assertEquals(3 to 1, view.diffStat()) + } + + fun `test multi file patch body renders a link and diff per file`() { + val opened = mutableListOf() + val view = track(EditToolView(tool().also { + it.input = emptyMap() + it.metadata = mapOf("files" to filesMeta( + FileChange("src/A.kt", 2, 0, ADD_HUNK), + FileChange("pkg/B.kt", 1, 1, UPDATE_HUNK), + )) + }, openFile = { href, _ -> opened.add(href) })) + + view.toggle() + + assertTrue(view.isExpanded()) + assertEquals(2, view.codeEditors().size) + + val fileLinks = labels(view).filter { it.text?.contains("") == true } + assertTrue(fileLinks.any { it.text!!.contains("A.kt") && !it.text!!.contains("src/") }) + assertTrue(fileLinks.any { it.text!!.contains("B.kt") && !it.text!!.contains("pkg/") }) + assertTrue(fileLinks.any { it.text!!.contains("A.kt") && it.toolTipText == "src/A.kt" }) + assertTrue(fileLinks.any { it.text!!.contains("B.kt") && it.toolTipText == "pkg/B.kt" }) + + // The per-file header renders one changes badge per file (plus the aggregate header badge). + assertEquals(3, badges(view).size) + + click(fileLinks.first { it.text!!.contains("A.kt") }, 1) + assertEquals(listOf("src/A.kt"), opened) + } + + fun `test single file apply_patch keeps link and hides count tag`() { + val view = track(EditToolView(tool().also { + it.input = emptyMap() + it.title = "src/Only.kt" + it.metadata = mapOf("files" to filesMeta(FileChange("src/Only.kt", 1, 1, UPDATE_HUNK))) + })) + + assertFalse(view.filesTagVisible()) + assertTrue(view.linkVisible()) + assertEquals(1 to 1, view.diffStat()) + assertFalse(view.markdown().contains("src/Only.kt")) + assertEquals(1, Regex("```patch-pure").findAll(view.markdown()).count()) + } + + fun `test edit body renders unified diff and expands`() { + val view = track(EditToolView(tool())) + + assertTrue(view.hasToggle()) + assertFalse(view.isExpanded()) + assertFalse(view.bodyVisible()) + assertTrue(view.markdown().contains("```patch-pure")) + assertTrue(view.markdown().contains("+new1")) + + view.toggle() + + assertTrue(view.isExpanded()) + assertTrue(view.bodyVisible()) + assertTrue(view.bodyCreated()) + assertTrue(view.codeEditors().single().text.contains("new1")) + assertFalse(view.codeEditors().single().text.contains("+new1")) + assertFalse(view.codeEditors().single().text.contains("-old")) + } + + fun `test edit body strips patch metadata headers`() { + // Relative-path headers so the `--- `/`+++ ` file-header assertions below actually exercise + // stripping: the header text (`--- src/App.kt`) shares its prefix with nothing in the body. + val patch = """ + Index: src/App.kt + =================================================================== + --- src/App.kt + +++ src/App.kt + @@ -1,2 +1,2 @@ + keep + -old + +new + """.trimIndent() + val view = track(EditToolView(tool().also { it.metadata = mapOf("filediff" to fileDiff(1, 1, patch)) })) + + assertFalse(view.markdown().contains("@@ -1,2 +1,2 @@")) + assertTrue(view.markdown().contains("-old")) + assertTrue(view.markdown().contains("+new")) + assertFalse(view.markdown().contains("Index:")) + assertFalse(view.markdown().contains("--- src/App.kt")) + assertFalse(view.markdown().contains("+++ src/App.kt")) + assertFalse(view.markdown().contains("====")) + + view.toggle() + + assertTrue(view.codeEditors().single().text.contains("old")) + assertTrue(view.codeEditors().single().text.contains("new")) + assertFalse(view.codeEditors().single().text.contains("-old")) + assertFalse(view.codeEditors().single().text.contains("+new")) + } + + fun `test edit body colors added and removed diff lines`() { + val view = track(EditToolView(tool())) + view.toggle() + val editor = view.codeEditors().single().getEditor(true)!! + val chars = editor.document.charsSequence + val spans = editor.markupModel.allHighlighters.mapNotNull { h -> + val key = h.textAttributesKey ?: return@mapNotNull null + key to chars.subSequence(h.startOffset, h.endOffset).toString() + } + + assertTrue(spans.any { it.first == DiffColors.DIFF_INSERTED && it.second.startsWith("new1") }) + assertTrue(spans.any { it.first == DiffColors.DIFF_DELETED && it.second.startsWith("old") }) + } + + fun `test clicking link text opens file but empty slot toggles body`() { + val opened = mutableListOf() + val view = track(EditToolView(tool(), openFile = { href, _ -> opened.add(href) })) + val link = linkLabel(view) + val slot = link.parent + + click(slot, link.preferredSize.width + 50) + + assertTrue(opened.isEmpty()) + assertTrue(view.isExpanded()) + + click(link, 0) + + assertEquals(listOf("/repo/src/App.kt"), opened) + } + + fun `test metadata only patch falls back to raw text`() { + // A pure rename (no +/-/context lines) is entirely metadata: stripping it leaves nothing, so + // the raw patch must survive rather than render an empty fenced block. + val patch = """ + diff --git a/src/Old.kt b/src/New.kt + similarity index 100% + rename from src/Old.kt + rename to src/New.kt + """.trimIndent() + val view = track(EditToolView(tool().also { it.metadata = mapOf("filediff" to fileDiff(0, 0, patch)) })) + + assertTrue(view.markdown().contains("rename from src/Old.kt")) + assertTrue(view.markdown().contains("rename to src/New.kt")) + } + + fun `test collapsed hover popup shows diff and none when expanded`() { + val view = track(EditToolView(tool())) + + assertNotNull(view.headerPopup()) + + view.toggle() + + assertNull(view.headerPopup()) + } + + fun `test edit header popup widens to diff content`() { + val patch = """ + --- src/App.kt + +++ src/App.kt + @@ -1 +1 @@ + -old + +${"x".repeat(180)} + """.trimIndent() + val view = track(EditToolView(tool().also { + it.metadata = mapOf("filediff" to fileDiff(1, 1, patch)) + })) + val body = view.headerPopup()!!.build() + + try { + assertTrue(body.component.preferredSize.width > JBUI.scale(SessionUiStyle.View.Popup.MAX_WIDTH)) + assertTrue(body.component.preferredSize.width <= JBUI.scale(SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)) + } finally { + Disposer.dispose(body.disposable) + } + } + + fun `test edit header popup stays narrow for short diff`() { + val patch = """ + --- src/App.kt + +++ src/App.kt + @@ -1 +1 @@ + -old + +new + """.trimIndent() + val view = track(EditToolView(tool().also { + it.metadata = mapOf("filediff" to fileDiff(1, 1, patch)) + })) + val body = view.headerPopup()!!.build() + + try { + assertTrue(body.component.preferredSize.width < JBUI.scale(SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)) + } finally { + Disposer.dispose(body.disposable) + } + } + + fun `test multi file patch popup reuses patch body links`() { + val opened = mutableListOf() + val view = track(EditToolView(tool().also { + it.input = emptyMap() + it.metadata = mapOf("files" to filesMeta( + FileChange("src/A.kt", 2, 0, ADD_HUNK), + FileChange("pkg/B.kt", 1, 1, UPDATE_HUNK), + )) + }, openFile = { href, _ -> opened.add(href) })) + val body = view.headerPopup()!!.build() + + try { + val fileLinks = labels(body.component).filter { it.text?.contains("") == true } + assertTrue(fileLinks.any { it.text!!.contains("A.kt") && it.toolTipText == "src/A.kt" }) + assertTrue(fileLinks.any { it.text!!.contains("B.kt") && it.toolTipText == "pkg/B.kt" }) + + click(fileLinks.first { it.text!!.contains("A.kt") }, 1) + assertEquals(listOf("src/A.kt"), opened) + } finally { + Disposer.dispose(body.disposable) + } + } + + fun `test no hover popup without diff`() { + val view = track(EditToolView(tool().also { it.metadata = emptyMap() })) + + assertNull(view.headerPopup()) + } + + fun `test view factory routes write tools to edit tool view`() { + assertTrue(ViewFactory.create(tool(), openFile = { _, _ -> }) is EditToolView) + assertTrue(ViewFactory.create(write("write"), openFile = { _, _ -> }) is EditToolView) + assertTrue(ViewFactory.create(write("apply_patch"), openFile = { _, _ -> }) is EditToolView) + } + + fun `test canRender matches write kind tools only`() { + assertTrue(EditToolView.canRender(tool())) + assertTrue(EditToolView.canRender(write("write"))) + assertFalse(EditToolView.canRender(Tool("p2", "read", toolKind("read")))) + assertFalse(EditToolView.canRender(Tool("p3", "bash", toolKind("bash")))) + } + + fun `test shouldReplace swaps generic and edit views`() { + val edit = tool() + val other = Tool("p9", "mystery", toolKind("mystery")).also { it.state = ToolExecState.COMPLETED } + + assertTrue(ViewFactory.shouldReplace(ToolView(edit), edit)) + assertTrue(ViewFactory.shouldReplace(EditToolView(edit), other)) + assertFalse(ViewFactory.shouldReplace(EditToolView(edit), edit)) + } + + fun `test edit editors are disposed after churn`() { + val base = EditorFactory.getInstance().allEditors.size + + repeat(40) { i -> + val view = EditToolView(tool().also { it.metadata = mapOf("diff" to patch(i)) }) + view.toggle() + view.codeEditors().forEach { it.getEditor(true) } + Disposer.dispose(view) + } + UIUtil.dispatchAllInvocationEvents() + + assertEquals(base, EditorFactory.getInstance().allEditors.size) + } + + fun `test multi file patch editors are disposed after churn`() { + val base = EditorFactory.getInstance().allEditors.size + + repeat(20) { i -> + val view = EditToolView(tool().also { + it.input = emptyMap() + it.metadata = mapOf("files" to filesMeta( + FileChange("src/A$i.kt", 2, 0, ADD_HUNK), + FileChange("src/B$i.kt", 1, 1, UPDATE_HUNK), + )) + }) + view.toggle() + view.codeEditors().forEach { it.getEditor(true) } + Disposer.dispose(view) + } + UIUtil.dispatchAllInvocationEvents() + + assertEquals(base, EditorFactory.getInstance().allEditors.size) + } + + private fun track(view: EditToolView): EditToolView { + views.add(view) + return view + } + + private fun click(component: Component, x: Int) { + component.dispatchEvent(MouseEvent(component, MouseEvent.MOUSE_CLICKED, System.currentTimeMillis(), 0, x, 1, 1, false)) + } + + private fun linkLabel(view: EditToolView): JBLabel = + labels(view).first { it.text?.contains("") == true } + + private fun labels(root: Container): List = root.components.flatMap { child -> + val nested = if (child is Container) labels(child) else emptyList() + if (child is JBLabel) nested + child else nested + } + + private fun badges(root: Container): List = root.components.flatMap { child -> + val nested = if (child is Container) badges(child) else emptyList() + if (child is DiffStatBadge) nested + child else nested + } + + private fun tool() = Tool("p1", "edit", toolKind("edit")).also { + it.state = ToolExecState.COMPLETED + it.title = "src/App.kt" + it.input = mapOf("filePath" to "/repo/src/App.kt") + it.output = "Edit applied successfully." + it.metadata = mapOf("filediff" to fileDiff(2, 1, PATCH)) + } + + private fun write(name: String) = Tool("p1", name, toolKind(name)).also { + it.state = ToolExecState.COMPLETED + it.input = mapOf("filePath" to "/repo/src/App.kt") + it.metadata = mapOf("filediff" to fileDiff(2, 1, PATCH)) + } + + private fun patch(i: Int) = """ + --- src/App.kt + +++ src/App.kt + @@ -1,2 +1,2 @@ + line$i + -old$i + +new$i + """.trimIndent() + + private data class FileChange(val path: String, val additions: Int, val deletions: Int, val patch: String) + + // Mirrors how the CLI serializes metadata.files (a JsonArray of per-file changes rendered to string). + private fun filesMeta(vararg files: FileChange): String = buildJsonArray { + files.forEach { file -> + addJsonObject { + put("relativePath", file.path) + put("type", "update") + put("additions", file.additions) + put("deletions", file.deletions) + put("patch", file.patch) + } + } + }.toString() + + // Mirrors how the CLI serializes metadata.filediff (a JsonObject rendered to string). + private fun fileDiff( + additions: Int, + deletions: Int, + patch: String, + path: String = "src/App.kt", + ): String = buildJsonObject { + put("file", path) + put("additions", additions) + put("deletions", deletions) + put("patch", patch) + }.toString() + + companion object { + private val PATCH = """ + --- src/App.kt + +++ src/App.kt + @@ -1,3 +1,4 @@ + line1 + -old + +new1 + +new2 + line3 + """.trimIndent() + + private val ADD_HUNK = """ + @@ -0,0 +1,2 @@ + +alpha + +beta + """.trimIndent() + + private val UPDATE_HUNK = """ + @@ -1,2 +1,2 @@ + keep + -old + +new + """.trimIndent() + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReadToolViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReadToolViewTest.kt index aef4c8ded13..685059de343 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReadToolViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReadToolViewTest.kt @@ -51,6 +51,7 @@ class ReadToolViewTest : BasePlatformTestCase() { assertTrue(view.linkVisible()) assertEquals("SessionUiLayoutTest.kt", view.linkText()) assertEquals(path, view.linkHref()) + assertEquals(path, view.linkTooltip()) assertTrue(view.linkMarkup().contains("SessionUiLayoutTest.kt")) assertEquals(UiStyle.Colors.fg().rgb, view.linkForeground().rgb) assertEquals(view.linkFont(), view.bodyFont()) @@ -75,6 +76,7 @@ class ReadToolViewTest : BasePlatformTestCase() { assertFalse(view.linkVisible()) assertNull(view.linkHref()) + assertNull(view.linkTooltip()) assertEquals(UiStyle.Colors.fg().rgb, view.subtitleForeground().rgb) assertEquals(view.subtitleFont(), view.bodyFont()) assertTrue(view.labelText().contains(path)) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt index 63972f05ab0..a35c86b0ba8 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ReasoningViewTest.kt @@ -290,8 +290,8 @@ class ReasoningViewTest : BasePlatformTestCase() { val panel = scroll.viewport.view as JPanel assertEquals(1, panel.components.filterIsInstance().size) - assertTrue(body.component.preferredSize.width in 1..JBUI.scale(350)) - assertEquals(JBUI.scale(450), body.component.preferredSize.height) + assertTrue(body.component.preferredSize.width in 1..JBUI.scale(SessionUiStyle.View.Popup.MAX_WIDTH)) + assertEquals(JBUI.scale(SessionUiStyle.View.Popup.MAX_HEIGHT), body.component.preferredSize.height) } finally { Disposer.dispose(body.disposable) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ShellToolViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ShellToolViewTest.kt index f9f05fd33d3..03cd9d4d2ba 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ShellToolViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ShellToolViewTest.kt @@ -45,7 +45,7 @@ class ShellToolViewTest : BasePlatformTestCase() { assertEquals("pwd", view.bodyText()) view.toggle() - assertEquals("**Command**\n\n```shell-command\npwd\n```", view.markdown()) + assertEquals("**Command**\n\n```bash\npwd\n```", view.markdown()) assertEquals(listOf("pwd"), view.codeTexts()) } @@ -73,7 +73,7 @@ class ShellToolViewTest : BasePlatformTestCase() { view.toggle() assertEquals( - "**Command**\n\n```shell-command\ngit status\n```\n\n**Output**\n\n```shell-output\nclean\n```", + "**Command**\n\n```bash\ngit status\n```\n\n**Output**\n\n```shell-output\nclean\n```", view.markdown(), ) assertEquals(listOf("git status", "clean"), view.codeTexts()) @@ -91,7 +91,7 @@ class ShellToolViewTest : BasePlatformTestCase() { assertEquals("printf 'one\ntwo'\n\none\ntwo", view.bodyText()) view.toggle() - assertTrue(view.markdown().contains("```shell-command\nprintf 'one\ntwo'\n```")) + assertTrue(view.markdown().contains("```bash\nprintf 'one\ntwo'\n```")) assertTrue(view.markdown().contains("```shell-output\none\ntwo\n```")) } @@ -155,7 +155,7 @@ class ShellToolViewTest : BasePlatformTestCase() { view.toggle() assertEquals( - "**Command**\n\n```shell-command\nfail\n```\n\n**Error**\n\n```ansi-stderr\nboom\n```", + "**Command**\n\n```bash\nfail\n```\n\n**Error**\n\n```ansi-stderr\nboom\n```", view.markdown(), ) assertEquals(listOf("fail", "boom"), view.codeTexts()) @@ -305,8 +305,8 @@ class ShellToolViewTest : BasePlatformTestCase() { field.text.substring(it.startOffset, it.endOffset) to it.textAttributesKey } - assertTrue(view.markdown().contains("```shell-command\ngit log -30 --oneline --decorate\n```")) - assertTrue(spans.contains("git" to DefaultLanguageHighlighterColors.FUNCTION_CALL)) + assertTrue(view.markdown().contains("```bash\ngit log -30 --oneline --decorate\n```")) + assertTrue(spans.contains("git" to DefaultLanguageHighlighterColors.KEYWORD)) assertTrue(spans.contains("-30" to DefaultLanguageHighlighterColors.KEYWORD)) assertTrue(spans.contains("--oneline" to DefaultLanguageHighlighterColors.KEYWORD)) assertTrue(spans.contains("--decorate" to DefaultLanguageHighlighterColors.KEYWORD)) @@ -416,9 +416,9 @@ class ShellToolViewTest : BasePlatformTestCase() { assertTrue(field.preferredSize.height - border.top >= editor.lineHeight * lines) assertTrue(field.minimumSize.height - border.top >= editor.lineHeight * lines) assertTrue(pane.preferredSize.height >= field.preferredSize.height + pad.top + pad.bottom) - assertTrue(body.component.preferredSize.width in 1..JBUI.scale(350)) + assertTrue(body.component.preferredSize.width in 1..JBUI.scale(SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)) assertTrue(body.component.preferredSize.height > 0) - assertTrue(body.component.preferredSize.height <= JBUI.scale(450)) + assertTrue(body.component.preferredSize.height <= JBUI.scale(SessionUiStyle.View.Popup.MAX_HEIGHT)) } finally { Disposer.dispose(body.disposable) } @@ -427,6 +427,33 @@ class ShellToolViewTest : BasePlatformTestCase() { assertEquals(base, EditorFactory.getInstance().allEditors.size) } + fun `test shell header popup widens to command content`() { + val view = track(ShellToolView(tool().also { + it.input = mapOf("command" to "echo ${"x".repeat(180)}") + })) + val body = view.headerPopup()!!.build() + + try { + assertTrue(body.component.preferredSize.width > JBUI.scale(SessionUiStyle.View.Popup.MAX_WIDTH)) + assertTrue(body.component.preferredSize.width <= JBUI.scale(SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)) + } finally { + Disposer.dispose(body.disposable) + } + } + + fun `test shell header popup stays narrow for short command`() { + val view = track(ShellToolView(tool().also { + it.input = mapOf("command" to "ls") + })) + val body = view.headerPopup()!!.build() + + try { + assertTrue(body.component.preferredSize.width < JBUI.scale(SessionUiStyle.View.Popup.WIDE_MAX_WIDTH)) + } finally { + Disposer.dispose(body.disposable) + } + } + fun `test shell header popup breaks chained operators outside quotes`() { val view = track(ShellToolView(tool().also { it.input = mapOf( diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolBodyStressTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolBodyStressTest.kt index f2595428c43..7682e51ba59 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolBodyStressTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolBodyStressTest.kt @@ -3,6 +3,7 @@ package ai.kilocode.client.session.views import ai.kilocode.client.session.model.Tool import ai.kilocode.client.session.model.ToolExecState import ai.kilocode.client.session.model.toolKind +import ai.kilocode.client.session.views.tool.EditToolView import ai.kilocode.client.session.views.tool.GlobToolView import ai.kilocode.client.session.views.tool.SearchToolView import ai.kilocode.client.session.views.tool.ShellToolView @@ -11,6 +12,8 @@ import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.util.Disposer import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.ui.UIUtil +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put @Suppress("UnstableApiUsage") class ToolBodyStressTest : BasePlatformTestCase() { @@ -62,11 +65,46 @@ class ToolBodyStressTest : BasePlatformTestCase() { assertEquals(base, EditorFactory.getInstance().allEditors.size) } + fun `test expanded edit tool editors are disposed after churn`() { + val base = EditorFactory.getInstance().allEditors.size + + repeat(60) { i -> + val view = EditToolView(edit(i)) + view.toggle() + view.codeEditors().forEach { it.getEditor(true) } + Disposer.dispose(view) + } + drainEdt() + + assertEquals(base, EditorFactory.getInstance().allEditors.size) + } + private fun tool(index: Int) = Tool("p$index", "mystery", toolKind("mystery")).also { it.state = ToolExecState.COMPLETED it.output = (1..20).joinToString("\n") { line -> "line $index/$line" } } + private fun edit(index: Int) = Tool("e$index", "edit", toolKind("edit")).also { + it.state = ToolExecState.COMPLETED + it.input = mapOf("filePath" to "/repo/src/File$index.kt") + val patch = buildString { + append("--- src/File$index.kt\n") + append("+++ src/File$index.kt\n") + append("@@ -1,3 +1,4 @@\n") + append(" line1\n") + append("-old$index\n") + append("+new$index\n") + } + it.metadata = mapOf( + "filediff" to buildJsonObject { + put("file", "src/File$index.kt") + put("additions", 1) + put("deletions", 1) + put("patch", patch) + }.toString(), + ) + } + private fun shell(index: Int) = Tool("p$index", "bash", toolKind("bash")).also { it.state = ToolExecState.COMPLETED it.input = mapOf("command" to "log $index") diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt index e5b3b991a3a..777cf1157b8 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/ToolViewTest.kt @@ -9,6 +9,7 @@ import ai.kilocode.client.session.ui.style.SessionUiStyle import ai.kilocode.client.session.views.base.SecondarySessionPartView import ai.kilocode.client.session.views.tool.ToolView import ai.kilocode.client.ui.UiStyle +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors import com.intellij.openapi.util.Disposer import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.ui.scale.JBUIScale @@ -99,6 +100,24 @@ class ToolViewTest : BasePlatformTestCase() { assertTrue(view.bodyCreated()) } + fun `test bash tool editor highlights command text`() { + val t = tool("p1", "bash", ToolExecState.COMPLETED).also { + it.input = mapOf("command" to "git remote -v", "description" to "View remotes") + it.output = "origin git@example.com:repo.git" + } + val view = track(ToolView(t)) + + view.toggle() + val field = view.bodyEditor()!! + val editor = field.getEditor(true)!! + val spans = editor.markupModel.allHighlighters.map { + field.text.substring(it.startOffset, it.endOffset) to it.textAttributesKey + } + + assertTrue(spans.contains("git" to DefaultLanguageHighlighterColors.KEYWORD)) + assertTrue(spans.contains("-v" to DefaultLanguageHighlighterColors.KEYWORD)) + } + fun `test bash tool uses secondary chrome`() { val view = ToolView(tool("p1", "bash", ToolExecState.COMPLETED)) val base: Any = view diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt index 838cead1606..6018178065f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/base/BaseQuestionViewTest.kt @@ -275,6 +275,7 @@ class BaseQuestionViewTest : BasePlatformTestCase() { val icon = west as JBLabel assertEquals("icon should be horizontally centered", JBLabel.CENTER, icon.horizontalAlignment) assertEquals("icon should be vertically centered", JBLabel.CENTER, icon.verticalAlignment) + assertEquals("icon gap should use the next standard spacing step", UiStyle.Gap.md(), layout.hgap) assertTrue("center should contain header and description text", findAll(center).size >= 2) } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt index 6b52bef0020..83cec9799cf 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/views/permission/PermissionViewTest.kt @@ -1,34 +1,52 @@ package ai.kilocode.client.session.views.permission +import ai.kilocode.client.plugin.KiloPluginSettings import ai.kilocode.client.session.model.Permission import ai.kilocode.client.session.model.PermissionFileDiff import ai.kilocode.client.session.model.PermissionMeta import ai.kilocode.client.session.model.PermissionRequestState -import ai.kilocode.client.session.views.SessionViewIcons +import ai.kilocode.client.session.model.PermissionRuleCandidate +import ai.kilocode.client.session.model.PermissionRuleDecision import ai.kilocode.client.session.views.base.BaseQuestionView import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionUiStyle -import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.ui.md.MdCommon +import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto import ai.kilocode.rpc.dto.PermissionReplyDto +import com.intellij.icons.AllIcons import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.EditorFactory import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.ui.components.JBLabel +import com.intellij.util.ui.UIUtil import java.awt.Container import javax.swing.AbstractButton +import javax.swing.SwingUtilities @Suppress("UnstableApiUsage") class PermissionViewTest : BasePlatformTestCase() { - private val replies = mutableListOf>() + private val replies = mutableListOf>() private lateinit var view: PermissionView override fun setUp() { super.setUp() + KiloPluginSettings.unsetPermissionRulesExpanded() view = PermissionView( - reply = { id, dto -> replies.add(id to dto) }, + reply = { id, dto, rules -> replies.add(Triple(id, dto, rules)) }, ) } + override fun tearDown() { + try { + view.dispose() + KiloPluginSettings.unsetPermissionRulesExpanded() + } finally { + super.tearDown() + } + } + fun `test run button replies once`() { view.show(permission()) @@ -37,6 +55,7 @@ class PermissionViewTest : BasePlatformTestCase() { assertEquals(1, replies.size) assertEquals("perm1", replies.single().first) assertEquals("once", replies.single().second.reply) + assertNull(replies.single().third) assertFalse(view.runButtonForTest().isEnabled) assertFalse(view.denyButtonForTest().isEnabled) } @@ -49,6 +68,7 @@ class PermissionViewTest : BasePlatformTestCase() { assertEquals(1, replies.size) assertEquals("perm1", replies.single().first) assertEquals("reject", replies.single().second.reply) + assertNull(replies.single().third) } fun `test view is visible after show`() { @@ -99,7 +119,7 @@ class PermissionViewTest : BasePlatformTestCase() { assertTrue("Expected no code labels for star-only patterns", view.codeLabelsForTest().isEmpty()) } - fun `test bash permission shows action and command on same row`() { + fun `test bash permission shows action and command editor`() { view.show( Permission( id = "perm4", @@ -113,10 +133,15 @@ class PermissionViewTest : BasePlatformTestCase() { val text = allText(view) assertTrue("Expected Shell action label in text, got: $text", text.contains("Shell")) - assertTrue("Expected command in text, got: $text", text.contains("git status --short")) val labels = view.codeLabelsForTest() - assertEquals("Expected exactly one target pane for command", 1, labels.size) - assertTrue("Expected command in target pane, got: ${labels[0].text}", labels[0].text.contains("git status --short")) + assertEquals("Expected exactly one command editor", 1, labels.size) + assertTrue("Expected command in editor, got: ${labels[0].text}", labels[0].text.contains("git status --short")) + val editor = labels[0].getEditor(true)!! + val spans = editor.markupModel.allHighlighters.map { + labels[0].text.substring(it.startOffset, it.endOffset) to it.textAttributesKey + } + assertTrue(spans.contains("git" to DefaultLanguageHighlighterColors.KEYWORD)) + assertTrue(spans.contains("--short" to DefaultLanguageHighlighterColors.KEYWORD)) } fun `test bash permission shows only header and compact detail`() { @@ -134,12 +159,12 @@ class PermissionViewTest : BasePlatformTestCase() { val text = allText(view) assertTrue("Expected permission header, got: $text", text.contains("Permission required")) - assertTrue("Expected command in text, got: $text", text.contains("git status --short")) + assertTrue("Expected command editor text", view.codeLabelsForTest().single().text.contains("git status --short")) // State message should not appear for PENDING state assertFalse("Should not show state message for PENDING, got: $text", text.contains("Run this command?")) } - fun `test non-bash patterns show action and path as separate labels`() { + fun `test non-bash patterns show action and path in editor`() { view.show( Permission( id = "perm5", @@ -153,11 +178,10 @@ class PermissionViewTest : BasePlatformTestCase() { val text = allText(view) assertTrue("Expected 'Read' in text, got: $text", text.contains("Read")) - assertTrue("Expected path in text, got: $text", text.containsPath("src/App.kt")) val labels = view.codeLabelsForTest() - assertEquals("Expected exactly one target pane for the pattern", 1, labels.size) - assertTrue("Expected path in target pane, got: ${labels[0].text}", labels[0].text.containsPath("src/App.kt")) + assertEquals("Expected exactly one target editor for the pattern", 1, labels.size) + assertTrue("Expected path in editor, got: ${labels[0].text}", labels[0].text.containsPath("src/App.kt")) } fun `test multiple patterns joined in code label`() { @@ -200,8 +224,8 @@ class PermissionViewTest : BasePlatformTestCase() { ) val text = allText(view) - assertTrue("Should render target file once, got: $text", text.containsPath("src/A.kt")) - assertEquals("Should not duplicate target file path, got: $text", 1, pathOccurrences(text, "src/A.kt")) + assertTrue("Should render target file in editor", view.codeLabelsForTest().single().text.containsPath("src/A.kt")) + assertEquals("Should render target file once in labels, got: $text", 1, pathOccurrences(text, "src/A.kt")) // Patch markers should NOT appear — no diff content is shown assertFalse("Should not render patch content, got: $text", text.contains("@@")) assertFalse("Should not render old line, got: $text", text.contains("-old")) @@ -237,8 +261,8 @@ class PermissionViewTest : BasePlatformTestCase() { ) val text = allText(view) - assertTrue("Should render target file once, got: $text", text.containsPath("src/A.kt")) - assertEquals("Should not duplicate target file path, got: $text", 1, pathOccurrences(text, "src/A.kt")) + assertTrue("Should render target file in editor", view.codeLabelsForTest().single().text.containsPath("src/A.kt")) + assertEquals("Should render target file once in labels, got: $text", 1, pathOccurrences(text, "src/A.kt")) // No "unavailable" fallback text expected in new design assertFalse("Should not render unavailable fallback, got: $text", text.contains("unavailable")) val badge = view.diffViewsForTest().single().badgeForTest() @@ -284,7 +308,7 @@ class PermissionViewTest : BasePlatformTestCase() { assertFalse("Should not render patch markers, got: $text", text.contains("@@")) } - fun `test no rule controls rendered`() { + fun `test rule controls render collapsed when candidates exist`() { view.show( Permission( id = "perm7", @@ -292,15 +316,53 @@ class PermissionViewTest : BasePlatformTestCase() { name = "edit", patterns = listOf("*.kt"), always = listOf("src/**"), - meta = PermissionMeta(rules = listOf("rule1")), + meta = PermissionMeta( + ruleDecisions = listOf( + PermissionRuleCandidate("*.kt", defaultDecision = PermissionRuleDecision.DENIED), + PermissionRuleCandidate("src/**", PermissionRuleDecision.APPROVED), + ), + ), ) ) val text = allText(view) - assertFalse("Should not contain 'Manage Auto-Approve Rules'", text.contains("Manage Auto-Approve Rules")) - // Only Run and Deny buttons — not extra rule toggle buttons - val btns = buttons(view) - assertEquals("Expected exactly 2 buttons (Run and Deny)", 2, btns.size) + assertTrue("Should contain rules title, got: $text", text.contains("Auto-approve Rules")) + assertFalse("Rules should be collapsed by default", view.rulesForTest().isExpanded()) + assertTrue("Rules body should be lazy", view.rulesForTest().commandFieldsForTest().isEmpty()) + + view.rulesForTest().toggle() + + val approve = view.rulesForTest().approveButtonsForTest() + val deny = view.rulesForTest().denyButtonsForTest() + assertEquals(2, approve.size) + assertEquals(2, deny.size) + assertEquals("Add to allowed", approve[0].toolTipText) + assertEquals("Remove from allowed", approve[1].toolTipText) + assertEquals("Add to denied", deny[1].toolTipText) + val commands = view.rulesForTest().commandFieldsForTest() + assertEquals(2, commands.size) + assertEquals("*.kt", commands[0].text) + assertEquals("src/**", commands[1].text) + layoutTree(view) + val hintY = SwingUtilities.convertPoint(view.rulesForTest().hintLabelsForTest()[0], 0, 0, view).y + val fieldY = SwingUtilities.convertPoint(commands[0], 0, 0, view).y + assertTrue("Rule hint should render below the controls row", hintY > fieldY) + assertTrue(allText(view).contains("Future matching calls will use the default permission setting: Reject.")) + assertTrue(allText(view).contains("This request and future matching calls will be allowed.")) + assertEquals("Allow once", view.runButtonForTest().text) + assertEquals("Reject", view.denyButtonForTest().text) + assertEquals("Expected exactly 6 buttons including rule toggles", 6, buttons(view).size) + view.runButtonForTest().doClick() + assertNull(replies.single().third) + } + + fun `test no rule controls render when no candidates`() { + view.show(permission()) + + val text = allText(view) + assertFalse("Should not contain rules title, got: $text", text.contains("Auto-approve Rules")) + assertFalse(view.rulesForTest().isVisible) + assertEquals("Allow once", view.runButtonForTest().text) } fun `test responding state disables buttons`() { @@ -320,6 +382,27 @@ class PermissionViewTest : BasePlatformTestCase() { assertFalse(view.denyButtonForTest().isEnabled) } + fun `test responding state keeps buttons disabled when rules change`() { + view.show( + Permission( + id = "perm_responding_rules", + sessionId = "ses", + name = "bash", + patterns = listOf("git status"), + always = listOf("git status"), + meta = PermissionMeta( + ruleDecisions = listOf(PermissionRuleCandidate("git status")), + ), + state = PermissionRequestState.RESPONDING, + ) + ) + + view.rulesForTest().update(listOf(PermissionRuleCandidate("git status", PermissionRuleDecision.APPROVED))) + + assertFalse(view.runButtonForTest().isEnabled) + assertFalse(view.denyButtonForTest().isEnabled) + } + fun `test responding state shows responding message`() { view.show( Permission( @@ -384,6 +467,7 @@ class PermissionViewTest : BasePlatformTestCase() { assertEquals(1, replies.size) assertEquals("once", replies.single().second.reply) + assertNull(replies.single().third) } fun `test deny button uses bundle text and rejects`() { @@ -393,6 +477,165 @@ class PermissionViewTest : BasePlatformTestCase() { assertEquals(1, replies.size) assertEquals("reject", replies.single().second.reply) + assertNull(replies.single().third) + } + + fun `test approved rule changes label and replies with rules`() { + view.show( + Permission( + id = "perm_rules", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta( + command = "git add .", + ruleDecisions = listOf( + PermissionRuleCandidate("git *"), + PermissionRuleCandidate("git add *"), + ), + ), + ) + ) + + assertEquals("Allow once", view.runButtonForTest().text) + view.rulesForTest().toggle() + view.rulesForTest().approveButtonsForTest()[1].doClick() + + assertEquals("Allow", view.runButtonForTest().text) + assertEquals("Reject", view.denyButtonForTest().text) + assertTrue(view.runButtonForTest().isEnabled) + assertFalse(view.denyButtonForTest().isEnabled) + assertTrue(allText(view).contains("This request and future matching calls will be allowed.")) + view.runButtonForTest().doClick() + + assertEquals(1, replies.size) + assertEquals("perm_rules", replies.single().first) + assertEquals("once", replies.single().second.reply) + assertEquals(listOf("git add *"), replies.single().third?.approvedAlways) + assertEquals(emptyList(), replies.single().third?.deniedAlways) + } + + fun `test denied rule changes label and replies with denied rules`() { + view.show( + Permission( + id = "perm_deny_rules", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta( + command = "git clean -fd", + ruleDecisions = listOf(PermissionRuleCandidate("git clean *")), + ), + ) + ) + + view.rulesForTest().toggle() + view.rulesForTest().approveButtonsForTest()[0].doClick() + assertEquals("Remove from allowed", view.rulesForTest().approveButtonsForTest()[0].toolTipText) + view.rulesForTest().denyButtonsForTest()[0].doClick() + + assertEquals("Remove from denied", view.rulesForTest().denyButtonsForTest()[0].toolTipText) + assertEquals("Allow", view.runButtonForTest().text) + assertEquals("Reject", view.denyButtonForTest().text) + assertFalse(view.runButtonForTest().isEnabled) + assertTrue(view.denyButtonForTest().isEnabled) + assertTrue(allText(view).contains("This request and future matching calls will be rejected.")) + view.denyButtonForTest().doClick() + + assertEquals("reject", replies.single().second.reply) + assertEquals(emptyList(), replies.single().third?.approvedAlways) + assertEquals(listOf("git clean *"), replies.single().third?.deniedAlways) + } + + fun `test reject with changed rules replies with rules`() { + view.show( + Permission( + id = "perm_reject_rules", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta( + command = "git push", + ruleDecisions = listOf(PermissionRuleCandidate("git push *")), + ), + ) + ) + + view.rulesForTest().toggle() + view.rulesForTest().denyButtonsForTest()[0].doClick() + view.denyButtonForTest().doClick() + + assertEquals("reject", replies.single().second.reply) + assertEquals(emptyList(), replies.single().third?.approvedAlways) + assertEquals(listOf("git push *"), replies.single().third?.deniedAlways) + } + + fun `test active rule toggle clears back to allow once`() { + view.show( + Permission( + id = "perm_clear_rules", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta( + command = "git status", + ruleDecisions = listOf(PermissionRuleCandidate("git status")), + ), + ) + ) + + view.rulesForTest().toggle() + view.rulesForTest().approveButtonsForTest()[0].doClick() + view.rulesForTest().approveButtonsForTest()[0].doClick() + + assertEquals("Add to allowed", view.rulesForTest().approveButtonsForTest()[0].toolTipText) + assertEquals("Allow once", view.runButtonForTest().text) + view.runButtonForTest().doClick() + assertNull(replies.single().third) + } + + fun `test rules expansion persists for new view`() { + view.show( + Permission( + id = "perm_persist_rules", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta( + command = "pwd", + ruleDecisions = listOf(PermissionRuleCandidate("pwd")), + ), + ) + ) + + view.rulesForTest().toggle() + assertTrue(KiloPluginSettings.getPermissionRulesExpanded()) + + val next = PermissionView(reply = { id, dto, rules -> replies.add(Triple(id, dto, rules)) }) + try { + next.show( + Permission( + id = "perm_persist_rules_next", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta( + command = "pwd", + ruleDecisions = listOf(PermissionRuleCandidate("pwd")), + ), + ) + ) + assertTrue(next.rulesForTest().isExpanded()) + assertEquals(1, next.rulesForTest().commandFieldsForTest().size) + } finally { + next.dispose() + } } // ------ shared card shell ------ @@ -410,10 +653,30 @@ class PermissionViewTest : BasePlatformTestCase() { val labels = findAll(view) assertTrue( "Expected permission warning icon in header", - labels.any { it.icon == SessionViewIcons.warning }, + labels.any { it.icon == AllIcons.General.Warning }, ) } + fun `test permission description renders as content row`() { + view.show( + Permission( + id = "perm_desc", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta( + command = "bun test", + raw = mapOf("description" to "Run the targeted tests"), + ), + ) + ) + + val text = allText(view) + assertTrue("Expected description content row, got: $text", text.contains("Run the targeted tests")) + assertTrue("Expected permission header, got: $text", text.contains("Permission required")) + } + // ------ button types ------ fun `test run button uses default style key`() { @@ -456,8 +719,8 @@ class PermissionViewTest : BasePlatformTestCase() { val labels = view.codeLabelsForTest() assertNotNull("Should have at least one code label for command", labels.firstOrNull()) - assertEquals("Code label font family should use transcript family", style.transcriptFont.name, labels[0].font.name) - assertEquals(style.transcriptFont.size, labels[0].font.size) + assertEquals("Code label font family should use editor family", style.editorFont.name, labels[0].font.name) + assertEquals(style.editorFont.size, labels[0].font.size) } fun `test permission header uses headerFont not editor font family`() { @@ -480,7 +743,7 @@ class PermissionViewTest : BasePlatformTestCase() { assertEquals("Permission header should equal headerFont", style.headerFont, header) } - fun `test code label uses code background`() { + fun `test command editor uses markdown code block background`() { view.show( Permission( id = "perm_bg", @@ -494,7 +757,124 @@ class PermissionViewTest : BasePlatformTestCase() { val labels = view.codeLabelsForTest() assertFalse("Expected code labels", labels.isEmpty()) - assertEquals(SessionUiStyle.View.Surface.headerHoverBgColor(), labels[0].background) + assertEquals(MdCommon.defaults(SessionEditorStyle.current()).preBg, labels[0].background) + } + + fun `test command editor is retained and disposed`() { + val base = EditorFactory.getInstance().allEditors.size + view.show( + Permission( + id = "perm_retain", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta(command = "git status"), + ) + ) + val editor = view.codeLabelsForTest().single() + editor.getEditor(true) + val count = EditorFactory.getInstance().allEditors.size + + repeat(40) { i -> + view.show( + Permission( + id = "perm_retain", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta(command = "echo $i"), + state = if (i % 2 == 0) PermissionRequestState.PENDING else PermissionRequestState.RESPONDING, + ) + ) + assertSame(editor, view.codeLabelsForTest().single()) + view.codeLabelsForTest().single().getEditor(true) + assertEquals(count, EditorFactory.getInstance().allEditors.size) + } + + view.hideView() + UIUtil.dispatchAllInvocationEvents() + + assertTrue(view.codeLabelsForTest().isEmpty()) + assertEquals(base, EditorFactory.getInstance().allEditors.size) + } + + fun `test rule command field uses editor font after applyStyle`() { + view.show( + Permission( + id = "perm_rule_codefont", + sessionId = "ses", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta( + command = "git log --oneline -10", + ruleDecisions = listOf(PermissionRuleCandidate("git log *")), + ), + ) + ) + val style = SessionEditorStyle.create(family = "Courier New", size = 18) + view.applyStyle(style) + view.rulesForTest().toggle() + + val field = view.rulesForTest().commandFieldsForTest().single() + assertEquals("git log *", field.text) + assertEquals(style.editorFont.name, field.font.name) + assertEquals(style.editorFont.size, field.font.size) + } + + fun `test rule command fields are retained and disposed`() { + val base = EditorFactory.getInstance().allEditors.size + view.show(permissionWithRules("perm_rules_retain", listOf("git status *", "git push *"))) + view.rulesForTest().toggle() + + val fields = view.rulesForTest().commandFieldsForTest() + assertEquals(2, fields.size) + fields.forEach { it.getEditor(true) } + val count = EditorFactory.getInstance().allEditors.size + val components = componentCount(view.rulesForTest()) + + repeat(40) { + view.show(permissionWithRules("perm_rules_retain", listOf("git status *", "git push *"))) + + val next = view.rulesForTest().commandFieldsForTest() + assertEquals(2, next.size) + assertSame(fields[0], next[0]) + assertSame(fields[1], next[1]) + assertEquals(components, componentCount(view.rulesForTest())) + next.forEach { it.getEditor(true) } + assertEquals(count, EditorFactory.getInstance().allEditors.size) + } + + view.hideView() + UIUtil.dispatchAllInvocationEvents() + + assertTrue(view.rulesForTest().commandFieldsForTest().isEmpty()) + assertEquals(base, EditorFactory.getInstance().allEditors.size) + } + + fun `test stale rule command fields are released on rebuild`() { + val base = EditorFactory.getInstance().allEditors.size + view.show(permissionWithRules("perm_rules_rebuild", listOf("git status *"))) + view.rulesForTest().toggle() + view.rulesForTest().commandFieldsForTest().single().getEditor(true) + val count = EditorFactory.getInstance().allEditors.size + + repeat(20) { i -> + view.show(permissionWithRules("perm_rules_rebuild", listOf("git command $i *"))) + + val fields = view.rulesForTest().commandFieldsForTest() + assertEquals(1, fields.size) + fields.single().getEditor(true) + assertEquals(count, EditorFactory.getInstance().allEditors.size) + } + + view.hideView() + UIUtil.dispatchAllInvocationEvents() + + assertTrue(view.rulesForTest().commandFieldsForTest().isEmpty()) + assertEquals(base, EditorFactory.getInstance().allEditors.size) } private fun permission() = Permission( @@ -507,6 +887,18 @@ class PermissionViewTest : BasePlatformTestCase() { message = "Review file changes", ) + private fun permissionWithRules(id: String, patterns: List) = Permission( + id = id, + sessionId = "ses_test", + name = "bash", + patterns = emptyList(), + always = emptyList(), + meta = PermissionMeta( + command = "git status", + ruleDecisions = patterns.map { PermissionRuleCandidate(it) }, + ), + ) + private fun buttons(root: Container): List = root.components.flatMap { comp -> val item = if (comp is AbstractButton) listOf(comp) else emptyList() if (comp is Container) item + buttons(comp) else item @@ -515,6 +907,7 @@ class PermissionViewTest : BasePlatformTestCase() { private fun allText(root: Container): String = buildString { fun collect(c: Container) { for (comp in c.components) { + if (!comp.isVisible) continue if (comp is javax.swing.text.JTextComponent) append(comp.text).append(" ") if (comp is javax.swing.JLabel) append(comp.text).append(" ") if (comp is AbstractButton) append(comp.text).append(" ") @@ -524,6 +917,20 @@ class PermissionViewTest : BasePlatformTestCase() { collect(root) } + private fun layoutTree(root: Container) { + root.setSize(900, 600) + fun layout(node: Container) { + node.doLayout() + for (child in node.components) { + if (child is Container) layout(child) + } + } + layout(root) + } + + private fun componentCount(root: Container): Int = + 1 + root.components.sumOf { if (it is Container) componentCount(it) else 1 } + private fun occurrences(text: String, token: String): Int { if (token.isEmpty()) return 0 return text.split(token).size - 1 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt index 2015edebba2..5b7a77d0a74 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/KiloSettingsConfigurableTest.kt @@ -4,7 +4,9 @@ import ai.kilocode.client.settings.profile.UserProfileConfigurable import ai.kilocode.client.settings.context.ContextConfigurable import ai.kilocode.client.settings.models.ModelsConfigurable import ai.kilocode.client.settings.agents.AgentBehaviorConfigurable +import ai.kilocode.client.settings.autoapprove.AutoApproveConfigurable import ai.kilocode.client.settings.providers.ProvidersConfigurable +import ai.kilocode.client.settings.rules.RulesConfigurable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.SearchableConfigurable @@ -37,6 +39,16 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() { fun `test child provider and behavior ids match xml registration`() { assertEquals("ai.kilocode.jetbrains.settings.providers", ProvidersConfigurable.ID) assertEquals("ai.kilocode.jetbrains.settings.agentBehavior", AgentBehaviorConfigurable.ID) + assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.rules", RulesConfigurable.ID) + } + + fun `test auto approve opts out of platform scrollpane`() { + // Auto-Approve renders its own fixed search field and scrollable body, so it must not be + // wrapped in the platform configurable scrollpane. + val auto: Configurable = AutoApproveConfigurable() + val context: Configurable = ContextConfigurable() + assertTrue(auto is Configurable.NoScroll) + assertTrue(context is Configurable.NoScroll) } fun `test root implements SearchableConfigurable but not Parent`() { @@ -98,7 +110,7 @@ class KiloSettingsConfigurableTest : BasePlatformTestCase() { edt { val panel = cfg.createComponent() val labels = links(panel as Container).map { it.text } - assertEquals(listOf("User Profile", "Models", "Providers", "Agent Behavior", "Context"), labels) + assertEquals(listOf("User Profile", "Models", "Providers", "Agent Behavior", "Auto-Approve", "Context"), labels) } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt index b0f60db7d21..42153250d21 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/UserProfileConfigurableTest.kt @@ -6,6 +6,7 @@ import ai.kilocode.client.settings.profile.formatResetDate import ai.kilocode.client.settings.profile.formatShortBalance import ai.kilocode.client.testing.FakeAppRpcApi import ai.kilocode.client.testing.TestCoroutines +import ai.kilocode.client.ui.UiStyle import ai.kilocode.rpc.dto.DeviceAuthDto import ai.kilocode.rpc.dto.KiloAppStateDto import ai.kilocode.rpc.dto.KiloAppStatusDto @@ -92,6 +93,13 @@ class UserProfileConfigurableTest : BasePlatformTestCase() { assertEquals(listOf("https://auth.kilo.ai/device"), urls) } + fun `test profile page has standard horizontal content padding`() { + edt { + assertEquals(UiStyle.Gap.xl(), panel.insets.left) + assertEquals(UiStyle.Gap.xl(), panel.insets.right) + } + } + fun `test logout updates profile UI`() { val profile = ProfileDto(email = "alice@test.com", name = "Alice") rpc.fakeProfile = profile diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurableTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurableTest.kt index 5f79482b75c..08ba02deb04 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurableTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/AgentBehaviorConfigurableTest.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.settings.agents +import ai.kilocode.client.settings.rules.RulesConfigurable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.options.SearchableConfigurable import com.intellij.testFramework.fixtures.BasePlatformTestCase @@ -18,6 +19,8 @@ class AgentBehaviorConfigurableTest : BasePlatformTestCase() { fun `test child ids match xml registration`() { assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.agents", AgentsConfigurable.ID) assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.mcp", McpConfigurable.ID) + assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.skills", SkillsConfigurable.ID) + assertEquals("ai.kilocode.jetbrains.settings.agentBehavior.rules", RulesConfigurable.ID) } fun `test createComponent contains child links in order`() { @@ -26,7 +29,7 @@ class AgentBehaviorConfigurableTest : BasePlatformTestCase() { edt { val panel = cfg.createComponent() val labels = links(panel as Container).map { it.text } - assertEquals(listOf("Agents", "MCP Servers"), labels) + assertEquals(listOf("Agents", "MCP Servers", "Skills", "Rules"), labels) } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt new file mode 100644 index 00000000000..e47bc94b2f5 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/agents/SkillsSettingsUiTest.kt @@ -0,0 +1,621 @@ +package ai.kilocode.client.settings.agents + +import ai.kilocode.client.app.KiloAgentBehaviorService +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.settings.base.SettingsListItem +import ai.kilocode.client.settings.base.SettingsPathDialogHandle +import ai.kilocode.client.settings.base.settingsListCellBounds +import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi +import ai.kilocode.client.testing.FakeAppRpcApi +import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.client.testing.fire +import ai.kilocode.rpc.dto.ConfigDto +import ai.kilocode.rpc.dto.KiloAppStateDto +import ai.kilocode.rpc.dto.KiloAppStatusDto +import ai.kilocode.rpc.dto.SkillDto +import ai.kilocode.rpc.dto.SkillsConfigDto +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.fileTypes.PlainTextFileType +import com.intellij.openapi.fileTypes.UnknownFileType +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.TestDialog +import com.intellij.openapi.ui.TestDialogManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.replaceService +import com.intellij.ui.TitledSeparator +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import java.awt.BorderLayout +import java.awt.Container +import java.awt.Dimension +import java.awt.Point +import java.awt.event.InputEvent +import java.awt.event.MouseEvent +import javax.swing.JComponent +import javax.swing.ScrollPaneConstants +import javax.swing.Scrollable +import javax.swing.JTextField + +class SkillsSettingsUiTest : BasePlatformTestCase() { + private var scope: CoroutineScope? = null + private var ui: SkillsSettingsUi? = null + private lateinit var app: KiloAppService + private lateinit var appRpc: FakeAppRpcApi + private lateinit var agentRpc: FakeAgentBehaviorRpcApi + private lateinit var workspaceRpc: FakeWorkspaceRpcApi + private var shown = 0 + + override fun tearDown() { + try { + TestDialogManager.setTestDialog(TestDialog.DEFAULT) + ui?.let { panel -> edt { panel.dispose(); true } } + ui = null + scope?.cancel() + scope = null + } finally { + super.tearDown() + } + } + + fun `test loads skills with location note and builtins have no actions`() { + val panel = panel() + + flushUntil { rows(panel).size == 3 } + + edt { + val rows = rows(panel) + val custom = rows.single { it.key == CUSTOM } + assertEquals("plan", custom.title) + assertEquals(CUSTOM, custom.note) + assertEquals("Plan work", custom.description) + assertEquals("edit", custom.doubleClick) + assertEquals(listOf("open", "edit", "delete"), custom.cells.map { it.id }) + assertTrue(custom.cells.single { it.id == "open" }.primary) + assertFalse(custom.cells.single { it.id == "edit" }.primary) + assertEquals("Edit", custom.cells.single { it.id == "edit" }.label) + assertTrue(custom.cells.single { it.id == "delete" }.iconOnly) + val builtin = rows.single { it.key == "builtin" } + assertEquals("thinking", builtin.title) + assertNull(builtin.note) + assertEquals("edit", builtin.doubleClick) + assertEquals(listOf("built-in"), builtin.badges.map { it.text }) + assertEquals(listOf("edit"), builtin.cells.map { it.id }) + assertEquals("Open", builtin.cells.single().label) + val remote = rows.single { it.key == REMOTE } + assertEquals(listOf("edit"), remote.cells.map { it.id }) + assertEquals("Open", remote.cells.single().label) + assertEquals(listOf(DIR), agentRpc.skillCalls) + true + } + } + + fun `test skills list is vertically scrolled without horizontal scrollbar`() { + val panel = panel() + flushUntil { rows(panel).size == 3 } + + edt { + val pane = scrollFor(panel, skillsList(panel)) + val view = pane.viewport.view + val layout = panel.content.layout as BorderLayout + + assertEquals(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, pane.horizontalScrollBarPolicy) + assertTrue((view as Scrollable).getScrollableTracksViewportWidth()) + assertFalse(view.getScrollableTracksViewportHeight()) + assertSame(pane, layout.getLayoutComponent(BorderLayout.CENTER)) + assertSame(panel.sources, layout.getLayoutComponent(BorderLayout.SOUTH)) + true + } + } + + fun `test sources section has additional sources title`() { + val panel = panel() + flushUntil { sourceRows(panel).size == 2 } + + assertTrue(edt { + components(panel).filterIsInstance().any { it.text == "Additional Skill Sources" } + }) + } + + fun `test skills list does not show description tooltips`() { + val panel = panel() + flushUntil { rows(panel).size == 3 } + + edt { + val list = skillsList(panel) + list.size = Dimension(520, 320) + list.doLayout() + val bounds = list.getCellBounds(0, 0) + + assertNull(list.getToolTipText(mouse(list, MouseEvent.MOUSE_MOVED, Point(bounds.x + 8, bounds.y + 8)))) + true + } + } + + fun `test renderer puts location on first line and description on preview line`() { + val panel = panel() + flushUntil { rows(panel).size == 3 } + + edt { + val list = skillsList(panel) + val row = rows(panel).single { it.key == CUSTOM } + val idx = rows(panel).indexOf(row) + val comp = list.cellRenderer.getListCellRendererComponent(list, row, idx, true, true) + comp.setSize(520, list.fixedCellHeight) + layout(comp) + val title = components(comp).filterIsInstance().single() + val labels = components(comp).filterIsInstance().filter { it.isVisible }.map { it.text } + + assertEquals("plan $CUSTOM", title.toString()) + assertTrue(labels.contains("Plan work")) + true + } + } + + fun `test double click stages skill content until apply`() { + val panel = panel(edit = { _, _ -> FakeSkillDialog("# Saved") }) + flushUntil { rows(panel).size == 3 } + + doubleClick(skillsList(panel), panel, CUSTOM) + + assertTrue(edt { panel.modified() }) + assertTrue(agentRpc.skillSaves.isEmpty()) + edt { panel.applyDraft(); true } + flushUntil { agentRpc.skillSaves.size == 1 } + assertEquals(Triple(DIR, CUSTOM, "# Saved"), agentRpc.skillSaves.single()) + } + + fun `test edited skill row keeps normal actions`() { + val panel = panel(edit = { _, _ -> FakeSkillDialog("# Draft") }) + flushUntil { rows(panel).size == 3 } + + doubleClick(skillsList(panel), panel, CUSTOM) + + assertEquals(listOf("open", "edit", "delete"), edt { rows(panel).single { it.key == CUSTOM }.cells.map { it.id } }) + assertTrue(edt { panel.modified() }) + } + + fun `test reopening staged skill edit shows draft content before apply`() { + val seen = mutableListOf() + val panel = panel(edit = { skill, _ -> + seen += skill.content + FakeSkillDialog(if (seen.size == 1) "# Draft" else "# Draft 2") + }) + flushUntil { rows(panel).size == 3 } + + doubleClick(skillsList(panel), panel, CUSTOM) + doubleClick(skillsList(panel), panel, CUSTOM) + + assertEquals(listOf("# Plan\nUse steps", "# Draft"), seen) + assertTrue(agentRpc.skillSaves.isEmpty()) + } + + fun `test open in editor action opens skill file`() { + val panel = panel() + flushUntil { rows(panel).size == 3 } + + click(skillsList(panel), panel, CUSTOM, "open") + + assertEquals("The skill file will open after you close Settings.", edt { progressText(panel) }) + flushUntil { workspaceRpc.openedFiles.size == 1 } + assertEquals(FakeWorkspaceRpcApi.Opened(CUSTOM, null, null), workspaceRpc.openedFiles.single()) + } + + fun `test read only skills open without staging edits or editor file open`() { + shown = 0 + val panel = panel(edit = { _, savable -> + assertFalse(savable) + FakeSkillDialog("# Ignored") { shown += 1 } + }) + flushUntil { rows(panel).size == 3 } + + click(skillsList(panel), panel, REMOTE, "edit") + + assertEquals(1, shown) + assertFalse(edt { panel.modified() }) + assertTrue(agentRpc.skillSaves.isEmpty()) + assertTrue(workspaceRpc.openedFiles.isEmpty()) + } + + fun `test skill edit dialog shows content with fallback`() { + edt { + val content = SkillEditDialog(SkillDto("plan", "desc", CUSTOM, "# Plan\nUse steps"), true) + val fallback = SkillEditDialog(SkillDto("plan", "desc", CUSTOM), true) + val readonly = SkillEditDialog(SkillDto("kilo-config", "desc", "builtin", "

Kilo Config

"), false) + try { + assertEquals("# Plan\nUse steps", content.content()) + assertEquals("desc", fallback.content()) + assertEquals("

Kilo Config

", readonly.content()) + assertEquals("OK", content.okText()) + } finally { + content.close(DialogWrapper.CANCEL_EXIT_CODE) + fallback.close(DialogWrapper.CANCEL_EXIT_CODE) + readonly.close(DialogWrapper.CANCEL_EXIT_CODE) + } + true + } + } + + fun `test skill editor file type follows content syntax before location`() { + assertEquals( + FileTypeManager.getInstance().getFileTypeByFileName("index.html"), + skillFileType("builtin", "

Kilo CLI Configuration Reference

All config lives in kilo.json.

"), + ) + assertEquals( + skillFileType("SKILL.md"), + skillFileType("builtin", "# Kilo CLI Configuration Reference\n\nAll config lives in `kilo.json`."), + ) + assertEquals(PlainTextFileType.INSTANCE, skillFileType("builtin", "Plain fallback text")) + } + + + fun `test delete action stages skill removal until apply`() { + val panel = panel() + flushUntil { rows(panel).size == 3 } + TestDialogManager.setTestDialog(TestDialog.YES) + + click(skillsList(panel), panel, CUSTOM, "delete") + + assertTrue(edt { rows(panel).none { it.key == CUSTOM } }) + assertTrue(agentRpc.skillRemovals.isEmpty()) + edt { panel.applyDraft(); true } + flushUntil { agentRpc.skillRemovals.size == 1 } + assertEquals(listOf(DIR to CUSTOM), agentRpc.skillRemovals) + } + + fun `test delete action requires confirmation`() { + val panel = panel() + flushUntil { rows(panel).size == 3 } + TestDialogManager.setTestDialog { Messages.NO } + + click(skillsList(panel), panel, CUSTOM, "delete") + + edt { UIUtil.dispatchAllInvocationEvents(); true } + assertTrue(agentRpc.skillRemovals.isEmpty()) + assertTrue(edt { rows(panel).any { it.key == CUSTOM } }) + } + + fun `test add path and url write skills config patch on apply`() { + var path = "/extra/skills" + var url = "https://skills.test/index.json" + val panel = panel(source = { _, isPath, _ -> FakeSourceDialog(if (isPath) path else url) }) + flushUntil { rows(panel).size == 3 } + + edt { panel.sources.addPath(); true } + edt { panel.sources.addUrl(); true } + flushUntil { sourceRows(panel).any { it.key == "url:$url" } } + assertTrue(appRpc.configPatches.isEmpty()) + + edt { panel.applyDraft(); true } + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } + + val paths = appRpc.configPatches.single().skills!!.paths + val urls = appRpc.configPatches.single().skills!!.urls + assertEquals(listOf("/global/skills", path), paths) + assertEquals(listOf("https://skills.test/base.json", url), urls) + assertEquals( + listOf("path:/global/skills", "path:$path", "url:https://skills.test/base.json", "url:$url"), + edt { sourceRows(panel).map { it.key } }, + ) + assertEquals(listOf(DIR), agentRpc.skillReloads) + } + + fun `test stale config update result keeps added skill sources visible`() { + val path = "/extra/skills" + val url = "https://skills.test/index.json" + val extra = "$path/extra/SKILL.md" + val panel = panel(source = { _, isPath, _ -> FakeSourceDialog(if (isPath) path else url) }) + appRpc.configUpdateReturnStale = true + appRpc.afterConfig = { agentRpc.skills = agentRpc.skills + SkillDto("extra", "Extra skill", extra) } + flushUntil { rows(panel).size == 3 } + + edt { + panel.sources.addPath() + panel.sources.addUrl() + panel.applyDraft() + true + } + + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } + assertTrue(edt { rows(panel).any { it.key == extra } }) + assertEquals( + listOf("path:/global/skills", "path:$path", "url:https://skills.test/base.json", "url:$url"), + edt { sourceRows(panel).map { it.key } }, + ) + } + + fun `test blocked reload completes apply with warning`() { + val path = "/extra/skills" + val panel = panel(source = { _, isPath, _ -> FakeSourceDialog(if (isPath) path else null) }) + agentRpc.reloadSkillResult = false + flushUntil { rows(panel).size == 3 } + + edt { + panel.sources.addPath() + panel.applyDraft() + true + } + + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } + assertEquals(listOf(DIR), agentRpc.skillReloads) + assertEquals("Skills settings saved, but active sessions are present. Reload the core after those sessions finish to apply the new skills.", edt { progressText(panel) }) + } + + fun `test post apply skills refresh failure keeps saved rows`() { + val panel = panel(edit = { _, _ -> FakeSkillDialog("# Saved") }) + flushUntil { rows(panel).size == 3 } + + doubleClick(skillsList(panel), panel, CUSTOM) + agentRpc.skillsError = RuntimeException("timeout") + edt { panel.applyDraft(); true } + + flushUntil { agentRpc.skillSaves.size == 1 && !edt { panel.modified() } } + assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } }) + assertEquals("# Saved", agentRpc.skills.single { it.location == CUSTOM }.content) + } + + fun `test source reset discards staged changes`() { + val path = "/extra/skills" + val panel = panel(source = { _, isPath, _ -> FakeSourceDialog(if (isPath) path else null) }) + flushUntil { rows(panel).size == 3 } + + edt { panel.sources.addPath(); true } + + assertTrue(edt { sourceRows(panel).any { it.key == "path:$path" } }) + assertTrue(edt { panel.modified() }) + edt { panel.resetDraft(); true } + + assertTrue(appRpc.configPatches.isEmpty()) + assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } }) + assertFalse(edt { sourceRows(panel).any { it.key == "path:$path" } }) + assertTrue(agentRpc.skillReloads.isEmpty()) + } + + fun `test delete source writes skills config patch`() { + val panel = panel() + flushUntil { rows(panel).size == 3 && sourceRows(panel).size == 2 } + + edt { + sourceList(panel).selectedIndices = intArrayOf(0) + panel.sources.removeSelected() + true + } + + assertTrue(appRpc.configPatches.isEmpty()) + edt { panel.applyDraft(); true } + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } + val patch = appRpc.configPatches.single().skills!! + assertEquals(emptyList(), patch.paths) + assertEquals(listOf("https://skills.test/base.json"), patch.urls) + assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } }) + assertEquals(listOf(DIR), agentRpc.skillReloads) + } + + fun `test stale config update result keeps removed skill sources hidden`() { + val panel = panel() + appRpc.configUpdateReturnStale = true + appRpc.afterConfig = { agentRpc.skills = agentRpc.skills.filterNot { it.location == CUSTOM } } + flushUntil { rows(panel).size == 3 && sourceRows(panel).size == 2 } + + edt { + sourceList(panel).selectedIndices = intArrayOf(0) + panel.sources.removeSelected() + panel.applyDraft() + true + } + + flushUntil { appRpc.configPatches.size == 1 && !edt { panel.modified() } } + assertEquals(listOf("builtin", REMOTE), edt { rows(panel).map { it.key } }) + assertEquals(listOf("url:https://skills.test/base.json"), edt { sourceRows(panel).map { it.key } }) + } + + fun `test search filters skills by name`() { + val panel = panel() + flushUntil { rows(panel).size == 3 } + + edt { + components(panel).filterIsInstance().single().text = "think" + UIUtil.dispatchAllInvocationEvents() + true + } + + flushUntil { rows(panel).map { it.key } == listOf("builtin") } + } + + fun `test skills reload failure keeps existing rows`() { + val panel = panel() + flushUntil { rows(panel).size == 3 } + agentRpc.skillsError = RuntimeException("timeout") + + edt { panel.reload(); true } + flushUntil { edt { skillsList(panel).isEnabled } } + + assertEquals(listOf(CUSTOM, "builtin", REMOTE), edt { rows(panel).map { it.key } }) + } + + fun `test skill editor file type follows location extension`() { + assertNotSame(UnknownFileType.INSTANCE, skillFileType("/tmp/skills/plan/SKILL.md")) + assertEquals( + FileTypeManager.getInstance().getFileTypeByFileName("index.html"), + skillFileType("/tmp/skills/index.html"), + ) + assertEquals(PlainTextFileType.INSTANCE, skillFileType("/tmp/skills/index.unknown")) + } + + fun `test skill path chooser accepts directories only`() { + val descriptor = skillPathDescriptor() + + assertTrue(descriptor.isChooseFolders) + assertFalse(descriptor.isChooseFiles) + } + + private fun panel( + choose: (JComponent) -> String? = { null }, + source: (Boolean, Boolean, String) -> SettingsPathDialogHandle = { _, _, _ -> FakeSourceDialog(null) }, + edit: (SkillDto, Boolean) -> SkillEditDialogHandle = { _, _ -> FakeSkillDialog("# Plan\nUse steps") }, + ): SkillsSettingsUi { + install() + val panel = edt { SkillsSettingsUi(scope!!, DIR, choose, source, edit) } + ui = panel + edt { panel.reload(); true } + return panel + } + + private fun install() { + val cs = CoroutineScope(SupervisorJob()) + scope = cs + appRpc = FakeAppRpcApi() + workspaceRpc = FakeWorkspaceRpcApi() + agentRpc = FakeAgentBehaviorRpcApi().apply { + skills = listOf( + SkillDto("plan", "Plan work", CUSTOM, "# Plan\nUse steps", editable = true), + SkillDto("thinking", "Built in", "builtin", "Built in content"), + SkillDto("remote", "Remote skill", REMOTE, "# Remote skill"), + ) + } + app = KiloAppService(cs, appRpc) + val ready = KiloAppStateDto( + KiloAppStatusDto.READY, + config = ConfigDto(skills = SkillsConfigDto( + paths = listOf("/global/skills"), + urls = listOf("https://skills.test/base.json"), + )), + ) + app._state.value = ready + appRpc.state.value = ready + ApplicationManager.getApplication().replaceService(KiloAppService::class.java, app, testRootDisposable) + ApplicationManager.getApplication().replaceService(KiloAgentBehaviorService::class.java, KiloAgentBehaviorService(cs, agentRpc), testRootDisposable) + ApplicationManager.getApplication().replaceService(KiloWorkspaceService::class.java, KiloWorkspaceService(cs, workspaceRpc), testRootDisposable) + } + + private fun click(list: JBList, panel: SkillsSettingsUi, key: String, id: String) { + edt { + list.size = Dimension(520, 320) + list.doLayout() + val rows = if (list === skillsList(panel)) rows(panel) else sourceRows(panel) + val idx = rows.indexOfFirst { it.key == key } + list.selectedIndex = idx + val area = settingsListCellBounds(list, idx, selected = true).getValue(id) + click(list, center(area)) + true + } + } + + private fun doubleClick(list: JBList, panel: SkillsSettingsUi, key: String) { + edt { + list.size = Dimension(520, 320) + list.doLayout() + val idx = rows(panel).indexOfFirst { it.key == key } + list.selectedIndex = idx + val area = list.getCellBounds(idx, idx) + fire(list, mouse(list, MouseEvent.MOUSE_CLICKED, center(area), count = 2)) + true + } + } + + private fun rows(panel: SkillsSettingsUi): List = items(skillsList(panel)) + + private fun sourceRows(panel: SkillsSettingsUi): List = items(sourceList(panel)) + + private fun items(list: JBList): List { + val model = list.model + return (0 until model.size).map { model.getElementAt(it) } + } + + private fun skillsList(panel: SkillsSettingsUi) = components(panel).filterIsInstance>().first() + + private fun sourceList(panel: SkillsSettingsUi) = components(panel).filterIsInstance>().last() + + private fun scrollFor(panel: SkillsSettingsUi, list: JBList) = components(panel) + .filterIsInstance() + .single { pane -> pane.viewport.view === list.parent } + + private fun progressText(panel: SkillsSettingsUi) = components(panel.progress).filterIsInstance().single().text + + private fun SkillEditDialog.okText(): String { + val method = DialogWrapper::class.java.getDeclaredMethod("getOKAction") + method.isAccessible = true + return (method.invoke(this) as javax.swing.Action).getValue(javax.swing.Action.NAME) as String + } + + private fun components(root: java.awt.Component): List { + val out = mutableListOf() + fun visit(item: java.awt.Component) { + out += item + if (item is Container) item.components.forEach { visit(it) } + } + visit(root) + return out + } + + private fun layout(root: java.awt.Component) { + root.doLayout() + if (root is Container) root.components.filterIsInstance().forEach { layout(it) } + UIUtil.dispatchAllInvocationEvents() + } + + private fun center(rect: java.awt.Rectangle) = Point(rect.x + rect.width / 2, rect.y + rect.height / 2) + + private fun click(list: JBList, point: Point) { + fire(list, mouse(list, MouseEvent.MOUSE_PRESSED, point)) + fire(list, mouse(list, MouseEvent.MOUSE_RELEASED, point)) + } + + private fun mouse(list: JBList, id: Int, point: Point, count: Int = 1) = MouseEvent( + list, + id, + System.currentTimeMillis(), + if (id == MouseEvent.MOUSE_PRESSED) InputEvent.BUTTON1_DOWN_MASK else 0, + point.x, + point.y, + count, + false, + MouseEvent.BUTTON1, + ) + + private fun edt(block: () -> T): T { + var result: T? = null + ApplicationManager.getApplication().invokeAndWait { result = block() } + @Suppress("UNCHECKED_CAST") + return result as T + } + + private fun flushUntil(done: () -> Boolean) = runBlocking { + repeat(300) { + delay(10) + edt { UIUtil.dispatchAllInvocationEvents(); true } + if (done()) return@runBlocking + } + edt { UIUtil.dispatchAllInvocationEvents(); true } + assertTrue(done()) + } + + private companion object { + const val DIR = "/test" + const val CUSTOM = "/home/test/.config/kilo/skill/plan/SKILL.md" + const val REMOTE = "/home/test/.cache/kilo/skills/remote/SKILL.md" + } +} + +private class FakeSkillDialog(private val text: String, private val show: () -> Unit = {}) : SkillEditDialogHandle { + override fun showAndGet(): Boolean { + show() + return true + } + override fun content() = text +} + +private class FakeSourceDialog(private val text: String?) : SettingsPathDialogHandle { + override fun showAndGet() = text != null + override fun value() = text ?: "" +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsStateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsStateTest.kt new file mode 100644 index 00000000000..63ba407135f --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsStateTest.kt @@ -0,0 +1,238 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.rpc.dto.ConfigDto +import ai.kilocode.rpc.dto.PermissionRuleDto +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class AutoApproveSettingsStateTest { + @Test + fun `draft reads permission config`() { + val draft = permissionDraft(ConfigDto(permission = mapOf("bash" to PermissionRuleDto.Level("allow")))) + + assertEquals("allow", wildcardLevel(draft.rules["bash"])) + } + + @Test + fun `default level rules match CLI defaults`() { + assertEquals("ask", defaultLevel("external_directory")) + assertEquals("ask", defaultLevel("bash")) + assertEquals("ask", defaultLevel("doom_loop")) + assertEquals("allow", defaultLevel("read")) + assertEquals("allow", defaultLevel("edit")) + } + + @Test + fun `effective level falls back to default when unset`() { + val draft = PermissionDraft() + + assertEquals("ask", effectiveLevel(draft, "bash")) + assertEquals("allow", effectiveLevel(draft, "read")) + } + + @Test + fun `inherited wildcard is true when tool absent or patterns wildcard missing`() { + assertTrue(inheritedWildcard(null)) + assertFalse(inheritedWildcard(PermissionRuleDto.Level("allow"))) + assertTrue(inheritedWildcard(PermissionRuleDto.Patterns(mapOf("*.env" to "deny")))) + assertFalse(inheritedWildcard(PermissionRuleDto.Patterns(mapOf("*" to "ask", "*.env" to "deny")))) + } + + @Test + fun `mostRestrictive orders allow ask deny`() { + assertEquals("deny", mostRestrictive(listOf("allow", "deny", "ask"))) + assertEquals("ask", mostRestrictive(listOf("allow", "ask"))) + assertEquals("allow", mostRestrictive(listOf("allow", "allow"))) + assertEquals("allow", mostRestrictive(emptyList())) + } + + @Test + fun `setWildcard on unset tool emits scalar patch`() { + val from = PermissionDraft() + val to = setWildcard(from, "bash", "deny") + + assertEquals(mapOf("bash" to PermissionRuleDto.Level("deny")), permissionPatch(from, to)) + } + + @Test + fun `setWildcard preserves existing exceptions as patterns`() { + val from = PermissionDraft(rules = mapOf("read" to PermissionRuleDto.Patterns(mapOf("*.env" to "deny")))) + val to = setWildcard(from, "read", "ask") + + assertEquals("ask", wildcardLevel(to.rules["read"])) + assertEquals(listOf("*.env" to "deny"), exceptions(to.rules["read"])) + assertEquals( + mapOf("read" to PermissionRuleDto.Patterns(mapOf("*" to "ask", "*.env" to "deny"))), + permissionPatch(from, to), + ) + } + + @Test + fun `inheritWildcard removes tool entirely when no exceptions`() { + val from = PermissionDraft(rules = mapOf("bash" to PermissionRuleDto.Level("deny"))) + val to = inheritWildcard(from, "bash") + + assertTrue(to.rules.isEmpty()) + assertEquals(mapOf("bash" to PermissionRuleDto.Level(null)), permissionPatch(from, to)) + } + + @Test + fun `inheritWildcard clears only wildcard when exceptions remain`() { + val from = PermissionDraft( + rules = mapOf("read" to PermissionRuleDto.Patterns(mapOf("*" to "ask", "*.env" to "deny"))), + ) + val to = inheritWildcard(from, "read") + + assertTrue(inheritedWildcard(to.rules["read"])) + assertEquals(listOf("*.env" to "deny"), exceptions(to.rules["read"])) + assertEquals( + mapOf("read" to PermissionRuleDto.Patterns(mapOf("*" to null, "*.env" to "deny"))), + permissionPatch(from, to), + ) + } + + @Test + fun `addException on scalar wildcard preserves the wildcard as star pattern`() { + val from = PermissionDraft(rules = mapOf("bash" to PermissionRuleDto.Level("ask"))) + val to = addException(from, "bash", "git *") + + assertEquals(listOf("git *" to "allow"), exceptions(to.rules["bash"])) + assertEquals("ask", wildcardLevel(to.rules["bash"])) + assertEquals( + mapOf("bash" to PermissionRuleDto.Patterns(mapOf("*" to "ask", "git *" to "allow"))), + permissionPatch(from, to), + ) + } + + @Test + fun `addException ignores an existing pattern`() { + val draft = PermissionDraft( + rules = mapOf("bash" to PermissionRuleDto.Patterns(mapOf("*" to "ask", "git *" to "deny"))), + ) + + assertEquals(draft, addException(draft, "bash", "git *")) + } + + @Test + fun `editException ignores an existing target pattern`() { + val draft = PermissionDraft( + rules = mapOf( + "bash" to PermissionRuleDto.Patterns(mapOf("git *" to "deny", "git status" to "ask")), + ), + ) + + assertEquals(draft, editException(draft, "bash", "git *", "git status")) + } + + @Test + fun `setException changes an existing exception level`() { + val from = PermissionDraft( + rules = mapOf("read" to PermissionRuleDto.Patterns(mapOf("*" to "allow", "*.env" to "deny"))), + ) + val to = setException(from, "read", "*.env", "ask") + + assertEquals(listOf("*.env" to "ask"), exceptions(to.rules["read"])) + assertEquals( + mapOf("read" to PermissionRuleDto.Patterns(mapOf("*" to "allow", "*.env" to "ask"))), + permissionPatch(from, to), + ) + } + + @Test + fun `removeException deletes a single pattern and keeps others`() { + val from = PermissionDraft( + rules = mapOf( + "read" to PermissionRuleDto.Patterns(mapOf("*" to "allow", "*.env" to "deny", "*.key" to "deny")), + ), + ) + val to = removeException(from, "read", "*.env") + + assertEquals(listOf("*.key" to "deny"), exceptions(to.rules["read"])) + assertEquals( + mapOf("read" to PermissionRuleDto.Patterns(mapOf("*" to "allow", "*.key" to "deny", "*.env" to null))), + permissionPatch(from, to), + ) + } + + @Test + fun `removeException removing the last exception removes the tool key`() { + val from = PermissionDraft(rules = mapOf("read" to PermissionRuleDto.Patterns(mapOf("*.env" to "deny")))) + val to = removeException(from, "read", "*.env") + + assertTrue(to.rules.isEmpty()) + assertEquals(mapOf("read" to PermissionRuleDto.Level(null)), permissionPatch(from, to)) + } + + @Test + fun `grouped set applies scalar level to both ids`() { + val from = PermissionDraft() + val to = setGrouped(from, listOf("todoread", "todowrite"), "ask") + + assertEquals( + mapOf("todoread" to PermissionRuleDto.Level("ask"), "todowrite" to PermissionRuleDto.Level("ask")), + permissionPatch(from, to), + ) + } + + @Test + fun `grouped inherit deletes both ids`() { + val from = PermissionDraft( + rules = mapOf( + "todoread" to PermissionRuleDto.Level("deny"), + "todowrite" to PermissionRuleDto.Level("deny"), + ), + ) + val to = inheritGrouped(from, listOf("todoread", "todowrite")) + + assertEquals( + mapOf("todoread" to PermissionRuleDto.Level(null), "todowrite" to PermissionRuleDto.Level(null)), + permissionPatch(from, to), + ) + } + + @Test + fun `scalar to patterns transition emits full desired patterns`() { + val from = PermissionDraft(rules = mapOf("edit" to PermissionRuleDto.Level("ask"))) + val to = addException(from, "edit", "*.env") + + assertEquals( + mapOf("edit" to PermissionRuleDto.Patterns(mapOf("*" to "ask", "*.env" to "allow"))), + permissionPatch(from, to), + ) + } + + @Test + fun `no-op diff returns null`() { + val draft = PermissionDraft( + rules = mapOf( + "bash" to PermissionRuleDto.Patterns(mapOf("*" to "ask", "git *" to "allow")), + "read" to PermissionRuleDto.Level("allow"), + ), + ) + + assertNull(permissionPatch(draft, draft)) + assertNull(patch(from = draft, to = draft)) + } + + @Test + fun `patch wraps the permission patch in a ConfigPatchDto`() { + val from = PermissionDraft() + val to = setWildcard(from, "bash", "deny") + + assertEquals(mapOf("bash" to PermissionRuleDto.Level("deny")), patch(from, to)?.permission) + } + + @Test + fun `savedMatches drops null-valued entries before comparing`() { + val base = PermissionDraft(rules = mapOf("read" to PermissionRuleDto.Patterns(mapOf("*.env" to "deny")))) + val draftWithNull = PermissionDraft( + rules = mapOf("read" to PermissionRuleDto.Patterns(mapOf("*" to null, "*.env" to "deny"))), + ) + + assertTrue(savedMatches(base, draftWithNull)) + assertFalse(savedMatches(base, PermissionDraft())) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUiTest.kt new file mode 100644 index 00000000000..635e7711e74 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/AutoApproveSettingsUiTest.kt @@ -0,0 +1,477 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.settings.base.SettingsListItem +import ai.kilocode.client.settings.base.settingsListCellBounds +import ai.kilocode.client.testing.FakeAppRpcApi +import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.client.testing.fire +import ai.kilocode.rpc.dto.ConfigDto +import ai.kilocode.rpc.dto.KiloAppStateDto +import ai.kilocode.rpc.dto.KiloAppStatusDto +import ai.kilocode.rpc.dto.PermissionRuleDto +import com.intellij.openapi.application.ApplicationManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.ui.components.JBList +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import java.awt.Container +import java.awt.Point +import java.awt.event.InputEvent +import java.awt.event.MouseEvent +import javax.swing.AbstractButton +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.text.JTextComponent + +// Matches AutoApproveContent's fixed granular section order. +private val LEVEL_SELECT_ORDER = listOf("external_directory", "bash", "read", "edit") + +class AutoApproveSettingsUiTest : BasePlatformTestCase() { + private lateinit var appScope: CoroutineScope + private lateinit var uiScope: CoroutineScope + private lateinit var rpc: FakeAppRpcApi + private lateinit var workspaceRpc: FakeWorkspaceRpcApi + private lateinit var app: KiloAppService + private lateinit var workspaces: KiloWorkspaceService + private var ui: AutoApproveSettingsUi? = null + private var pick: (List) -> LevelChoice = { it.first() } + private val picker = LevelPicker { choices, choose -> + choose(pick(choices)) + null + } + + override fun setUp() { + super.setUp() + appScope = CoroutineScope(SupervisorJob()) + uiScope = CoroutineScope(SupervisorJob()) + rpc = FakeAppRpcApi() + workspaceRpc = FakeWorkspaceRpcApi() + app = KiloAppService(appScope, rpc) + workspaces = KiloWorkspaceService(appScope, workspaceRpc) + val state = KiloAppStateDto(KiloAppStatusDto.READY, config = ConfigDto()) + rpc.state.value = state + app._state.value = state + edt { ui = AutoApproveSettingsUi(uiScope, app, workspaces, picker) } + flushUntil { text(requireUi()).contains("External Directory") } + } + + override fun tearDown() { + try { + val panel = ui + if (panel != null) edt { panel.dispose() } + ui = null + uiScope.cancel() + appScope.cancel() + } finally { + super.tearDown() + } + } + + fun `test page is not editable before app is ready`() { + rpc.state.value = KiloAppStateDto(KiloAppStatusDto.LOADING) + app._state.value = KiloAppStateDto(KiloAppStatusDto.LOADING) + edt { ui = AutoApproveSettingsUi(uiScope, app, workspaces, picker) } + flushUntil { text(requireUi()).contains("External Directory") } + + edt { + assertTrue(levelSelects(requireUi()).all { !it.isEnabled }) + assertTrue(inlineLists(requireUi()).map { jbList(it) }.all { !it.isEnabled }) + } + } + + fun `test simple tool rows use list renderer and level action`() { + val panel = requireUi() + + edt { + pick = { choices -> choices.first { it is LevelChoice.Level && it.level == "allow" } } + clickLevel(toolsList(panel), "glob") + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() } + assertEquals(mapOf("glob" to PermissionRuleDto.Level("allow")), rpc.configPatches.single().permission) + } + + fun `test setting a simple tool level sends the expected patch`() { + val panel = requireUi() + + edt { + selectLevel(levelSelectFor(panel, "read"), "deny") + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() } + assertEquals(mapOf("read" to PermissionRuleDto.Level("deny")), rpc.configPatches.single().permission) + } + + fun `test choosing Default reverts a tool to inherited`() { + val panel = requireUi() + rpc.state.value = rpc.state.value.copy(config = ConfigDto(permission = mapOf("bash" to PermissionRuleDto.Level("deny")))) + app._state.value = rpc.state.value + flushUntil { !edt { panel.modified() } } + + edt { + selectInherit(levelSelectFor(panel, "bash")) + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() } + assertEquals(mapOf("bash" to PermissionRuleDto.Level(null)), rpc.configPatches.single().permission) + } + + fun `test adding an exception to a granular tool sends full patterns patch`() { + val panel = requireUi() + + edt { + val list = inlineListFor(panel, "bash") + list.input = { "git *" } + click(button(list, 0)) + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() } + val rule = rpc.configPatches.single().permission?.get("bash") + assertEquals(PermissionRuleDto.Patterns(mapOf("git *" to "allow")), rule) + } + + fun `test scalar wildcard is preserved after applying a new exception`() { + val panel = requireUi() + rpc.state.value = rpc.state.value.copy(config = ConfigDto(permission = mapOf("bash" to PermissionRuleDto.Level("ask")))) + app._state.value = rpc.state.value + flushUntil { !edt { panel.modified() } } + + edt { + val list = inlineListFor(panel, "bash") + list.input = { "git *" } + click(button(list, 0)) + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() } + val rule = rpc.state.value.config?.permission?.get("bash") + assertEquals(PermissionRuleDto.Patterns(mapOf("*" to "ask", "git *" to "allow")), rule) + } + + fun `test removing an exception sends a null delete for that pattern only`() { + val panel = requireUi() + rpc.state.value = rpc.state.value.copy( + config = ConfigDto(permission = mapOf( + "read" to PermissionRuleDto.Patterns(mapOf("*" to "allow", "*.env" to "deny", "*.key" to "deny")), + )), + ) + app._state.value = rpc.state.value + flushUntil { !edt { panel.modified() } } + + edt { + removeException(panel, "read", "*.env") + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() } + val rule = rpc.configPatches.single().permission?.get("read") + assertEquals(PermissionRuleDto.Patterns(mapOf("*" to "allow", "*.key" to "deny", "*.env" to null)), rule) + } + + fun `test grouped todo row uses the most restrictive level and applies to both ids`() { + val panel = requireUi() + + edt { + pick = { choices -> choices.first { it is LevelChoice.Level && it.level == "deny" } } + clickLevel(toolsList(panel), "todoread+todowrite") + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() } + assertEquals( + mapOf("todoread" to PermissionRuleDto.Level("deny"), "todowrite" to PermissionRuleDto.Level("deny")), + rpc.configPatches.single().permission, + ) + } + + fun `test isModified reflects unsaved changes and resetDraft reverts them`() { + val panel = requireUi() + + edt { + assertFalse(panel.modified()) + selectLevel(levelSelectFor(panel, "read"), "deny") + assertTrue(panel.modified()) + panel.resetDraft() + assertFalse(panel.modified()) + } + } + + fun `test reselecting the already explicit level leaves the page unmodified`() { + val panel = requireUi() + rpc.state.value = rpc.state.value.copy(config = ConfigDto(permission = mapOf("read" to PermissionRuleDto.Level("allow")))) + app._state.value = rpc.state.value + flushUntil { !edt { panel.modified() } } + + edt { + selectLevel(levelSelectFor(panel, "read"), "allow") + assertFalse(panel.modified()) + } + } + + fun `test apply keeps selected auto approve section row and height`() { + val panel = requireUi() + rpc.state.value = rpc.state.value.copy( + config = ConfigDto(permission = mapOf( + "bash" to PermissionRuleDto.Patterns(mapOf("*" to "ask", "git log *" to "allow", "got" to "allow")), + )), + ) + app._state.value = rpc.state.value + flushUntil { !edt { panel.modified() } } + val list = edt { inlineListFor(panel, "bash") } + val height = edt { + val jList = jbList(list) + val idx = indexOf(jList, "git log *") + jList.selectedIndex = idx + jList.fixedCellHeight + } + + edt { + pick = { choices -> choices.first { it is LevelChoice.Level && it.level == "deny" } } + clickLevel(list, "git log *") + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() } + edt { + val jList = jbList(list) + assertEquals("git log *", (jList.selectedValue as SettingsListItem).key) + assertEquals(height, jList.fixedCellHeight) + } + } + + fun `test granular row stays selected when level changes before apply`() { + val panel = requireUi() + rpc.state.value = rpc.state.value.copy( + config = ConfigDto(permission = mapOf( + "read" to PermissionRuleDto.Patterns(mapOf("*" to "ask", "*.env" to "allow")), + )), + ) + app._state.value = rpc.state.value + flushUntil { !edt { panel.modified() } } + val list = edt { inlineListFor(panel, "read") } + val height = edt { jbList(list).fixedCellHeight } + + edt { + pick = { choices -> choices.first { it is LevelChoice.Level && it.level == "deny" } } + clickLevel(list, "*.env") + val jList = jbList(list) + assertEquals("*.env", (jList.selectedValue as SettingsListItem).key) + assertEquals(height, jList.fixedCellHeight) + } + } + + fun `test granular exception edit renames pattern and keeps selection`() { + val panel = requireUi() + rpc.state.value = rpc.state.value.copy( + config = ConfigDto(permission = mapOf( + "bash" to PermissionRuleDto.Patterns(mapOf("*" to "ask", "git *" to "allow")), + )), + ) + app._state.value = rpc.state.value + flushUntil { !edt { panel.modified() } } + val list = edt { inlineListFor(panel, "bash") } + + edt { + list.editInput = { "git status" } + val jList = jbList(list) + jList.setSize(600, jList.preferredSize.height.coerceAtLeast(80)) + jList.doLayout() + doubleClickRow(jList, indexOf(jList, "git *")) + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() } + val rule = rpc.configPatches.last().permission?.get("bash") + assertEquals(PermissionRuleDto.Patterns(mapOf("*" to "ask", "git status" to "allow", "git *" to null)), rule) + edt { + assertEquals("git status", (jbList(list).selectedValue as SettingsListItem).key) + } + } + + private fun requireUi(): AutoApproveSettingsUi = requireNotNull(ui) + + private fun levelSelects(panel: AutoApproveSettingsUi): List = + components(panel).filterIsInstance() + + private fun levelSelectFor(panel: AutoApproveSettingsUi, tool: String): LevelSelect { + val index = LEVEL_SELECT_ORDER.indexOf(tool) + require(index >= 0) { "unknown tool $tool" } + return levelSelects(panel)[index] + } + + private fun inlineListFor(panel: AutoApproveSettingsUi, tool: String): SettingsInlineList { + val index = GRANULAR_ORDER.indexOf(tool) + require(index >= 0) { "unknown granular tool $tool" } + return inlineLists(panel)[index] + } + + private fun toolsList(panel: AutoApproveSettingsUi): SettingsInlineList = inlineLists(panel).last() + + private fun inlineLists(panel: AutoApproveSettingsUi): List = + components(panel).filterIsInstance() + + private fun removeException(panel: AutoApproveSettingsUi, tool: String, pattern: String) { + val list = inlineListFor(panel, tool) + val jList = components(list).filterIsInstance>().single() + val idx = (0 until jList.model.size).first { jList.model.getElementAt(it).toString().contains(pattern) } + jList.selectedIndex = idx + UIUtil.dispatchAllInvocationEvents() + click(button(list, 1)) + } + + private fun clickLevel(list: SettingsInlineList, key: String) { + val jList = jbList(list) + val model = jList.model + val idx = (0 until model.size).first { (model.getElementAt(it) as SettingsListItem).key == key } + jList.selectedIndex = idx + jList.setSize(600, jList.preferredSize.height.coerceAtLeast(80)) + jList.doLayout() + val bounds = settingsListCellBounds(jList, idx, true)["level"] ?: error("missing level cell for $key") + click(jList, Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2)) + } + + private fun jbList(list: SettingsInlineList): JBList<*> = components(list).filterIsInstance>().single() + + private fun indexOf(list: JBList<*>, key: String): Int = (0 until list.model.size) + .first { (list.model.getElementAt(it) as SettingsListItem).key == key } + + private fun button(list: SettingsInlineList, index: Int): JComponent = components(list) + .filterIsInstance() + .filter { it.javaClass.name.endsWith("ActionButton") } + .let { it[index] } + + private fun click(target: JComponent) { + target.setSize(target.preferredSize) + val point = Point(target.width.coerceAtLeast(2) / 2, target.height.coerceAtLeast(2) / 2) + click(target, point) + } + + private fun click(target: JComponent, point: Point) { + val press = MouseEvent( + target, + MouseEvent.MOUSE_PRESSED, + System.currentTimeMillis(), + InputEvent.BUTTON1_DOWN_MASK, + point.x, + point.y, + 1, + false, + MouseEvent.BUTTON1, + ) + val release = MouseEvent( + target, + MouseEvent.MOUSE_RELEASED, + System.currentTimeMillis(), + 0, + point.x, + point.y, + 1, + false, + MouseEvent.BUTTON1, + ) + val clicked = MouseEvent( + target, + MouseEvent.MOUSE_CLICKED, + System.currentTimeMillis(), + 0, + point.x, + point.y, + 1, + false, + MouseEvent.BUTTON1, + ) + dispatch(target, press) + dispatch(target, release) + dispatch(target, clicked) + UIUtil.dispatchAllInvocationEvents() + } + + private fun dispatch(target: JComponent, event: MouseEvent) { + if (target is JBList<*>) { + fire(target, event) + return + } + target.dispatchEvent(event) + } + + private fun doubleClickRow(list: JBList<*>, idx: Int) { + val bounds = list.getCellBounds(idx, idx) + val point = Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2) + val event = MouseEvent( + list, + MouseEvent.MOUSE_CLICKED, + System.currentTimeMillis(), + 0, + point.x, + point.y, + 2, + false, + MouseEvent.BUTTON1, + ) + list.dispatchEvent(event) + UIUtil.dispatchAllInvocationEvents() + } + + private fun selectLevel(combo: LevelSelect, level: String) { + val item = (0 until combo.itemCount).map { combo.getItemAt(it) } + .first { it is LevelSelect.Item.Level && it.value == level } + combo.selectedItem = item + } + + private fun selectInherit(combo: LevelSelect) { + val item = (0 until combo.itemCount).map { combo.getItemAt(it) }.first { it is LevelSelect.Item.Default } + combo.selectedItem = item + } + + private fun edt(block: () -> T): T { + var result: T? = null + ApplicationManager.getApplication().invokeAndWait { result = block() } + @Suppress("UNCHECKED_CAST") + return result as T + } + + private fun flushUntil(done: () -> Boolean) = runBlocking { + repeat(200) { + delay(10) + edt { UIUtil.dispatchAllInvocationEvents() } + if (done()) return@runBlocking + } + edt { UIUtil.dispatchAllInvocationEvents() } + assertTrue(done()) + } + + private fun text(root: Container): String { + val out = mutableListOf() + for (comp in components(root)) { + if (!comp.isVisible) continue + when (comp) { + is AbstractButton -> comp.text?.let { out.add(it) } + is JLabel -> comp.text?.let { out.add(it) } + is JTextComponent -> comp.text?.let { out.add(it) } + } + } + return out.joinToString("\n") + } + + private fun components(root: Container): List = buildList { + fun visit(comp: java.awt.Component) { + add(comp) + if (comp is Container) comp.components.forEach { visit(it) } + } + visit(root) + } + + private companion object { + val GRANULAR_ORDER = listOf("external_directory", "bash", "read", "edit") + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt new file mode 100644 index 00000000000..f6e91707acf --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/autoapprove/SettingsInlineListTest.kt @@ -0,0 +1,345 @@ +package ai.kilocode.client.settings.autoapprove + +import ai.kilocode.client.ui.UiStyle +import ai.kilocode.client.settings.base.SettingsListItem +import ai.kilocode.client.settings.base.settingsListCellBounds +import ai.kilocode.client.testing.fire +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.ui.components.JBList +import com.intellij.util.ui.UIUtil +import java.awt.Container +import java.awt.Point +import java.awt.event.InputEvent +import java.awt.event.MouseEvent +import javax.swing.JComponent +import javax.swing.ListSelectionModel + +class SettingsInlineListTest : BasePlatformTestCase() { + fun `test empty list keeps minimum height for empty text`() { + edt { + val list = list() + list.syncItems(emptyList(), true) + layout(list) + + assertTrue(jbList(list).minimumSize.height >= UiStyle.Gap.xl()) + } + } + + fun `test filtering to no rows keeps minimum empty list area`() { + edt { + val list = list() + list.syncItems(listOf("*.env" to "deny", "*.key" to "deny", "*.pem" to "deny"), true) + layout(list) + + list.filter("nomatch") + layout(list) + + assertEquals(0, jbList(list).model.size) + assertTrue(jbList(list).minimumSize.height >= UiStyle.Gap.xl()) + } + } + + fun `test toolbar delete removes selected rows in bulk`() { + edt { + val removed = mutableListOf() + val list = list(onRemove = { removed += it }, selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) + list.syncItems(listOf("*.env" to "deny", "*.key" to "deny"), true) + layout(list) + + val jList = jbList(list) + jList.setSelectionInterval(0, 1) + UIUtil.dispatchAllInvocationEvents() + click(button(list, 1)) + + assertEquals(listOf("*.env", "*.key"), removed) + } + } + + fun `test toolbar add invokes onAdd with the input override value`() { + edt { + val added = mutableListOf() + val list = list(onAdd = { added += it }) + list.input = { "git *" } + layout(list) + + click(button(list, 0)) + + assertEquals(listOf("git *"), added) + } + } + + fun `test toolbar add ignores duplicate input`() { + edt { + val added = mutableListOf() + val list = list(onAdd = { added += it }) + list.syncItems(listOf("git *" to "ask"), true) + list.input = { "git *" } + layout(list) + + click(button(list, 0)) + + assertTrue(added.isEmpty()) + } + } + + fun `test row level action changes through picker selection`() { + edt { + val changed = mutableListOf() + val picker = FakePicker { it.first { c -> c is LevelChoice.Level && c.level == "ask" } } + val list = list(onSet = { _, level -> changed += level }, picker = picker) + list.syncRows(listOf(PermissionListRow("glob", "Glob", "Search files", "allow")), true) + layout(list) + + clickLevel(list, "glob") + + assertEquals(listOf(LevelChoice.Level("allow"), LevelChoice.Level("ask"), LevelChoice.Level("deny")), picker.offered) + assertEquals(listOf("ask"), changed) + } + } + + fun `test picker offers default option for inheritable rows`() { + edt { + val inherited = mutableListOf() + val picker = FakePicker { it.first { c -> c is LevelChoice.Default } } + val list = list(onInherit = { inherited += it }, picker = picker) + list.syncRows( + listOf(PermissionListRow("glob", "Glob", "Search files", "allow", inherited = true, canInherit = true)), + true, + ) + layout(list) + + clickLevel(list, "glob") + + assertEquals(LevelChoice.Default("allow"), picker.offered.first()) + assertEquals(listOf("glob"), inherited) + } + } + + fun `test exception edit action is selected only and double click edits`() { + edt { + val edits = mutableListOf>() + val list = list(onEdit = { from, to -> edits += from to to }) + list.editInput = { "git status" } + list.syncItems(listOf("git *" to "allow"), true) + val jList = jbList(list) + jList.setSize(400, jList.preferredSize.height.coerceAtLeast(50)) + jList.doLayout() + + assertFalse(settingsListCellBounds(jList, 0, false).containsKey("edit")) + jList.selectedIndex = 0 + assertTrue(settingsListCellBounds(jList, 0, true).containsKey("edit")) + + doubleClickRow(jList, 0) + + assertEquals(listOf("git *" to "git status"), edits) + } + } + + fun `test exception edit ignores duplicate target`() { + edt { + val edits = mutableListOf>() + val list = list(onEdit = { from, to -> edits += from to to }) + list.editInput = { "git status" } + list.syncItems(listOf("git *" to "allow", "git status" to "ask"), true) + val jList = jbList(list) + jList.setSize(400, jList.preferredSize.height.coerceAtLeast(50)) + jList.doLayout() + + doubleClickRow(jList, 0) + + assertTrue(edits.isEmpty()) + } + } + + fun `test syncItems retains the same list view instance across updates`() { + edt { + val list = list() + list.syncItems(listOf("*.env" to "deny"), true) + val jList = jbList(list) + + list.syncItems(listOf("*.env" to "deny", "*.key" to "deny"), true) + + assertSame(jList, jbList(list)) + } + } + + fun `test row height stays stable across level changes and reload`() { + edt { + val list = list() + list.syncRows(listOf(PermissionListRow("git log *", "git log *", level = "allow")), true) + val jList = jbList(list) + jList.selectedIndex = 0 + val height = jList.fixedCellHeight + + list.syncRows(listOf(PermissionListRow("git log *", "git log *", level = "ask")), true) + assertEquals(height, jList.fixedCellHeight) + + list.syncRows(listOf(PermissionListRow("git log *", "git log *", level = "ask")), false) + list.syncRows(listOf(PermissionListRow("git log *", "git log *", level = "deny")), true) + assertEquals(height, jList.fixedCellHeight) + } + } + + fun `test setEnabled disables add and list`() { + edt { + val list = list() + list.syncItems(listOf("*.env" to "deny"), true) + + list.setEnabled(false) + + assertFalse(button(list, 0).isEnabled) + assertFalse(jbList(list).isEnabled) + } + } + + private fun list( + onAdd: (String) -> Unit = {}, + onSet: (String, String) -> Unit = { _, _ -> }, + onInherit: (String) -> Unit = {}, + onEdit: (String, String) -> Unit = { _, _ -> }, + onRemove: (List) -> Unit = {}, + picker: LevelPicker = PopupLevelPicker, + selection: Int = ListSelectionModel.SINGLE_SELECTION, + ): SettingsInlineList = SettingsInlineList( + empty = "Empty", + addLabel = "Add", + placeholder = "e.g. *.env", + onAdd = onAdd, + onSetLevel = onSet, + onInherit = onInherit, + onEdit = onEdit, + onRemove = onRemove, + picker = picker, + selectionMode = selection, + ) + + private class FakePicker(private val select: (List) -> LevelChoice) : LevelPicker { + var offered: List = emptyList() + private set + + override fun popup(choices: List, choose: (LevelChoice) -> Unit): JBPopup? { + offered = choices + choose(select(choices)) + return null + } + } + + private fun jbList(list: SettingsInlineList): JBList<*> = components(list).filterIsInstance>().single() + + private fun layout(root: Container) { + root.setSize(400, root.preferredSize.height.coerceAtLeast(50)) + root.doLayout() + root.components.filterIsInstance().forEach { layout(it) } + UIUtil.dispatchAllInvocationEvents() + } + + private fun button(list: SettingsInlineList, index: Int): JComponent = components(list) + .filterIsInstance() + .filter { it.javaClass.name.endsWith("ActionButton") } + .let { it[index] } + + private fun click(target: JComponent) { + target.setSize(target.preferredSize) + val point = Point(target.width.coerceAtLeast(2) / 2, target.height.coerceAtLeast(2) / 2) + click(target, point) + } + + private fun clickLevel(list: SettingsInlineList, key: String) { + val jList = jbList(list) + val model = jList.model + val idx = (0 until model.size).first { (model.getElementAt(it) as SettingsListItem).key == key } + jList.selectedIndex = idx + jList.setSize(400, jList.preferredSize.height.coerceAtLeast(50)) + jList.doLayout() + val bounds = settingsListCellBounds(jList, idx, true)["level"] ?: error("missing level cell for $key") + click(jList, Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2)) + } + + private fun click(target: JComponent, point: Point) { + val press = MouseEvent( + target, + MouseEvent.MOUSE_PRESSED, + System.currentTimeMillis(), + InputEvent.BUTTON1_DOWN_MASK, + point.x, + point.y, + 1, + false, + MouseEvent.BUTTON1, + ) + val release = MouseEvent( + target, + MouseEvent.MOUSE_RELEASED, + System.currentTimeMillis(), + 0, + point.x, + point.y, + 1, + false, + MouseEvent.BUTTON1, + ) + val clicked = MouseEvent( + target, + MouseEvent.MOUSE_CLICKED, + System.currentTimeMillis(), + 0, + point.x, + point.y, + 1, + false, + MouseEvent.BUTTON1, + ) + dispatch(target, press) + dispatch(target, release) + dispatch(target, clicked) + UIUtil.dispatchAllInvocationEvents() + } + + private fun dispatch(target: JComponent, event: MouseEvent) { + if (target is JBList<*>) { + fire(target, event) + return + } + target.dispatchEvent(event) + } + + private fun doubleClickRow(list: JBList<*>, idx: Int) { + val bounds = list.getCellBounds(idx, idx) + click(list, Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2), count = 2) + } + + private fun click(target: JComponent, point: Point, count: Int) { + val event = MouseEvent( + target, + MouseEvent.MOUSE_CLICKED, + System.currentTimeMillis(), + 0, + point.x, + point.y, + count, + false, + MouseEvent.BUTTON1, + ) + target.dispatchEvent(event) + UIUtil.dispatchAllInvocationEvents() + } + + private fun components(root: java.awt.Component): List { + val out = mutableListOf() + fun visit(item: java.awt.Component) { + out += item + if (item is Container) item.components.forEach { visit(it) } + } + visit(root) + return out + } + + private fun edt(block: () -> T): T { + var result: T? = null + ApplicationManager.getApplication().invokeAndWait { result = block() } + @Suppress("UNCHECKED_CAST") + return result as T + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt index ca7f5cd7c8e..bc08ff92498 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsListViewTest.kt @@ -4,9 +4,11 @@ import ai.kilocode.client.ui.UiStyle import ai.kilocode.client.testing.fire import com.intellij.openapi.application.ApplicationManager import com.intellij.ui.CollectionListModel +import com.intellij.ui.ScrollingUtil import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBScrollPane import com.intellij.testFramework.fixtures.BasePlatformTestCase import com.intellij.util.ui.UIUtil import java.awt.Container @@ -14,10 +16,13 @@ import java.awt.Dimension import java.awt.Point import java.awt.event.InputEvent import java.awt.event.MouseEvent +import javax.swing.ListSelectionModel +import javax.swing.Scrollable +import javax.swing.SwingConstants import javax.swing.SwingUtilities class SettingsListViewTest : BasePlatformTestCase() { - fun `test list owns formatted description tooltip`() { + fun `test list shows description tooltip over row body`() { edt { val view = SettingsListView("Empty") { _, _ -> } val row = item("with", "Alpha", "Use text\nAcross lines") @@ -29,25 +34,23 @@ class SettingsListViewTest : BasePlatformTestCase() { val bounds = view.list.getCellBounds(0, 0) val tip = view.list.getToolTipText(event(view.list, Point(bounds.x + 4, bounds.y + 4))) - assertNotNull(tip) - assertTrue(tip, tip!!.startsWith("")) - assertTrue(tip, tip.contains("Use <safe> text")) - assertTrue(tip, tip.contains("
Across lines")) + assertEquals("Use <safe> text
Across lines", tip) } } - fun `test list description tooltip ignores blank rows and outside points`() { + fun `test tooltip config suppresses description tooltip but keeps action tooltip`() { edt { - val view = SettingsListView("Empty") { _, _ -> } - view.update(listOf(item("without", "Beta", null))) - view.list.size = Dimension(320, 80) - view.list.doLayout() - UIUtil.dispatchAllInvocationEvents() + val cfg = SettingsListConfig.Equal.copy(tooltip = false) + val view = SettingsListView("Empty", cfg) { _, _ -> } + val row = item("with", "Alpha", "Description", SettingsListCell("edit", "Edit", alwaysVisible = true)) + view.update(listOf(row)) + layout(view) val bounds = view.list.getCellBounds(0, 0) + val area = settingsListCellBounds(view.list, 0, selected = true).getValue("edit") assertNull(view.list.getToolTipText(event(view.list, Point(bounds.x + 4, bounds.y + 4)))) - assertNull(view.list.getToolTipText(event(view.list, Point(4, bounds.y + bounds.height + 20)))) + assertEquals("Edit", view.list.getToolTipText(event(view.list, center(area)))) } } @@ -236,6 +239,109 @@ class SettingsListViewTest : BasePlatformTestCase() { } } + fun `test unfocused selected row is not painted as active`() { + edt { + val row = item("with", "Alpha", "Description") + val model = CollectionListModel(listOf(row)) + val list = JBList(model) + val renderer = SettingsListRenderer(model, SettingsListConfig.Equal) + + renderer.getListCellRendererComponent(list, row, 0, true, false) + + val desc = components(renderer).filterIsInstance().single { it.text == "Description" } + assertEquals(UiStyle.Colors.weak(), desc.foreground) + } + } + + fun `test in-place action cells are hidden on unfocused selected row`() { + edt { + val row = item("with", "Alpha", "Description", SettingsListCell("edit", "Edit")) + val model = CollectionListModel(listOf(row)) + val list = JBList(model) + val renderer = SettingsListRenderer(model, SettingsListConfig.Equal) + + renderer.getListCellRendererComponent(list, row, 0, true, false) + assertTrue(actionCells(renderer).none { it.isVisible }) + + renderer.getListCellRendererComponent(list, row, 0, true, true) + assertEquals(listOf("edit"), actionCells(renderer).filter { it.isVisible }.map { it.cellId }) + } + } + + fun `test always visible action cells stay on unfocused row`() { + edt { + val row = item("with", "Alpha", "Description", SettingsListCell("level", "Allow", alwaysVisible = true)) + val model = CollectionListModel(listOf(row)) + val list = JBList(model) + val renderer = SettingsListRenderer(model, SettingsListConfig.Equal) + + renderer.getListCellRendererComponent(list, row, 0, true, false) + + assertEquals(listOf("level"), actionCells(renderer).filter { it.isVisible }.map { it.cellId }) + } + } + + fun `test active popup paints selected row as active without focus`() { + edt { + val row = item("with", "Alpha", "Description") + val model = CollectionListModel(listOf(row)) + val list = object : JBList(model), SettingsListActive { + override fun active(): Boolean = true + } + val renderer = SettingsListRenderer(model, SettingsListConfig.Equal) + + renderer.getListCellRendererComponent(list, row, 0, true, false) + + val desc = components(renderer).filterIsInstance().single { it.text == "Description" } + assertEquals(UIUtil.getListForeground(true, true), desc.foreground) + } + } + + fun `test action click invokes on second selected row in multi selection list`() { + edt { + val calls = mutableListOf() + val cfg = SettingsListConfig.Equal.copy(selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) + val view = SettingsListView("Empty", cfg) { key, id -> calls += "$key:$id" } + view.update(listOf( + item("a", "Alpha", null, SettingsListCell("edit", "Edit", alwaysVisible = false)), + item("b", "Beta", null, SettingsListCell("edit", "Edit", alwaysVisible = false)), + )) + layout(view) + view.list.selectedIndices = intArrayOf(0, 1) + + val area = settingsListCellBounds(view.list, 1, selected = true).getValue("edit") + click(view, center(area)) + + assertEquals(listOf("b:edit"), calls) + } + } + + fun `test preserve no scroll keeps scroll position after row change`() { + edt { + val view = SettingsListView("Empty") { _, _ -> } + val rows = (0 until 30).map { item("row$it", "Row $it", null, SettingsListCell("level", "Allow", alwaysVisible = true)) } + view.update(rows) + val scroll = JBScrollPane(view.list) + scroll.size = Dimension(320, 80) + scroll.doLayout() + view.list.doLayout() + UIUtil.dispatchAllInvocationEvents() + + view.list.selectedIndex = 25 + ScrollingUtil.ensureIndexIsVisible(view.list, 25, 0) + scroll.doLayout() + UIUtil.dispatchAllInvocationEvents() + val before = scroll.viewport.viewPosition.y + assertTrue("expected a scrolled viewport", before > 0) + + view.update(rows, SettingsListSelection.PreserveNoScroll) + UIUtil.dispatchAllInvocationEvents() + + assertEquals(before, scroll.viewport.viewPosition.y) + assertEquals("row25", view.selected()?.key) + } + } + fun `test update selects preferred key`() { edt { val view = SettingsListView("Empty") { _, _ -> } @@ -260,6 +366,17 @@ class SettingsListViewTest : BasePlatformTestCase() { } } + fun `test list view tracks viewport width`() { + edt { + val view = SettingsListView("Empty") { _, _ -> } + view.update(listOf(item("long", "Alpha", "A very long description that should wrap instead of scrolling"))) + + assertTrue((view as Scrollable).getScrollableTracksViewportWidth()) + assertFalse(view.getScrollableTracksViewportHeight()) + assertEquals(160, view.getScrollableBlockIncrement(java.awt.Rectangle(0, 0, 320, 160), SwingConstants.VERTICAL, 1)) + } + } + private fun item(id: String, name: String, note: String?, vararg cells: SettingsListCell) = object : SettingsListItem { override val key = id override val title = name @@ -279,6 +396,9 @@ class SettingsListViewTest : BasePlatformTestCase() { UIUtil.dispatchAllInvocationEvents() } + private fun actionCells(root: java.awt.Component): List = + components(root).filterIsInstance() + private fun components(root: java.awt.Component): List { val out = mutableListOf() fun visit(item: java.awt.Component) { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsPathDialogTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsPathDialogTest.kt new file mode 100644 index 00000000000..e9d4f30dfd5 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsPathDialogTest.kt @@ -0,0 +1,34 @@ +package ai.kilocode.client.settings.base + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.ui.components.JBTextField + +class SettingsPathDialogTest : BasePlatformTestCase() { + fun `test browse input wraps the field and writes the chosen path`() { + val field = JBTextField() + val input = settingsPathInput(field) { "/chosen" } + assertSame(field, input.childComponent) + @Suppress("DEPRECATION") + input.button.doClick() + assertEquals("/chosen", field.text) + } + + fun `test browse variant dialog focuses the field`() { + val dialog = SettingsPathDialog("Add Instruction File", "", browse = { "/chosen" }) + try { + assertTrue(dialog.preferredFocusedComponent is JBTextField) + } finally { + dialog.close(0) + } + } + + fun `test plain variant dialog focuses the field and exposes its value`() { + val dialog = SettingsPathDialog("Add Skill URL", "https://x") + try { + assertTrue(dialog.preferredFocusedComponent is JBTextField) + assertEquals("https://x", dialog.value()) + } finally { + dialog.close(0) + } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsRowsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsRowsTest.kt index 8267e793990..b4c13fda1a8 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsRowsTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/base/SettingsRowsTest.kt @@ -6,6 +6,7 @@ import com.intellij.ui.SeparatorComponent import com.intellij.ui.components.JBLabel import java.awt.Color import java.awt.Container +import java.awt.BorderLayout import java.awt.Rectangle import java.awt.image.BufferedImage import javax.swing.AbstractButton @@ -134,6 +135,17 @@ class SettingsRowsTest : BasePlatformTestCase() { assertTrue((view as Scrollable).getScrollableTracksViewportWidth()) } + fun `test settings panel header gets right inset outside scroll`() { + val panel = SettingsPanel() + val field = javax.swing.JTextField() + + panel.setHeader(field) + + val header = (panel.content.layout as BorderLayout).getLayoutComponent(BorderLayout.NORTH) as JComponent + assertEquals(UiStyle.Gap.xl(), header.insets.right) + assertTrue(components(header).any { it === field }) + } + fun `test settings progress overlay is centered near top`() { val panel = SettingsPanel().apply { setSize(400, 300) } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt index 84c823e1519..6654fbc536b 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/providers/ProvidersSettingsUiTest.kt @@ -1215,7 +1215,8 @@ class ProvidersSettingsUiTest : BasePlatformTestCase() { private fun render(renderer: SettingsListRenderer, list: JBList, row: ProviderListRow, selected: Boolean) { @Suppress("UNCHECKED_CAST") - renderer.getListCellRendererComponent(list as JList, row, 0, selected, false) + // A selected row exposes its in-place actions only when the selection is visible (focused). + renderer.getListCellRendererComponent(list as JList, row, 0, selected, selected) } private fun actionTexts(renderer: SettingsListRenderer): List = components(renderer) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsStateTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsStateTest.kt new file mode 100644 index 00000000000..c68d7d42326 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsStateTest.kt @@ -0,0 +1,64 @@ +package ai.kilocode.client.settings.rules + +import ai.kilocode.rpc.dto.ConfigDto +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +class RulesSettingsStateTest { + @Test + fun `draft reads instructions and compat`() { + val draft = rulesDraft(ConfigDto(instructions = listOf("./RULES.md")), true) + + assertEquals(listOf("./RULES.md"), draft.instructions) + assertTrue(draft.compat) + } + + @Test + fun `unchanged instructions emit no config patch`() { + val draft = RulesDraft(instructions = listOf("./RULES.md"), compat = true) + + assertNull(configPatch(draft, draft)) + } + + @Test + fun `changed instructions emit full list`() { + val from = RulesDraft(instructions = listOf("./RULES.md")) + val to = RulesDraft(instructions = listOf("./RULES.md", "./TEAM.md")) + + assertEquals(listOf("./RULES.md", "./TEAM.md"), configPatch(from, to)?.instructions) + } + + @Test + fun `empty instructions list is emitted`() { + val from = RulesDraft(instructions = listOf("./RULES.md")) + val to = RulesDraft(instructions = emptyList()) + + assertEquals(emptyList(), configPatch(from, to)?.instructions) + } + + @Test + fun `saved match compares instructions compat and staged edits`() { + assertTrue(savedMatches(RulesDraft(listOf("a"), true), RulesDraft(listOf("a"), true))) + assertFalse(savedMatches(RulesDraft(listOf("a"), true), RulesDraft(listOf("b"), true))) + assertFalse(savedMatches(RulesDraft(listOf("a"), true), RulesDraft(listOf("a"), false))) + assertFalse(savedMatches(RulesDraft(listOf("a"), true), RulesDraft(listOf("a"), true, mapOf("a" to "x")))) + } + + @Test + fun `change captures config compat and edits`() { + val from = RulesDraft(listOf("a"), false) + assertNull(rulesChange(from, from)) + + val edited = rulesChange(from, from.copy(edited = mapOf("a" to "x"))) + assertNull(edited?.config) + assertNull(edited?.compat) + assertEquals(mapOf("a" to "x"), edited?.edited) + + val both = rulesChange(from, RulesDraft(listOf("a", "b"), true)) + assertEquals(listOf("a", "b"), both?.config?.instructions) + assertEquals(true, both?.compat) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUiTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUiTest.kt new file mode 100644 index 00000000000..70097d24043 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/settings/rules/RulesSettingsUiTest.kt @@ -0,0 +1,401 @@ +package ai.kilocode.client.settings.rules + +import ai.kilocode.client.app.KiloAgentBehaviorService +import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.settings.base.SettingsListItem +import ai.kilocode.client.settings.base.SettingsToggle +import ai.kilocode.client.settings.base.settingsListCellBounds +import ai.kilocode.client.testing.FakeAgentBehaviorRpcApi +import ai.kilocode.client.testing.FakeAppRpcApi +import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.client.testing.TestCoroutines +import ai.kilocode.client.testing.fire +import ai.kilocode.rpc.dto.ConfigDto +import ai.kilocode.rpc.dto.KiloAppStateDto +import ai.kilocode.rpc.dto.KiloAppStatusDto +import com.intellij.openapi.actionSystem.impl.ActionButton +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.TestDialog +import com.intellij.openapi.ui.TestDialogManager +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.ui.TitledSeparator +import com.intellij.ui.components.JBList +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.ui.UIUtil +import java.awt.BorderLayout +import java.awt.Container +import java.awt.Dimension +import java.awt.Point +import java.awt.event.InputEvent +import java.awt.event.MouseEvent +import javax.swing.JComponent +import javax.swing.ScrollPaneConstants + +class RulesSettingsUiTest : BasePlatformTestCase() { + private lateinit var appCoroutines: TestCoroutines + private lateinit var uiCoroutines: TestCoroutines + private lateinit var rpc: FakeAppRpcApi + private lateinit var workspaceRpc: FakeWorkspaceRpcApi + private lateinit var agentRpc: FakeAgentBehaviorRpcApi + private lateinit var app: KiloAppService + private lateinit var workspaces: KiloWorkspaceService + private lateinit var agent: KiloAgentBehaviorService + private val writes = mutableListOf>() + private var ui: RulesSettingsUi? = null + + override fun tearDown() { + try { + TestDialogManager.setTestDialog(TestDialog.DEFAULT) + val panel = ui + if (panel != null) edt { panel.dispose() } + ui = null + if (::uiCoroutines.isInitialized) uiCoroutines.close(::pump) + if (::appCoroutines.isInitialized) appCoroutines.close(::pump) + } finally { + super.tearDown() + } + } + + fun `test rules list is center with claude footer south and right padding`() { + val panel = panel() + flushUntil { rows(panel).size == 1 } + + edt { + val pane = scrollFor(panel, rulesList(panel)) + val layout = panel.content.layout as BorderLayout + assertSame(pane, layout.getLayoutComponent(BorderLayout.CENTER)) + assertSame(panel.footer, layout.getLayoutComponent(BorderLayout.SOUTH)) + assertEquals(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER, pane.horizontalScrollBarPolicy) + assertTrue(panel.footer.insets.right > 0) + assertTrue(components(panel.footer).filterIsInstance().any { it.text == "Claude Code Compatibility" }) + } + } + + fun `test toolbar has add action and no refresh action`() { + val panel = panel() + flushUntil { rows(panel).size == 1 } + + edt { + val texts = components(panel).filterIsInstance().mapNotNull { it.presentation.text } + assertTrue(texts.any { it == "Add file" }) + assertFalse(texts.any { it.contains("Refresh", ignoreCase = true) }) + } + } + + fun `test rows use standard action cells`() { + val panel = panel() + flushUntil { rows(panel).size == 1 } + + edt { + val row = rows(panel).single() + assertEquals("./RULES.md", row.title) + assertEquals("edit", row.doubleClick) + assertEquals(listOf("open", "edit", "delete"), row.cells.map { it.id }) + assertTrue(row.cells.single { it.id == "open" }.primary) + assertEquals("Edit", row.cells.single { it.id == "edit" }.label) + assertTrue(row.cells.single { it.id == "delete" }.iconOnly) + } + } + + fun `test add file stages instructions patch only`() { + val panel = panel(input = { "./TEAM.md" }) + flushUntil { rows(panel).size == 1 } + + edt { + panel.addFile() + panel.applyDraft() + } + + flushUntil { rpc.configPatches.isNotEmpty() && !edt { panel.modified() } } + assertEquals(listOf("./RULES.md", "./TEAM.md"), rpc.configPatches.single().instructions) + assertTrue(agentRpc.compatSaves.isEmpty()) + assertTrue(writes.isEmpty()) + } + + fun `test edit opens content editor and writes file on apply without config patch`() { + val edited = mutableListOf>() + val panel = panel( + read = { path -> "# $path" }, + editor = { title, content -> + edited += title to content + FakeContentDialog("# edited") + }, + ) + flushUntil { rows(panel).size == 1 } + + doubleClick(rulesList(panel), panel, "./RULES.md") + assertEquals(listOf("./RULES.md" to "# ./RULES.md"), edited) + assertTrue(edt { panel.modified() }) + assertTrue(rpc.configPatches.isEmpty()) + + edt { panel.applyDraft() } + flushUntil { writes.isNotEmpty() && !edt { panel.modified() } } + assertEquals(listOf("./RULES.md" to "# edited"), writes) + assertTrue(rpc.configPatches.isEmpty()) + } + + fun `test reopening staged edit shows draft content`() { + val seen = mutableListOf() + val panel = panel( + read = { "# disk" }, + editor = { _, content -> + seen += content + FakeContentDialog("# draft") + }, + ) + flushUntil { rows(panel).size == 1 } + + doubleClick(rulesList(panel), panel, "./RULES.md") + doubleClick(rulesList(panel), panel, "./RULES.md") + + assertEquals(listOf("# disk", "# draft"), seen) + } + + fun `test edit is a no-op when file content is unavailable`() { + var opened = false + val panel = panel(read = { null }, editor = { _, _ -> opened = true; FakeContentDialog("x") }) + flushUntil { rows(panel).size == 1 } + + doubleClick(rulesList(panel), panel, "./RULES.md") + + assertFalse(opened) + assertFalse(edt { panel.modified() }) + } + + fun `test open in editor action opens instruction file`() { + val panel = panel(root = "/repo") + flushUntil { rows(panel).size == 1 } + + click(rulesList(panel), panel, "./RULES.md", "open") + + flushUntil { workspaceRpc.openedFiles.size == 1 } + assertEquals(FakeWorkspaceRpcApi.Opened("/repo/RULES.md", null, null), workspaceRpc.openedFiles.single()) + } + + fun `test delete action stages removal until apply`() { + val panel = panel() + flushUntil { rows(panel).size == 1 } + TestDialogManager.setTestDialog(TestDialog.YES) + + click(rulesList(panel), panel, "./RULES.md", "delete") + assertTrue(edt { rows(panel).isEmpty() }) + edt { panel.applyDraft() } + + flushUntil { rpc.configPatches.isNotEmpty() } + assertEquals(emptyList(), rpc.configPatches.single().instructions) + } + + fun `test delete action requires confirmation`() { + val panel = panel() + flushUntil { rows(panel).size == 1 } + TestDialogManager.setTestDialog { Messages.NO } + + click(rulesList(panel), panel, "./RULES.md", "delete") + + assertEquals(listOf("./RULES.md"), edt { rows(panel).map { it.key } }) + assertFalse(edt { panel.modified() }) + } + + fun `test toggling compat saves compat only`() { + val panel = panel() + flushUntil { rows(panel).size == 1 } + + edt { + toggle(panel).doClick() + panel.applyDraft() + } + + flushUntil { agentRpc.compatSaves.isNotEmpty() && !edt { panel.modified() } } + assertEquals(listOf(false), agentRpc.compatSaves) + assertTrue(rpc.configPatches.isEmpty()) + } + + fun `test save survives dialog dispose on ok`() { + val panel = panel(input = { "./TEAM.md" }) + flushUntil { rows(panel).size == 1 } + + edt { + panel.addFile() + panel.applyDraft() + // Emulate the platform disposing the configurable immediately after apply() on OK. + panel.dispose() + } + ui = null + + flushUntil { rpc.configPatches.isNotEmpty() } + assertEquals(listOf("./RULES.md", "./TEAM.md"), rpc.configPatches.single().instructions) + } + + fun `test reset restores seeded baseline`() { + val panel = panel(input = { "./TEAM.md" }, read = { "# disk" }, editor = { _, _ -> FakeContentDialog("# edited") }) + flushUntil { rows(panel).size == 1 } + + edt { + panel.addFile() + toggle(panel).doClick() + assertTrue(panel.modified()) + panel.resetDraft() + assertFalse(panel.modified()) + assertEquals(listOf("./RULES.md"), rows(panel).map { it.key }) + assertTrue(toggle(panel).isSelected) + } + } + + fun `test content editor dialog exposes content`() { + edt { + val dialog = InstructionEditDialog("./RULES.md", "# Rules") + try { + assertEquals("# Rules", dialog.content()) + } finally { + dialog.close(0) + } + } + } + + fun `test content scroll renders an editor field`() { + edt { + val field = ai.kilocode.client.settings.base.SettingsContentField( + "# Rules", + ai.kilocode.client.settings.base.settingsEditorFileType("./RULES.md", "# Rules"), + true, + ) + val scroll = ai.kilocode.client.settings.base.settingsContentScroll(field) + assertTrue(components(scroll).any { it is com.intellij.ui.EditorTextField }) + } + } + + fun `test rule path descriptor chooses files`() { + assertTrue(rulePathDescriptor().isChooseFiles) + assertFalse(rulePathDescriptor().isChooseFolders) + } + + private fun panel( + root: String? = null, + choose: (JComponent) -> String? = { null }, + input: () -> String? = { null }, + read: (String) -> String? = { null }, + editor: (String, String) -> RuleContentDialogHandle = { _, _ -> FakeContentDialog("") }, + ): RulesSettingsUi { + install() + val write: (String, String) -> Boolean = { path, text -> writes += path to text; true } + val panel = edt { RulesSettingsUi(uiCoroutines.scope, root, choose, input, read, write, editor, app, workspaces, agent) } + ui = panel + return panel + } + + private fun install() { + appCoroutines = TestCoroutines() + uiCoroutines = TestCoroutines() + rpc = FakeAppRpcApi() + workspaceRpc = FakeWorkspaceRpcApi() + agentRpc = FakeAgentBehaviorRpcApi() + agentRpc.claudeCodeCompat = true + app = KiloAppService(appCoroutines.scope, rpc) + workspaces = KiloWorkspaceService(appCoroutines.scope, workspaceRpc) + agent = KiloAgentBehaviorService(appCoroutines.scope, agentRpc) + val state = KiloAppStateDto( + KiloAppStatusDto.READY, + config = ConfigDto(instructions = listOf("./RULES.md")), + ) + rpc.state.value = state + app._state.value = state + } + + private fun click(list: JBList, panel: RulesSettingsUi, key: String, id: String) { + edt { + list.size = Dimension(520, 320) + list.doLayout() + val idx = rows(panel).indexOfFirst { it.key == key } + list.selectedIndex = idx + val area = settingsListCellBounds(list, idx, selected = true).getValue(id) + click(list, center(area)) + } + } + + private fun doubleClick(list: JBList, panel: RulesSettingsUi, key: String) { + edt { + list.size = Dimension(520, 320) + list.doLayout() + val idx = rows(panel).indexOfFirst { it.key == key } + list.selectedIndex = idx + val area = list.getCellBounds(idx, idx) + fire(list, mouse(list, MouseEvent.MOUSE_CLICKED, center(area), count = 2)) + } + } + + private fun rows(panel: RulesSettingsUi): List { + val list = rulesList(panel) + val model = list.model + return (0 until model.size).map { model.getElementAt(it) } + } + + private fun rulesList(panel: RulesSettingsUi) = components(panel).filterIsInstance>().single() + + private fun toggle(panel: RulesSettingsUi): SettingsToggle = components(panel).filterIsInstance().single() + + private fun scrollFor(panel: RulesSettingsUi, list: JBList) = components(panel) + .filterIsInstance() + .single { pane -> pane.viewport.view === list.parent } + + private fun center(rect: java.awt.Rectangle) = Point(rect.x + rect.width / 2, rect.y + rect.height / 2) + + private fun click(list: JBList, point: Point) { + fire(list, mouse(list, MouseEvent.MOUSE_PRESSED, point)) + fire(list, mouse(list, MouseEvent.MOUSE_RELEASED, point)) + } + + private fun mouse(list: JBList, id: Int, point: Point, count: Int = 1) = MouseEvent( + list, + id, + System.currentTimeMillis(), + if (id == MouseEvent.MOUSE_PRESSED) InputEvent.BUTTON1_DOWN_MASK else 0, + point.x, + point.y, + count, + false, + MouseEvent.BUTTON1, + ) + + private fun edt(block: () -> T): T { + var result: T? = null + ApplicationManager.getApplication().invokeAndWait { result = block() } + @Suppress("UNCHECKED_CAST") + return result as T + } + + private fun flushUntil(done: () -> Boolean) { + repeat(200) { + flush() + if (done()) return + } + flush() + assertTrue(done()) + } + + private fun flush() { + appCoroutines.drain(::pump) + uiCoroutines.drain(::pump) + pump() + } + + private fun pump() { + edt { UIUtil.dispatchAllInvocationEvents() } + } + + private fun components(root: java.awt.Component): List { + val out = mutableListOf() + fun visit(item: java.awt.Component) { + out += item + if (item is Container) item.components.forEach { visit(it) } + } + visit(root) + return out + } +} + +private class FakeContentDialog(private val text: String) : RuleContentDialogHandle { + override fun showAndGet() = true + override fun content() = text +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt index bbcc4873170..87f13a939c4 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAgentBehaviorRpcApi.kt @@ -11,9 +11,14 @@ import ai.kilocode.rpc.dto.SkillDto class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { var agents = emptyList() + var skills = emptyList() var mcps = emptyList() var mcpConfigs = emptyMap() val agentCalls = mutableListOf() + val skillCalls = mutableListOf() + val skillRemovals = mutableListOf>() + val skillReloads = mutableListOf() + val skillSaves = mutableListOf>() val mcpCalls = mutableListOf() val mcpConfigCalls = mutableListOf() val mcpSaves = mutableListOf>() @@ -27,13 +32,21 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { var afterRemove: (suspend (String, String) -> Unit)? = null var afterMcpConnect: (suspend (String, String) -> Unit)? = null var createError: Exception? = null + var skillsError: Exception? = null var removeError: Exception? = null + var removeSkillError: Exception? = null + var saveSkillError: Exception? = null var mcpStatusError: Exception? = null var mcpConnectError: Exception? = null var removeResult = true + var removeSkillResult = true + var reloadSkillResult = true + var saveSkillResult = true var mcpConnectResult = true var mcpDisconnectResult = true var mcpAuthenticateResult = true + var claudeCodeCompat = false + val compatSaves = mutableListOf() override suspend fun agents(directory: String): List { assertNotEdt("agentBehavior.agents") @@ -43,12 +56,41 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { override suspend fun skills(directory: String): List { assertNotEdt("agentBehavior.skills") - return emptyList() + skillsError?.let { throw it } + skillCalls.add(directory) + return skills } override suspend fun removeSkill(directory: String, location: String): Boolean { assertNotEdt("agentBehavior.removeSkill") - return false + removeSkillError?.let { throw it } + skillRemovals.add(directory to location) + if (removeSkillResult) skills = skills.filterNot { it.location == location } + return removeSkillResult + } + + override suspend fun reloadSkills(directory: String): Boolean { + assertNotEdt("agentBehavior.reloadSkills") + skillReloads.add(directory) + return reloadSkillResult + } + + override suspend fun saveSkill(directory: String, location: String, content: String): Boolean { + assertNotEdt("agentBehavior.saveSkill") + saveSkillError?.let { throw it } + skillSaves.add(Triple(directory, location, content)) + if (saveSkillResult) skills = skills.map { if (it.location == location) it.copy(content = content) else it } + return saveSkillResult + } + + override suspend fun saveSkills(directory: String, edits: Map): Boolean { + assertNotEdt("agentBehavior.saveSkills") + saveSkillError?.let { throw it } + for ((location, content) in edits) skillSaves.add(Triple(directory, location, content)) + if (saveSkillResult) skills = skills.map { skill -> + edits[skill.location]?.let { skill.copy(content = it) } ?: skill + } + return saveSkillResult } override suspend fun removeAgent(directory: String, name: String): Boolean { @@ -123,11 +165,13 @@ class FakeAgentBehaviorRpcApi : KiloAgentBehaviorRpcApi { override suspend fun claudeCodeCompat(): Boolean { assertNotEdt("agentBehavior.claudeCodeCompat") - return false + return claudeCodeCompat } override suspend fun setClaudeCodeCompat(value: Boolean): Boolean { assertNotEdt("agentBehavior.setClaudeCodeCompat") + compatSaves.add(value) + claudeCodeCompat = value return value } } diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt index 57868aadf0d..7c7d4a9affe 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt @@ -14,6 +14,8 @@ import ai.kilocode.rpc.dto.ModelSelectionDto import ai.kilocode.rpc.dto.ModelSelectionUpdateDto import ai.kilocode.rpc.dto.ModelStateDto import ai.kilocode.rpc.dto.ModelVariantUpdateDto +import ai.kilocode.rpc.dto.PermissionConfigDto +import ai.kilocode.rpc.dto.PermissionRuleDto import ai.kilocode.rpc.dto.ProfileDto import ai.kilocode.rpc.dto.SkillsConfigDto import ai.kilocode.rpc.dto.TelemetryCaptureDto @@ -230,9 +232,35 @@ class FakeAppRpcApi : KiloAppRpcApi { skills = patch.skills?.let { SkillsConfigDto(paths = it.paths.orEmpty(), urls = it.urls.orEmpty()) } ?: config.skills, mcp = mcp, agent = agents, + permission = mergePermission(config.permission, patch.permission), ) } + /** Mirrors the CLI's PATCH deep-merge for `config.permission`: `null` deletes a tool/pattern. */ + private fun mergePermission(base: PermissionConfigDto?, patch: PermissionConfigDto?): PermissionConfigDto? { + if (patch == null) return base + val result = (base ?: emptyMap()).toMutableMap() + for ((tool, rule) in patch) { + when (rule) { + is PermissionRuleDto.Level -> { + if (rule.value == null) result.remove(tool) else result[tool] = rule + } + is PermissionRuleDto.Patterns -> { + val merged = when (val old = result[tool]) { + is PermissionRuleDto.Level -> old.value?.let { mapOf("*" to it) } ?: emptyMap() + is PermissionRuleDto.Patterns -> old.map + null -> emptyMap() + }.toMutableMap() + for ((pattern, level) in rule.map) { + if (level == null) merged.remove(pattern) else merged[pattern] = level + } + if (merged.isEmpty()) result.remove(tool) else result[tool] = PermissionRuleDto.Patterns(merged) + } + } + } + return result.takeIf { it.isNotEmpty() } + } + var fakeProfile: ProfileDto? = null var fakeDeviceAuth = DeviceAuthDto(code = "TEST-1234", verificationUrl = "https://auth.kilo.ai/device") val orgProfiles = mutableMapOf() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt index 4c1b60ef21b..817fc677e99 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeWorkspaceRpcApi.kt @@ -43,6 +43,7 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi { var globalConfigExists = true var beforeLocalConfigTarget: (suspend () -> Unit)? = null var beforeGlobalConfigTarget: (suspend () -> Unit)? = null + var refreshConfigThrows: Exception? = null val fileCalls = CopyOnWriteArrayList>() val searchQueries = CopyOnWriteArrayList() val opened = CopyOnWriteArrayList() @@ -53,6 +54,7 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi { private set var globalConfigPathCalls = 0 private set + val refreshedConfigs = CopyOnWriteArrayList() override suspend fun resolveProjectDirectory(projectId: ProjectId?, hint: String): String { assertNotEdt("resolveProjectDirectory") @@ -113,6 +115,12 @@ class FakeWorkspaceRpcApi : KiloWorkspaceRpcApi { return ConfigTargetDto(globalConfigPath, globalConfigDisplayPath, globalConfigExists) } + override suspend fun refreshConfigFiles(directory: String) { + assertNotEdt("refreshConfigFiles") + refreshedConfigs.add(directory) + refreshConfigThrows?.let { throw it } + } + override suspend fun openLocalConfig(directory: String): Boolean { assertNotEdt("openLocalConfig") localConfigs.add(directory) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdLanguageTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdLanguageTest.kt index 7921173f52c..06793dd446f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdLanguageTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdLanguageTest.kt @@ -16,6 +16,10 @@ class MdLanguageTest : BasePlatformTestCase() { assertKind("ansi-stdout", Stream.Stdout, Mode.Ansi) assertKind("terminal", Stream.Stdout, Mode.Ansi) assertKind("terminal-output", Stream.Stdout, Mode.Ansi) + assertKind("bash", Stream.Stdout, Mode.Command) + assertKind("shell", Stream.Stdout, Mode.Command) + assertKind("zsh", Stream.Stdout, Mode.Command) + assertKind("shellscript", Stream.Stdout, Mode.Command) assertKind("shell-command", Stream.Stdout, Mode.Command) assertKind("shell-output", Stream.Stdout, Mode.Shell) assertKind("ansi-stderr", Stream.Stderr, Mode.Ansi) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdShellHighlightTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdShellHighlightTest.kt index 47da0f01e23..39f10251963 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdShellHighlightTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdShellHighlightTest.kt @@ -34,10 +34,10 @@ class MdShellHighlightTest : BasePlatformTestCase() { } fun `test command highlights commands flags strings and env vars`() { - val display = MdShellHighlight.command("FOO=bar; git commit -m 'hello world' --amend") + val display = MdShellHighlight.command("$ FOO=bar; git commit -m 'hello world' --amend") val spans = spans(display) - assertTrue(spans.contains("git" to DefaultLanguageHighlighterColors.FUNCTION_CALL)) + assertTrue(spans.contains("git" to DefaultLanguageHighlighterColors.KEYWORD)) assertTrue(spans.contains("-m" to DefaultLanguageHighlighterColors.KEYWORD)) assertTrue(spans.contains("--amend" to DefaultLanguageHighlighterColors.KEYWORD)) assertTrue(spans.contains("'hello world'" to DefaultLanguageHighlighterColors.STRING)) diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt index e78d86f8782..a8f9b91f412 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/MdViewHybridTest.kt @@ -273,6 +273,8 @@ class MdViewHybridTest : BasePlatformTestCase() { assertTrue(iter.isValid) val rect = pane.modelToView2D(iter.startOffset)!!.bounds + // Real AWT delivers MOUSE_ENTERED before MOUSE_MOVED; the enter arms scroll tracking. + pane.dispatchEvent(MouseEvent(pane, MouseEvent.MOUSE_ENTERED, System.currentTimeMillis(), 0, rect.x + 1, rect.y + rect.height / 2, 0, false, MouseEvent.NOBUTTON)) pane.dispatchEvent(MouseEvent(pane, MouseEvent.MOUSE_MOVED, System.currentTimeMillis(), 0, rect.x + 1, rect.y + rect.height / 2, 0, false, MouseEvent.NOBUTTON)) host.viewport.viewPosition = Point(0, 32) drainEdt() @@ -281,6 +283,24 @@ class MdViewHybridTest : BasePlatformTestCase() { assertTrue(events.contains(HyperlinkEvent.EventType.EXITED)) } + fun `test prose pane tracks viewport scrolls only while hovered`() { + view.set("See [docs](https://example.com)\n\n" + (1..20).joinToString("\n") { "line $it" }) + val pane = htmls().single() + val host = JBScrollPane(view.component) + host.setSize(420, 64) + view.component.setSize(420, view.component.preferredSize.height) + host.doLayout() + view.component.doLayout() + drainEdt() + val base = host.viewport.changeListeners.size + + pane.dispatchEvent(MouseEvent(pane, MouseEvent.MOUSE_ENTERED, System.currentTimeMillis(), 0, 1, 1, 0, false, MouseEvent.NOBUTTON)) + assertEquals("hovered prose pane must follow viewport scrolls", base + 1, host.viewport.changeListeners.size) + + pane.dispatchEvent(MouseEvent(pane, MouseEvent.MOUSE_EXITED, System.currentTimeMillis(), 0, -1, -1, 0, false, MouseEvent.NOBUTTON)) + assertEquals("pane must stop following scrolls once the pointer leaves", base, host.viewport.changeListeners.size) + } + fun `test file ref links include line suffix and exclude punctuation`() { view.set("See kilocode/session/prompt.ts:302, native-plan-prompt.txt:37-38.") val html = view.html() @@ -574,10 +594,20 @@ class MdViewHybridTest : BasePlatformTestCase() { assertSame(type("js"), editors().single().fileType) } - fun `test shell code fence resolves shell file type`() { - view.set("```shell\necho hi\n```") + fun `test bash code fence renders terminal semantic highlighters`() { + view.set("```bash\ngit log -30 --oneline --decorate\n```") + val field = editors().single() + val editor = field.getEditor(true)!! + val spans = editor.markupModel.allHighlighters.map { + field.text.substring(it.startOffset, it.endOffset) to it.textAttributesKey + } - assertSame(type("sh"), editors().single().fileType) + assertSame(PlainTextFileType.INSTANCE, field.fileType) + assertEquals("git log -30 --oneline --decorate", field.text) + assertTrue(spans.contains("git" to DefaultLanguageHighlighterColors.KEYWORD)) + assertTrue(spans.contains("-30" to DefaultLanguageHighlighterColors.KEYWORD)) + assertTrue(spans.contains("--oneline" to DefaultLanguageHighlighterColors.KEYWORD)) + assertTrue(spans.contains("--decorate" to DefaultLanguageHighlighterColors.KEYWORD)) } fun `test shell command code fence renders terminal semantic highlighters`() { @@ -590,7 +620,7 @@ class MdViewHybridTest : BasePlatformTestCase() { assertSame(PlainTextFileType.INSTANCE, field.fileType) assertEquals("git log -30 --oneline --decorate", field.text) - assertTrue(spans.contains("git" to DefaultLanguageHighlighterColors.FUNCTION_CALL)) + assertTrue(spans.contains("git" to DefaultLanguageHighlighterColors.KEYWORD)) assertTrue(spans.contains("-30" to DefaultLanguageHighlighterColors.KEYWORD)) assertTrue(spans.contains("--oneline" to DefaultLanguageHighlighterColors.KEYWORD)) assertTrue(spans.contains("--decorate" to DefaultLanguageHighlighterColors.KEYWORD)) @@ -611,17 +641,13 @@ class MdViewHybridTest : BasePlatformTestCase() { assertEquals("git status --short", field.text) assertTrue(editor.markupModel.allHighlighters.map { field.text.substring(it.startOffset, it.endOffset) to it.textAttributesKey - }.contains("git" to DefaultLanguageHighlighterColors.FUNCTION_CALL)) + }.contains("git" to DefaultLanguageHighlighterColors.KEYWORD)) } - fun `test shell script aliases resolve shell file type`() { + fun `test shell script metadata resolves shell file type`() { view.set("```shell script\necho hi\n```") assertSame(type("sh"), editors().single().fileType) - - view.set("```zsh\necho hi\n```") - - assertSame(type("sh"), editors().single().fileType) } fun `test fenced code block ignores whitespace metadata`() { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/hybrid/MdDiffHighlightTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/hybrid/MdDiffHighlightTest.kt new file mode 100644 index 00000000000..ad8da445a14 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/ui/md/hybrid/MdDiffHighlightTest.kt @@ -0,0 +1,31 @@ +package ai.kilocode.client.ui.md.hybrid + +import com.intellij.openapi.diff.DiffColors +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class MdDiffHighlightTest : BasePlatformTestCase() { + + fun `test inserted line whose content starts with plus plus is not dimmed as a header`() { + // "++x;" is an inserted line ("+" marker + "+x;" content), not a "+++" file header. + val out = MdDiffHighlight.display("++x;") + + assertEquals(1, out.spans.size) + assertEquals(DiffColors.DIFF_INSERTED, out.spans.single().span.key) + } + + fun `test deleted line whose content starts with a dash is not dimmed as a header`() { + // "--x" is a deleted line ("-" marker + "-x" content), not a "---" file header. + val out = MdDiffHighlight.display("--x") + + assertEquals(1, out.spans.size) + assertEquals(DiffColors.DIFF_DELETED, out.spans.single().span.key) + } + + fun `test real file headers are dimmed as comments`() { + val out = MdDiffHighlight.display("--- a/File.kt\n+++ b/File.kt") + + assertEquals(2, out.spans.size) + assertTrue(out.spans.all { it.span.key == DefaultLanguageHighlighterColors.LINE_COMMENT }) + } +} diff --git a/packages/kilo-jetbrains/gradle.properties b/packages/kilo-jetbrains/gradle.properties index cf34869c98e..6782f168445 100644 --- a/packages/kilo-jetbrains/gradle.properties +++ b/packages/kilo-jetbrains/gradle.properties @@ -1,5 +1,5 @@ kotlin.stdlib.default.dependency=false -kilo.jetbrains.version=7.0.7 +kilo.jetbrains.version=7.0.10 # When true (default) the JetBrains plugin uses the pinned CLI release from package.json. # Set to false ONLY for local dev: generate the client from local source + bundle the local binary. # false is NOT releasable -- production builds fail unless this is true. diff --git a/packages/kilo-jetbrains/package.json b/packages/kilo-jetbrains/package.json index e988f663c74..fee84e7b2a4 100644 --- a/packages/kilo-jetbrains/package.json +++ b/packages/kilo-jetbrains/package.json @@ -8,7 +8,7 @@ "test": "./gradlew test", "test:ci": "bun script/test-ci.ts" }, - "version": "7.4.11", + "version": "7.4.16", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt index 345ab0657cd..b13c03d526a 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAgentBehaviorRpcApi.kt @@ -26,6 +26,12 @@ interface KiloAgentBehaviorRpcApi : RemoteApi { suspend fun removeSkill(directory: String, location: String): Boolean + suspend fun reloadSkills(directory: String): Boolean + + suspend fun saveSkill(directory: String, location: String, content: String): Boolean + + suspend fun saveSkills(directory: String, edits: Map): Boolean + suspend fun removeAgent(directory: String, name: String): Boolean suspend fun createAgent(directory: String, input: AgentCreateDto): Boolean diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt index 1e267a07290..e85e5b27668 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloWorkspaceRpcApi.kt @@ -63,6 +63,9 @@ interface KiloWorkspaceRpcApi : RemoteApi { /** Resolve the editable global config target. */ suspend fun globalConfigTarget(): ConfigTargetDto + /** Refresh local and global config files after external CLI writes. */ + suspend fun refreshConfigFiles(directory: String) + /** Open or create the local config file in the IDE. */ suspend fun openLocalConfig(directory: String): Boolean diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt index cfc0ef47774..dbb053f8fd5 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/ChatDto.kt @@ -299,10 +299,18 @@ data class PermissionRequestDto( val message: String? = null, val command: String? = null, val rules: List = emptyList(), + val ruleDecisions: List = emptyList(), val filePath: String? = null, val fileDiffs: List = emptyList(), ) +@Serializable +data class PermissionRuleDecisionDto( + val pattern: String, + val decision: String = "pending", + val defaultDecision: String = decision, +) + @Serializable data class ToolRefDto( val messageID: String, diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt index 1ab2f302bde..1324b3073d7 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/KiloAppStateDto.kt @@ -70,6 +70,7 @@ data class ConfigDto( val skills: SkillsConfigDto? = null, val mcp: Map = emptyMap(), val agent: Map = emptyMap(), + val permission: PermissionConfigDto? = null, ) @Serializable @@ -129,6 +130,7 @@ data class ConfigPatchDto( val skills: SkillsPatchDto? = null, val mcp: Map? = null, val agents: Map = emptyMap(), + val permission: PermissionConfigDto? = null, ) @Serializable diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt index 6bdd4f9805c..50dfee7adf2 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/SkillDto.kt @@ -7,4 +7,6 @@ data class SkillDto( val name: String, val description: String? = null, val location: String, + val content: String? = null, + val editable: Boolean = false, ) diff --git a/packages/kilo-memory/package.json b/packages/kilo-memory/package.json index 5b6a4aab73b..df2adc33257 100644 --- a/packages/kilo-memory/package.json +++ b/packages/kilo-memory/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-memory", - "version": "7.4.13", + "version": "7.4.16", "type": "module", "license": "MIT", "description": "Project memory storage, indexing, recall, and command helpers for Kilo Code", diff --git a/packages/kilo-memory/src/commands.ts b/packages/kilo-memory/src/commands.ts index 8fcdbb8d4c1..0b7d48d453d 100644 --- a/packages/kilo-memory/src/commands.ts +++ b/packages/kilo-memory/src/commands.ts @@ -2,13 +2,12 @@ export const MEMORY_COMMAND_CATALOG = [ { usage: "on", description: "Enable project memory" }, { usage: "off", description: "Disable project memory" }, { usage: "status", description: "Storage location and stored memory overview" }, - { usage: "show", description: "Full audit view (sources, index, changes, decisions)" }, + { usage: "show", description: "Stored project memory overview" }, { usage: "remember ", description: "Save a project memory note" }, { usage: "correct ", description: "Save a correction to project memory" }, { usage: "forget ", description: "Remove matching project memory" }, { usage: "auto on|off", description: "Turn automatic memory saves on or off" }, - { usage: "verbose on|off", description: "Turn verbose memory details on or off" }, - { usage: "edit", description: "Open project.md in $VISUAL/$EDITOR, then rebuild" }, + { usage: "inspect", description: "Reveal the project memory folder" }, { usage: "rebuild", description: "Rebuild the memory index from source files" }, { usage: "purge confirm", description: "Delete all project memory files" }, ] as const @@ -18,7 +17,7 @@ export const MEMORY_USAGE = `/memory [project] ${MEMORY_COMMAND_CATALOG.map((ite export const MEMORY_OPERATIONS = [ "enable", "status", - "edit", + "inspect", "disable", "rebuild", "remember", @@ -26,21 +25,13 @@ export const MEMORY_OPERATIONS = [ "forget", "purge", "auto", - "verbose", ] as const -export const MEMORY_PROMPT_OPERATIONS = ["remember", "forget"] as const - export type MemoryOperation = (typeof MEMORY_OPERATIONS)[number] -export type MemoryPromptOperation = (typeof MEMORY_PROMPT_OPERATIONS)[number] export function isMemoryOperation(input: unknown): input is MemoryOperation { return typeof input === "string" && (MEMORY_OPERATIONS as readonly string[]).includes(input) } -export function isMemoryPromptOperation(input: unknown): input is MemoryPromptOperation { - return typeof input === "string" && (MEMORY_PROMPT_OPERATIONS as readonly string[]).includes(input) -} - type Help = { kind: "help" } @@ -62,7 +53,7 @@ type Operation = } | { kind: "operation" - operation: "auto" | "verbose" + operation: "auto" mode: "on" | "off" } | { @@ -72,7 +63,7 @@ type Operation = } | { kind: "operation" - operation: Exclude + operation: Exclude } type Usage = { @@ -104,7 +95,7 @@ function usage(reason: string): ParsedMemoryCommand { function operation(verb: string, text: string): ParsedMemoryCommand | undefined { if (verb === "on" || verb === "enable") return { kind: "operation", operation: "enable" } if (verb === "off" || verb === "disable") return { kind: "operation", operation: "disable" } - if (verb === "status" || verb === "edit" || verb === "rebuild") { + if (verb === "status" || verb === "inspect" || verb === "rebuild") { return { kind: "operation", operation: verb } } if (verb === "purge") { @@ -116,11 +107,6 @@ function operation(verb: string, text: string): ParsedMemoryCommand | undefined if (mode === "on" || mode === "off") return { kind: "operation", operation: "auto", mode } return usage("Missing auto mode. Run /memory auto on or /memory auto off.") } - if (verb === "verbose") { - const mode = text.toLowerCase() - if (mode === "on" || mode === "off") return { kind: "operation", operation: "verbose", mode } - return usage("Missing verbose mode. Run /memory verbose on or /memory verbose off.") - } if (verb === "remember") { if (text) return { kind: "operation", operation: "remember", text } return usage("Missing text.") diff --git a/packages/kilo-memory/src/effect/index.ts b/packages/kilo-memory/src/effect/index.ts index ca94a98aa6b..4a58e71bf39 100644 --- a/packages/kilo-memory/src/effect/index.ts +++ b/packages/kilo-memory/src/effect/index.ts @@ -46,7 +46,9 @@ export namespace KiloMemory { } export async function prepare(input: Input) { - return root(input) + const dir = root(input) + await MemoryFiles.cleanup(dir) + return dir } export async function status(input: Input) { diff --git a/packages/kilo-memory/src/storage/audit.ts b/packages/kilo-memory/src/storage/audit.ts index 1369ea1c677..70c7c42bf3a 100644 --- a/packages/kilo-memory/src/storage/audit.ts +++ b/packages/kilo-memory/src/storage/audit.ts @@ -1,13 +1,7 @@ -import { appendFile, chmod } from "fs/promises" -import path from "path" import z from "zod" import { MemoryFs } from "./fs" -import { MemoryPaths } from "./paths" -import { MemoryRedact } from "../capture/redact" export namespace MemoryAudit { - const MAX_LOG = 128_000 - const LOG_MARGIN = 16_000 const Log = z .object({ kind: z.literal("log"), @@ -49,45 +43,10 @@ export namespace MemoryAudit { }[] } - function cap(input: string) { - if (Buffer.byteLength(input) <= MAX_LOG) return input - const lines = input.split("\n").reverse() - const kept: string[] = [] - lines.reduce((sum, line) => { - if (sum >= MAX_LOG) return sum - kept.push(line) - return sum + Buffer.byteLength(`${line}\n`) - }, 0) - return kept.reverse().join("\n") - } - - async function line(file: string, text: string) { - await MemoryFs.dir(path.dirname(file)) - const info = await MemoryFs.guard(file) - if (info && !info.isFile()) throw new Error(`memory path is not a file: ${file}`) - await appendFile(file, text, { mode: MemoryFs.FILE }) - await chmod(file, MemoryFs.FILE).catch((error: unknown) => { - if (process.platform === "win32") return - throw error - }) - const next = await MemoryFs.guard(file) - if (!next?.isFile()) throw new Error(`memory path is not a file: ${file}`) - if (next.size <= MAX_LOG + LOG_MARGIN) return - await MemoryFs.write(file, cap((await MemoryFs.read(file)) ?? "")) - } - - async function audit(root: string, input: Decision) { - const data = MemoryRedact.value(input) as Decision - await MemoryFs.queue(root, () => - line( - MemoryPaths.files(root).decisions, - `${JSON.stringify({ - v: 1, - time: new Date().toISOString(), - ...data, - })}\n`, - ), - ) + function audit(root: string, input: Decision) { + void root + void input + return Promise.resolve() } export async function append(root: string, text: string) { @@ -99,12 +58,8 @@ export namespace MemoryAudit { } export async function readDecisions(root: string) { - return MemoryFs.read(MemoryPaths.files(root).decisions) - .then((text) => text ?? "") - .catch((error: unknown) => { - if (MemoryFs.miss(error)) return "" - throw error - }) + void root + return "" } function record(input: string) { diff --git a/packages/kilo-memory/src/storage/fs.ts b/packages/kilo-memory/src/storage/fs.ts index 3b0130ff189..e9df6dcf17c 100644 --- a/packages/kilo-memory/src/storage/fs.ts +++ b/packages/kilo-memory/src/storage/fs.ts @@ -109,6 +109,15 @@ export namespace MemoryFs { return readFile(file, "utf8") } + export async function remove(file: string) { + await parents(path.dirname(file)) + const info = await guard(file) + if (!info) return false + if (!info.isFile()) throw new Error(`memory path is not a file: ${file}`) + await rm(file, { force: true }) + return true + } + export async function json(file: string) { const text = await read(file) return text === undefined ? undefined : JSON.parse(text) diff --git a/packages/kilo-memory/src/storage/state.ts b/packages/kilo-memory/src/storage/state.ts index e3e1728ec36..bb373e24610 100644 --- a/packages/kilo-memory/src/storage/state.ts +++ b/packages/kilo-memory/src/storage/state.ts @@ -10,6 +10,9 @@ import { MemoryText } from "../text" import { MemoryTopics } from "../recall/topics" export namespace MemoryState { + const CLEAN_LIMIT = 128 + const CLEAN_RETRY_MS = 60_000 + const cleaned = new Map() const seed: Record = { "project.md": "# Project Memory\n\n## Facts\n\n## Decisions\n\n## Constraints\n\n## Open Questions\n", "environment.md": "# Environment Memory\n\n## Commands\n\n## Paths\n\n## Tooling\n", @@ -92,6 +95,38 @@ export namespace MemoryState { ) } + export async function cleanup(root: string) { + const retry = cleaned.get(root) + if (retry !== undefined && retry > Date.now()) return false + cleaned.delete(root) + const owns = await owned(root).catch((error: unknown) => { + MemoryFs.warn("failed to inspect legacy memory audit", { error, root }) + return undefined + }) + if (owns !== true) { + if (owns === undefined) cache(root, Date.now() + CLEAN_RETRY_MS) + return false + } + const removed = await MemoryFs.remove(MemoryPaths.files(root).decisions).catch((error: unknown) => { + MemoryFs.warn("failed to remove legacy memory audit", { error, root }) + return undefined + }) + if (removed === undefined) { + cache(root, Date.now() + CLEAN_RETRY_MS) + return false + } + cache(root, Number.POSITIVE_INFINITY) + return removed + } + + function cache(root: string, retry: number) { + cleaned.delete(root) + cleaned.set(root, retry) + if (cleaned.size <= CLEAN_LIMIT) return + const key = cleaned.keys().next().value + if (typeof key === "string") cleaned.delete(key) + } + export async function readIndex(root: string) { const file = MemoryPaths.files(root).index return MemoryFs.read(file) @@ -218,8 +253,9 @@ export namespace MemoryState { index: await readIndex(root), inventory, items: await inspect(root, inventory), - changes: await MemoryAudit.readChanges(root), - decisions: await MemoryAudit.readDecisions(root), + // Retain empty fields for wire compatibility while the legacy audit file is removed. + changes: "", + decisions: "", } } diff --git a/packages/kilo-memory/src/storage/store.ts b/packages/kilo-memory/src/storage/store.ts index c7ca4be45d0..8d79568f6c9 100644 --- a/packages/kilo-memory/src/storage/store.ts +++ b/packages/kilo-memory/src/storage/store.ts @@ -27,6 +27,7 @@ export namespace MemoryFiles { export const indexExpired = MemoryState.indexExpired export const scaffold = MemoryState.scaffold export const owned = MemoryState.owned + export const cleanup = MemoryState.cleanup export const writeSession = MemorySessions.writeSession export const readSession = MemorySessions.readSession diff --git a/packages/kilo-memory/test/command-cases.json b/packages/kilo-memory/test/command-cases.json index 1992f2dc48e..dea1a430422 100644 --- a/packages/kilo-memory/test/command-cases.json +++ b/packages/kilo-memory/test/command-cases.json @@ -26,10 +26,10 @@ "operation": "status" }, { - "name": "edit operation", - "input": "/memory edit", + "name": "inspect operation", + "input": "/memory inspect", "result": "operation", - "operation": "edit" + "operation": "inspect" }, { "name": "on operation", @@ -94,26 +94,6 @@ "operation": "auto", "mode": "off" }, - { - "name": "verbose mode usage", - "input": "/memory verbose", - "result": "usage", - "reason": "Missing verbose mode" - }, - { - "name": "verbose on operation", - "input": "/memory verbose on", - "result": "operation", - "operation": "verbose", - "mode": "on" - }, - { - "name": "verbose off operation", - "input": "/memory verbose off", - "result": "operation", - "operation": "verbose", - "mode": "off" - }, { "name": "remember operation keeps text", "input": "/memory remember use bun test from packages/opencode", @@ -148,12 +128,6 @@ "operation": "auto", "mode": "off" }, - { - "name": "inspect action is unknown", - "input": "/memory inspect", - "result": "usage", - "reason": "Unknown memory action" - }, { "name": "unknown action", "input": "/memory wat", diff --git a/packages/kilo-memory/test/commands.test.ts b/packages/kilo-memory/test/commands.test.ts index b91d4baad89..c5661b137a5 100644 --- a/packages/kilo-memory/test/commands.test.ts +++ b/packages/kilo-memory/test/commands.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { parseMemoryCommand, type MemoryOperation, type ParsedMemoryCommand } from "../src/commands" +import { MEMORY_USAGE, parseMemoryCommand, type MemoryOperation, type ParsedMemoryCommand } from "../src/commands" type Case = { name: string @@ -29,7 +29,7 @@ function expected(item: Case): ParsedMemoryCommand | undefined { if (!item.query) throw new Error(`Missing query for fixture: ${item.name}`) return { kind: "operation", operation: item.operation, query: item.query } } - if (item.operation === "auto" || item.operation === "verbose") { + if (item.operation === "auto") { if (!item.mode) throw new Error(`Missing mode for fixture: ${item.name}`) return { kind: "operation", operation: item.operation, mode: item.mode } } @@ -41,6 +41,23 @@ function expected(item: Case): ParsedMemoryCommand | undefined { } describe("memory commands", () => { + test("does not expose verbose mode", () => { + expect(MEMORY_USAGE).not.toContain("verbose") + expect(parseMemoryCommand("/memory verbose on")).toEqual({ + kind: "usage", + reason: "Unknown memory action: verbose.", + }) + }) + + test("replaces edit with inspect", () => { + expect(MEMORY_USAGE).toContain("inspect") + expect(MEMORY_USAGE).not.toContain("edit") + expect(parseMemoryCommand("/memory edit")).toEqual({ + kind: "usage", + reason: "Unknown memory action: edit.", + }) + }) + test("parse shared fixtures", () => { for (const item of cases) { const parsed = parseMemoryCommand(item.input) diff --git a/packages/kilo-memory/test/core.test.ts b/packages/kilo-memory/test/core.test.ts index 388dcb6c1b1..04b52a03624 100644 --- a/packages/kilo-memory/test/core.test.ts +++ b/packages/kilo-memory/test/core.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, test } from "bun:test" +import { describe, expect, spyOn, test } from "bun:test" import { mkdir, mkdtemp, readdir, rm, symlink, utimes, writeFile } from "fs/promises" import os from "os" import path from "path" @@ -10,6 +10,7 @@ import { MemoryOperations } from "../src/capture/operations" import { MemoryPaths } from "../src/storage/paths" import { MemoryRecall } from "../src/recall/recall" import { MemorySchema } from "../src/schema" +import { KiloMemory } from "../src/effect/index" async function tmp() { const dir = await mkdtemp(path.join(os.tmpdir(), "kilo-memory-")) @@ -43,13 +44,119 @@ describe("memory core package", () => { expect(shown.sources.corrections).toContain("## Corrections") expect(await Bun.file(path.join(t.root, ".gitignore")).text()).toBe("*\n!.gitignore\n") expect(shown.index).toBe("") + expect(shown.changes).toBe("") + expect(shown.decisions).toBe("") + expect(await Bun.file(path.join(t.root, "decisions.jsonl")).exists()).toBe(false) + }) + }) + + test("prepare removes legacy decisions once from owned memory roots", async () => { + await use(async (t) => { + await Memory.enable({ root: t.root }) + const legacy = path.join(t.root, "decisions.jsonl") + await writeFile(legacy, '{"kind":"log"}\n') + + await KiloMemory.status({ root: t.root }) + expect(await Bun.file(legacy).exists()).toBe(false) + + await writeFile(legacy, '{"kind":"log"}\n') + await KiloMemory.status({ root: t.root }) + expect(await Bun.file(legacy).exists()).toBe(true) + + const other = path.join(t.dir, "unowned") + await mkdir(other) + const file = path.join(other, "decisions.jsonl") + await writeFile(file, '{"kind":"log"}\n') + + await KiloMemory.status({ root: other }) + expect(await Bun.file(file).exists()).toBe(true) + }) + }) + + test("prepare ignores legacy decisions cleanup failures", async () => { + const clock = spyOn(Date, "now") + const now = Date.now() + clock.mockReturnValue(now) + try { + await use(async (t) => { + await Memory.enable({ root: t.root }) + const legacy = path.join(t.root, "decisions.jsonl") + await mkdir(legacy) + + const status = await KiloMemory.status({ root: t.root }) + expect(status.state.enabled).toBe(true) + + await rm(legacy, { recursive: true }) + await writeFile(legacy, '{"kind":"log"}\n') + await KiloMemory.status({ root: t.root }) + expect(await Bun.file(legacy).exists()).toBe(true) + + clock.mockReturnValue(now + 60_001) + await KiloMemory.status({ root: t.root }) + expect(await Bun.file(legacy).exists()).toBe(false) + }) + } finally { + clock.mockRestore() + } + }) + + test("prepare ignores corrupt manifests during legacy cleanup", async () => { + const clock = spyOn(Date, "now") + const now = Date.now() + clock.mockReturnValue(now) + try { + await use(async (t) => { + await Memory.enable({ root: t.root }) + const paths = MemoryPaths.files(t.root) + await writeFile(paths.manifest, "{") + + const status = await KiloMemory.status({ root: t.root }) + expect(status.state.enabled).toBe(true) + + await writeFile(paths.manifest, '{"kind":"kilo-memory","version":1}\n') + await writeFile(paths.decisions, '{"kind":"log"}\n') + await KiloMemory.status({ root: t.root }) + expect(await Bun.file(paths.decisions).exists()).toBe(true) + + clock.mockReturnValue(now + 60_001) + await KiloMemory.status({ root: t.root }) + expect(await Bun.file(paths.decisions).exists()).toBe(false) + }) + } finally { + clock.mockRestore() + } + }) + + test("legacy cleanup cache evicts older roots", async () => { + await use(async (t) => { + const first = path.join(t.dir, "cache-0") + await mkdir(first) + await MemoryFiles.writeManifest(first) + await MemoryFiles.cleanup(first) + const legacy = MemoryPaths.files(first).decisions + await writeFile(legacy, '{"kind":"log"}\n') + + for (let i = 1; i <= 128; i++) { + const root = path.join(t.dir, `cache-${i}`) + await mkdir(root) + await MemoryFiles.writeManifest(root) + await MemoryFiles.cleanup(root) + } + + await MemoryFiles.cleanup(first) + expect(await Bun.file(legacy).exists()).toBe(false) }) }) test("enable preserves existing memory settings", async () => { await use(async (t) => { const enabled = await Memory.enable({ root: t.root }) - await MemoryFiles.writeState(t.root, { ...enabled.state, autoInject: false, autoConsolidate: false, verbose: true }) + await MemoryFiles.writeState(t.root, { + ...enabled.state, + autoInject: false, + autoConsolidate: false, + verbose: true, + }) const next = await Memory.enable({ root: t.root }) @@ -111,43 +218,7 @@ describe("memory core package", () => { }) }) - test("decision and change audit records redact secret-like text in one log", async () => { - await use(async (t) => { - const secret = "sk-abcdefghijklmnopqrstuvwxyz123456" - await Memory.enable({ root: t.root }) - await MemoryFiles.decide(t.root, { - kind: "recall", - result: "skipped", - query: `check api_key=${secret}`, - skipped: [{ reason: "secret", text: `password=hunter2 ${secret}` }], - }) - await MemoryFiles.append(t.root, `provider error "api_key": "${secret}"`) - const shown = await Memory.show({ root: t.root }) - - expect(shown.decisions).toContain("[redacted]") - expect(shown.decisions).toContain('"kind":"log"') - expect(shown.decisions).not.toContain(secret) - expect(shown.decisions).not.toContain("hunter2") - expect(shown.changes).toContain("[redacted]") - expect(shown.decisions).toContain("provider error") - }) - }) - - test("targeted recall redacts query before decision truncation", async () => { - await use(async (t) => { - const secret = "sk-" + "a".repeat(40) - await Memory.enable({ root: t.root }) - - await Memory.recall({ root: t.root, query: "x".repeat(220) + secret }) - const shown = await Memory.show({ root: t.root }) - - expect(shown.decisions).toContain("[redacted]") - expect(shown.decisions).not.toContain(secret) - expect(shown.decisions).not.toContain(secret.slice(0, 20)) - }) - }) - - test("stale locks are stolen before appending audit records", async () => { + test("stale locks are stolen before applying memory", async () => { await use(async (t) => { await Memory.enable({ root: t.root }) const lock = path.join(t.root, ".lock") @@ -155,10 +226,10 @@ describe("memory core package", () => { await mkdir(lock) await utimes(lock, old, old) - await MemoryFiles.append(t.root, "after stale lock") + await Memory.apply({ root: t.root, ops: [{ action: "add", key: "after_lock", text: "Stale locks recover." }] }) const shown = await Memory.show({ root: t.root }) - expect(shown.changes).toContain("after stale lock") + expect(shown.sources.project).toContain("after_lock") }) }) @@ -174,7 +245,6 @@ describe("memory core package", () => { expect(state.enabled).toBe(false) expect(files.some((file) => file.startsWith("state.json.bad-"))).toBe(true) expect(shown.inventory.items).toEqual({}) - expect(shown.changes).toContain("recover state.json") }) }) @@ -280,7 +350,6 @@ describe("memory core package", () => { const shown = await Memory.show({ root: t.root }) expect(mixed.result.added).toBe(1) - // The skip record is redacted: it flows into the persistent decisions audit. expect(mixed.result.skipped).toContainEqual({ reason: "secret", text: "[redacted]" }) expect(JSON.stringify(mixed.result.skipped)).not.toContain("sk-abcdefghijklmnopqrstuvwxyz") expect(shown.sources.project).toContain("safe_fact") @@ -335,7 +404,6 @@ describe("memory core package", () => { ]) expect(shown.sources.project).not.toContain("memory_echo") expect(shown.index).not.toContain("memory_echo") - expect(shown.decisions).toContain('"reason":"self_referential"') }) }) @@ -372,17 +440,10 @@ describe("memory core package", () => { expect(shown.sources.project).not.toContain("Vim keybindings") expect(shown.index).toContain("repo_style") expect(shown.index).not.toContain("reply_style") - expect(shown.decisions).toContain('"reason":"out_of_scope"') - expect(shown.decisions).not.toContain("reply_style") - expect(shown.decisions).not.toContain("theme") - expect(shown.decisions).not.toContain("editor") - expect(shown.decisions).not.toContain("I prefer terse summaries") - expect(shown.decisions).not.toContain("dark mode") - expect(shown.decisions).not.toContain("Vim keybindings") }) }) - test("out-of-scope secret ops stay out of the operations audit", async () => { + test("out-of-scope secret ops stay out of memory", async () => { await use(async (t) => { await Memory.enable({ root: t.root }) @@ -393,9 +454,8 @@ describe("memory core package", () => { const shown = await Memory.show({ root: t.root }) expect(result.result.skipped).toEqual([{ reason: "out_of_scope", text: "My preference is [redacted]" }]) - expect(shown.decisions).toContain('"reason":"out_of_scope"') - expect(shown.decisions).not.toContain("private_pref") - expect(shown.decisions).not.toContain("password=hunter2") + expect(shown.sources.project).not.toContain("private_pref") + expect(shown.sources.project).not.toContain("password=hunter2") }) }) @@ -478,7 +538,7 @@ describe("memory core package", () => { }) }) - test("targeted recall returns typed memory and audits matched files", async () => { + test("targeted recall returns typed memory and matched files", async () => { await use(async (t) => { await Memory.enable({ root: t.root }) await Memory.remember({ @@ -490,12 +550,8 @@ describe("memory core package", () => { }) const result = await Memory.recall({ root: t.root, query: "what command runs cli tests?" }) - const shown = await Memory.show({ root: t.root }) - expect(result.result?.block).toContain("cli_tests") expect(result.files).toEqual(["environment.md"]) - expect(shown.decisions).toContain('"kind":"recall"') - expect(shown.decisions).toContain('"result":"recalled"') }) }) diff --git a/packages/kilo-memory/test/effect-capture.test.ts b/packages/kilo-memory/test/effect-capture.test.ts index ae1eaea0964..91e0ec84e04 100644 --- a/packages/kilo-memory/test/effect-capture.test.ts +++ b/packages/kilo-memory/test/effect-capture.test.ts @@ -80,7 +80,7 @@ function run(input: { } describe("MemoryCapture (fake ports)", () => { - test("turn-close typed LLM saves environment memory and audit records", async () => { + test("turn-close typed LLM saves environment memory", async () => { const t = await tmp() try { await KiloMemory.enable({ root: t.root }) @@ -102,9 +102,6 @@ describe("MemoryCapture (fake ports)", () => { const shown = await KiloMemory.show({ root: t.root }) expect(shown.sources.environment).toContain("cli_memory_tests") - expect(shown.decisions).toContain('"kind":"digest"') - expect(shown.decisions).toContain('"kind":"typed"') - expect(shown.decisions).toContain('"result":"saved"') } finally { await t.done() } @@ -138,9 +135,6 @@ describe("MemoryCapture (fake ports)", () => { const shown = await KiloMemory.show({ root: t.root }) expect(shown.sources.environment).toContain("cli_tests") expect(shown.sources.environment).not.toContain(secret) - expect(shown.decisions).toContain('"reason":"secret"') - // The audit record itself must not carry the raw secret (decisions are exposed via /memory/show). - expect(shown.decisions).not.toContain(secret) const detail = events.find((item) => item.detail?.type === "saved")?.detail expect(detail?.message).toContain("environment.md:cli_tests") expect(detail?.message).not.toContain(secret) @@ -173,18 +167,15 @@ describe("MemoryCapture (fake ports)", () => { sessionID: "ses_effect", max: MemorySchema.maxStoredDigestSummary, }) - const shown = await KiloMemory.show({ root: t.root }) expect(saved?.summary).toContain("[redacted]") expect(saved?.summary).not.toContain(secret) expect(saved?.summary).not.toContain(secret.slice(0, 20)) - expect(shown.decisions).not.toContain(secret) - expect(shown.decisions).not.toContain(secret.slice(0, 20)) } finally { await t.done() } }) - test("turn-close surfaces content-gate rejections in the audit with redacted text", async () => { + test("turn-close rejects self-referential content while applying safe operations", async () => { const t = await tmp() try { await KiloMemory.enable({ root: t.root }) @@ -206,10 +197,6 @@ describe("MemoryCapture (fake ports)", () => { expect(result).toMatchObject({ skipped: false, operationCount: 1 }) const shown = await KiloMemory.show({ root: t.root }) expect(shown.sources.project).not.toContain("gate_check") - // The apply-time content gate is visible in the audit, and its recorded text is redacted. - expect(shown.decisions).toContain('"reason":"self_referential"') - expect(shown.decisions).toContain("[redacted]") - expect(shown.decisions).not.toContain("password=hunter2") } finally { await t.done() } @@ -346,7 +333,7 @@ describe("MemoryCapture (fake ports)", () => { } }) - test("interrupted close records a non-LLM fallback digest tagged with the reason", async () => { + test("interrupted close records a non-LLM fallback digest", async () => { const t = await tmp() try { await KiloMemory.enable({ root: t.root }) @@ -367,9 +354,6 @@ describe("MemoryCapture (fake ports)", () => { const raw = await Bun.file(path.join(MemoryPaths.files(t.root).sessions, file)).text() expect(saved?.fallback).toBe(true) expect(raw).toContain("Fallback: true") - const shown = await KiloMemory.show({ root: t.root }) - expect(shown.decisions).toContain("session digest fallback on interrupted") - expect(shown.decisions).toContain('"fallback":true') } finally { await t.done() } @@ -457,7 +441,7 @@ describe("MemoryCapture (fake ports)", () => { } }) - test("template echo digest output falls back and records template_echo", async () => { + test("template echo digest output falls back", async () => { const t = await tmp() try { await KiloMemory.enable({ root: t.root }) @@ -473,16 +457,13 @@ describe("MemoryCapture (fake ports)", () => { }) const saved = await MemoryFiles.readSession(t.root, { sessionID: "ses_effect", max: 480 }) - const shown = await KiloMemory.show({ root: t.root }) expect(saved?.fallback).toBe(true) - expect(shown.decisions).toContain('"reason":"template_echo"') - expect(shown.decisions).toContain('"fallback":true') } finally { await t.done() } }) - test("empty digest output falls back and records empty_digest", async () => { + test("empty digest output falls back", async () => { const t = await tmp() try { await KiloMemory.enable({ root: t.root }) @@ -498,11 +479,8 @@ describe("MemoryCapture (fake ports)", () => { }) const saved = await MemoryFiles.readSession(t.root, { sessionID: "ses_effect", max: 480 }) - const shown = await KiloMemory.show({ root: t.root }) expect(saved?.fallback).toBe(true) expect(saved?.summary).toContain("User:") - expect(shown.decisions).toContain('"reason":"empty_digest"') - expect(shown.decisions).toContain('"fallback":true') } finally { await t.done() } @@ -603,7 +581,7 @@ describe("MemoryCapture (fake ports)", () => { } }) - test("records audit when configured memory model is unavailable", async () => { + test("configured memory model fallback still captures memory", async () => { const t = await tmp() try { await KiloMemory.enable({ root: t.root }) @@ -620,8 +598,8 @@ describe("MemoryCapture (fake ports)", () => { }), }) - const shown = await KiloMemory.show({ root: t.root }) - expect(shown.changes).toContain("memory_model_config reason=model unavailable fallback=1") + const saved = await MemoryFiles.readSession(t.root, { sessionID: "ses_effect", max: 480 }) + expect(saved?.summary).toContain("Explored repo setup") } finally { await t.done() } diff --git a/packages/kilo-sandbox/package.json b/packages/kilo-sandbox/package.json index 94fd594b9a2..ff29edd756d 100644 --- a/packages/kilo-sandbox/package.json +++ b/packages/kilo-sandbox/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/sandbox", - "version": "7.4.13", + "version": "7.4.16", "type": "module", "license": "MIT", "private": true, diff --git a/packages/kilo-sandbox/src/bubblewrap.ts b/packages/kilo-sandbox/src/bubblewrap.ts index 6c28b2e42c5..c2905d9302a 100644 --- a/packages/kilo-sandbox/src/bubblewrap.ts +++ b/packages/kilo-sandbox/src/bubblewrap.ts @@ -98,6 +98,26 @@ function validate(allow: ReadonlyArray, executable: string, mounts: Re } } +function code(cause: unknown) { + if (typeof cause !== "object" || cause === null || !("code" in cause)) return undefined + const value = (cause as { code: unknown }).code + return typeof value === "string" ? value : undefined +} + +// Lists one directory during the deny-name scan. A directory that vanished mid-scan +// (ENOENT/ENOTDIR) is treated as empty, and an unreadable directory (EACCES/EPERM) +// yields undefined so the caller can protect it instead of failing the whole scan. +function list(dir: string) { + try { + return readdirSync(dir, { withFileTypes: true }) + } catch (cause) { + const tag = code(cause) + if (tag === "ENOENT" || tag === "ENOTDIR") return [] + if (tag === "EACCES" || tag === "EPERM") return undefined + throw cause + } +} + function scan(root: string, names: ReadonlySet, found: Set) { if (names.has(path.basename(root))) { found.add(root) @@ -109,7 +129,16 @@ function scan(root: string, names: ReadonlySet, found: Set) { while (pending.length > 0) { const dir = pending.pop() if (!dir) continue - for (const entry of readdirSync(dir, { withFileTypes: true })) { + const entries = list(dir) + if (!entries) { + // Fail closed: a nested directory that cannot be enumerated might hide a deny-name + // match, so it is re-bound read-only as a whole rather than aborting sandbox setup. + // The writable root itself must stay readable, or there is nothing to scan. + if (dir === root) throw new Error(`Writable root is not readable: ${root}`) + found.add(dir) + continue + } + for (const entry of entries) { const target = path.join(dir, entry.name) if (names.has(entry.name)) { found.add(target) diff --git a/packages/kilo-sandbox/test/backend.test.ts b/packages/kilo-sandbox/test/backend.test.ts index df6981f9d9b..e908df252b6 100644 --- a/packages/kilo-sandbox/test/backend.test.ts +++ b/packages/kilo-sandbox/test/backend.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test" -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { chmodSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs" import os from "node:os" import path from "node:path" import { Effect, PlatformError, Result } from "effect" @@ -34,6 +34,17 @@ const launch: Launch = { }, } +// chmod-based permission tests only work when the test user is not root, +// since root bypasses filesystem permission checks entirely. +function readable(dir: string) { + try { + readdirSync(dir) + return true + } catch { + return false + } +} + describe("sandbox launch preparation", () => { test("generates a globally overriding overlapping deny policy with parameterized paths", () => { const result = generate(makeProfile(), launch) @@ -126,6 +137,60 @@ describe("sandbox launch preparation", () => { } }) + test("re-binds unreadable directories read-only instead of failing Linux setup", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "kilo-bubblewrap-unreadable-")) + const git = path.join(root, ".git") + const secrets = path.join(root, "secrets") + mkdirSync(git) + mkdirSync(secrets) + chmodSync(secrets, 0o000) + const profile: Profile = { + ...makeProfile("allow"), + filesystem: { + allowWrite: [{ path: root, kind: "subtree" }], + denyWrite: [], + denyNames: [".git"], + }, + } + + try { + if (readable(secrets)) return + const result = generateBubblewrap(profile, { ...launch, cwd: root }, "/opt/kilo/bwrap") + const writable = result.args.indexOf("--bind") + expect(result.args.slice(writable, writable + 3)).toEqual(["--bind", root, root]) + const first = result.args.indexOf("--ro-bind", writable + 3) + expect(result.args.slice(first, first + 3)).toEqual(["--ro-bind", git, git]) + const second = result.args.indexOf("--ro-bind", first + 3) + expect(result.args.slice(second, second + 3)).toEqual(["--ro-bind", secrets, secrets]) + } finally { + chmodSync(secrets, 0o700) + rmSync(root, { recursive: true, force: true }) + } + }) + + test("rejects an unreadable writable root", () => { + const root = mkdtempSync(path.join(os.tmpdir(), "kilo-bubblewrap-root-")) + chmodSync(root, 0o000) + const profile: Profile = { + ...makeProfile("allow"), + filesystem: { + allowWrite: [{ path: root, kind: "subtree" }], + denyWrite: [], + denyNames: [".git"], + }, + } + + try { + if (readable(root)) return + expect(() => generateBubblewrap(profile, { ...launch, cwd: root }, "/opt/kilo/bwrap")).toThrow( + `Writable root is not readable: ${root}`, + ) + } finally { + chmodSync(root, 0o700) + rmSync(root, { recursive: true, force: true }) + } + }) + test("parses escaped mount points from Linux mountinfo", () => { const content = [ String.raw`36 25 0:32 / / rw,relatime - overlay overlay rw`, diff --git a/packages/kilo-telemetry/package.json b/packages/kilo-telemetry/package.json index fb6f7203a96..7b9e81497a0 100644 --- a/packages/kilo-telemetry/package.json +++ b/packages/kilo-telemetry/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@kilocode/kilo-telemetry", - "version": "7.4.13", + "version": "7.4.16", "type": "module", "license": "MIT", "description": "Telemetry for Kilo CLI - PostHog analytics integration", diff --git a/packages/kilo-ui/package.json b/packages/kilo-ui/package.json index 68b6edeea73..1fb95bbdc6f 100644 --- a/packages/kilo-ui/package.json +++ b/packages/kilo-ui/package.json @@ -1,6 +1,6 @@ { "name": "@kilocode/kilo-ui", - "version": "7.4.13", + "version": "7.4.16", "type": "module", "license": "MIT", "exports": { diff --git a/packages/kilo-ui/src/components/basic-tool.css b/packages/kilo-ui/src/components/basic-tool.css index 65038a92c5b..c4495f862ff 100644 --- a/packages/kilo-ui/src/components/basic-tool.css +++ b/packages/kilo-ui/src/components/basic-tool.css @@ -7,11 +7,15 @@ } [data-slot="basic-tool-tool-info"] { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; font-size: var(--kilo-font-size-12); text-align: start; } [data-slot="basic-tool-tool-info-structured"] { + width: 100%; min-width: 0; overflow: hidden; } @@ -25,6 +29,7 @@ } [data-slot="basic-tool-tool-info-main"] { + width: 100%; align-items: baseline; overflow: hidden; } @@ -34,6 +39,7 @@ } [data-slot="basic-tool-tool-subtitle"] { + flex: 1 1 auto; font-size: var(--kilo-font-size-12); color: var(--text-weak); } @@ -51,6 +57,8 @@ } [data-slot="basic-tool-tool-arg"] { + flex: 0 1 auto; + max-width: 24ch; font-size: var(--kilo-font-size-12); color: var(--text-weak); } @@ -486,3 +494,25 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty } } } + +/* "why was this allowed" line inside a tool's expanded body */ +[data-slot="tool-approval-line"] { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 4px; + padding: 4px 0 6px; + font-family: var(--font-family-sans); + font-size: var(--font-size-small); + line-height: var(--line-height-normal); + color: var(--text-weak); + + [data-slot="tool-approval-decision"] { + font-weight: var(--font-weight-medium); + color: var(--text-strong); + } + + [data-slot="tool-approval-rule"] { + font-family: var(--font-family-mono); + } +} diff --git a/packages/kilo-ui/src/components/basic-tool.tsx b/packages/kilo-ui/src/components/basic-tool.tsx index 7ba072c7c44..ce545b832ee 100644 --- a/packages/kilo-ui/src/components/basic-tool.tsx +++ b/packages/kilo-ui/src/components/basic-tool.tsx @@ -1,6 +1,8 @@ +import { Show } from "solid-js" import { BasicTool as Base, GenericTool } from "@opencode-ai/ui/basic-tool" import type { BasicToolProps as BaseProps, TriggerTitle } from "@opencode-ai/ui/basic-tool" import { toolOpenKey, readToolOpen, writeToolOpen } from "./tool-open-state" +import { useToolApproval, ToolApprovalLine } from "./tool-approval" export { GenericTool } export type { TriggerTitle } @@ -20,16 +22,24 @@ export function initialOpen(props: OpenProps) { export function BasicTool(props: BasicToolProps) { const key = () => toolOpenKey(props) const initial = () => initialOpen(props) + const approval = useToolApproval() const change = (open: boolean) => { writeToolOpen(key(), open) props.onOpenChange?.(open) } - if (!("children" in props)) { + // The "why was this allowed" line lives in the expanded body, above any tool-specific details. + const details = () => ( +
+ {(value) => } + {props.children} +
+ ) + if (!("children" in props) && !approval()) { return } return ( - -
{props.children}
+ + {details()} ) } diff --git a/packages/kilo-ui/src/components/diff.tsx b/packages/kilo-ui/src/components/diff.tsx index 947cb2b4dc3..cee9fba7a47 100644 --- a/packages/kilo-ui/src/components/diff.tsx +++ b/packages/kilo-ui/src/components/diff.tsx @@ -740,6 +740,8 @@ export function Diff(props: DiffProps) { containerWrapper: container, }) } else { + const oldFile = local.before! + const newFile = local.after! const beforeContents = before() const afterContents = after() @@ -749,8 +751,8 @@ export function Diff(props: DiffProps) { } instance.render({ - oldFile: { ...local.before, contents: beforeContents, cacheKey: cacheKey(beforeContents) }, - newFile: { ...local.after, contents: afterContents, cacheKey: cacheKey(afterContents) }, + oldFile: { ...oldFile, contents: beforeContents, cacheKey: cacheKey(beforeContents) }, + newFile: { ...newFile, contents: afterContents, cacheKey: cacheKey(afterContents) }, lineAnnotations: annotations, containerWrapper: container, }) diff --git a/packages/kilo-ui/src/components/icon.tsx b/packages/kilo-ui/src/components/icon.tsx index 8509d5a2238..96e8621531e 100644 --- a/packages/kilo-ui/src/components/icon.tsx +++ b/packages/kilo-ui/src/components/icon.tsx @@ -54,6 +54,10 @@ const icons: Record = { viewBox: "0 0 24 24", path: ``, }, + gauge: { + viewBox: "0 0 24 24", + path: ``, + }, } type Name = keyof typeof icons diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index b711188f076..c7991e93922 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -1,5 +1,17 @@ /* Kilo Message Part overrides */ +/* Message action rows (copy, fork, revert, feedback, delete queued) + share one compact icon-button size. */ +[data-component="text-part"] [data-slot="text-part-copy-wrapper"][data-is-turn-copy], +[data-component="text-part"] [data-slot="assistant-copy-wrapper"], +[data-component="user-message"] [data-slot="user-message-copy-wrapper"], +[data-component="user-message"] [data-slot="user-message-queued-indicator"] { + [data-component="icon-button"] { + width: 20px; + height: 20px; + } +} + [data-component="text-part"] { margin-top: 8px; @@ -13,11 +25,6 @@ [data-slot="text-part-copy-wrapper"][data-is-turn-copy] { display: flex; - - [data-component="icon-button"] { - width: 20px; - height: 20px; - } } [data-slot="assistant-copy-wrapper"] { @@ -27,16 +34,19 @@ gap: 2px; margin-top: 2px; - [data-component="icon-button"] { - width: 20px; - height: 20px; - } - /* Thumbs up/down: fill the outline on hover to preview the rated state. */ [data-component="icon-button"][data-icon="thumbs-up"]:hover [data-slot="icon-svg"] path, [data-component="icon-button"][data-icon="thumbs-down"]:hover [data-slot="icon-svg"] path { fill: currentColor; } + + /* Throughput badge sits to the right of the copy/feedback buttons, + beside them rather than beneath the message. */ + [data-slot="assistant-throughput-inline"] { + margin-left: 6px; + display: flex; + align-items: center; + } } } @@ -403,6 +413,9 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] { } [data-slot="user-message-queued-indicator"] { + display: inline-flex; + align-items: center; + gap: 6px; margin-top: 6px; margin-right: 2px; font-size: var(--font-size-small); @@ -502,11 +515,6 @@ html[data-theme="kilo-vscode"] [data-component="todos"] { [data-slot="user-message-meta-wrap"] { display: none; } - - [data-component="icon-button"] { - width: 20px; - height: 20px; - } } } diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 1bbc8443c49..18344caa97a 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -47,6 +47,8 @@ import { checksum } from "@opencode-ai/core/util/encode" import { Tooltip } from "./tooltip" import { IconButton } from "./icon-button" import { TextShimmer } from "@opencode-ai/ui/text-shimmer" +import { ToolApprovalProvider, resolveToolApproval } from "./tool-approval" +export { ToolApprovalProvider, resolveToolApproval } from "./tool-approval" import { GrowBox } from "./grow-box" import { COLLAPSIBLE_SPRING } from "./motion" import { busy, createThrottledValue, useToolFade, useContextToolPending } from "./tool-utils" @@ -157,6 +159,7 @@ export interface MessagePartProps { animate?: boolean working?: boolean feedback?: MessageFeedbackControls + throughput?: JSX.Element } export type PartComponent = Component @@ -743,6 +746,7 @@ export function UserMessageDisplay(props: { text?: string copyText?: string header?: JSX.Element + onDelete?: () => void onFork?: () => void onRevert?: () => void }) { @@ -816,6 +820,25 @@ export function UserMessageDisplay(props: { setTimeout(() => setCopied(false), 2000) } + const Delete = () => ( + + + e.preventDefault()} + onClick={(event) => { + event.stopPropagation() + props.onDelete?.() + }} + aria-label={i18n.t("ui.message.deleteQueued")} + /> + + + ) + return (
@@ -852,6 +875,12 @@ export function UserMessageDisplay(props: {
+ +
+ + +
+
<>
@@ -864,6 +893,7 @@ export function UserMessageDisplay(props: {
+
@@ -950,9 +980,23 @@ function HighlightedText(props: { text: string; references: FilePart[]; agents: const data = useData() + const session = (segment: HighlightSegment) => { + const ref = props.references.find((ref) => ref.source?.text?.value === segment.text) + const url = (ref as { url?: unknown } | undefined)?.url + if (typeof url !== "string" || !url.startsWith("session:")) return + return url.slice("session:".length) + } + const click = (segment: HighlightSegment, e: MouseEvent) => { - if (segment.type !== "file" || !data.openFile) return + if (segment.type !== "file") return e.preventDefault() + // Past-chat mentions carry a session: URL — open that session instead of a file. + const id = session(segment) + if (id) { + data.navigateToSession?.(id) + return + } + if (!data.openFile) return const path = segment.text.replace(/^@/, "") if (path) data.openFile(path) } @@ -962,7 +1006,9 @@ function HighlightedText(props: { text: string; references: FilePart[]; agents: {(segment) => ( {segment.text} @@ -991,6 +1037,7 @@ export function Part(props: MessagePartProps) { animate={props.animate} working={props.working} feedback={props.feedback} + throughput={props.throughput} />
) @@ -1099,7 +1146,7 @@ function McpTool(props: ToolProps) { if (typeof value === "boolean") return [`${key}=${value}`] return [] }) - .slice(0, 3) + .slice(0, 1) } const formatted = createMemo(() => { @@ -1264,26 +1311,28 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) { }} - + resolveToolApproval(meta(), i18n.t as (k: string, p?: Record) => string)}> + + @@ -1448,6 +1497,9 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { /> + + {(el) => {el()}} + diff --git a/packages/kilo-ui/src/components/tool-approval.tsx b/packages/kilo-ui/src/components/tool-approval.tsx new file mode 100644 index 00000000000..af07af86ddf --- /dev/null +++ b/packages/kilo-ui/src/components/tool-approval.tsx @@ -0,0 +1,86 @@ +import { createContext, useContext, Show, type Accessor, type ParentProps } from "solid-js" + +/** + * Explains why a tool call was auto-approved, inside the expanded tool row. + * + * The backend records this on the tool part's `state.metadata.approval`. The display strings are + * resolved by the caller (which owns the localized `t`) and carried on the context, so this stays + * free of any i18n key coupling. + */ +export type ToolApproval = { + source: "agent" | "global" | "project" | "yolo" | "session" | "manual" | "default" + agent?: string + rule?: { permission: string; pattern: string; action: string } +} + +/** Pre-resolved, localized text plus the raw approval, supplied by the caller. */ +export type ToolApprovalDisplay = { + approval: ToolApproval + decision: string + source?: string + rule?: string +} + +const SOURCE_KEYS = ["agent", "global", "project", "yolo", "session", "manual", "default"] as const + +const Context = createContext>(() => undefined) + +/** Provide the resolved approval to the tool row below. */ +export function ToolApprovalProvider(props: ParentProps<{ value: Accessor }>) { + return {props.children} +} + +export function useToolApproval() { + return useContext(Context) +} + +/** Read the raw approval payload off a tool part's metadata, if present. */ +export function toolApprovalFrom(metadata: Record | undefined): ToolApproval | undefined { + const value = metadata?.approval + if (!value || typeof value !== "object") return undefined + const approval = value as ToolApproval + return SOURCE_KEYS.includes(approval.source) ? approval : undefined +} + +type Translate = (key: string, params?: Record) => string + +/** Resolve an approval read off metadata into localized display text via the caller's `t`. */ +export function resolveToolApproval( + metadata: Record | undefined, + t: Translate, +): ToolApprovalDisplay | undefined { + const approval = toolApprovalFrom(metadata) + if (!approval) return undefined + const sourceText = () => { + if (approval.source === "agent") + return approval.agent + ? t("ui.approval.source.agent", { agent: approval.agent }) + : t("ui.approval.source.agent.default") + if (approval.source === "manual") return undefined + return t(`ui.approval.source.${approval.source}`) + } + return { + approval, + decision: approval.source === "manual" ? t("ui.approval.manual") : t("ui.approval.auto"), + source: sourceText(), + rule: approval.rule + ? t("ui.approval.rule", { permission: approval.rule.permission, pattern: approval.rule.pattern }) + : undefined, + } +} + +/** The single "why was this allowed" line shown inside a tool row's expanded body. */ +export function ToolApprovalLine(props: { display: ToolApprovalDisplay }) { + const manual = () => props.display.approval.source === "manual" + return ( +
+ {props.display.decision} + + + {(text) => {text()}} + + {(text) => {text()}} + +
+ ) +} diff --git a/packages/kilo-vscode/CHANGELOG.md b/packages/kilo-vscode/CHANGELOG.md index 9131ae9949b..d41e4a8c944 100644 --- a/packages/kilo-vscode/CHANGELOG.md +++ b/packages/kilo-vscode/CHANGELOG.md @@ -1,5 +1,56 @@ # kilo-code +## 7.4.16 + +### Minor Changes + +- [#12370](https://github.com/Kilo-Org/kilocode/pull/12370) [`b367105`](https://github.com/Kilo-Org/kilocode/commit/b367105c8d648c8e05b62c2d27a28a95a4772f61) Thanks [@hdcodedev](https://github.com/hdcodedev)! - Support deleting queued chat messages from the VS Code chat before they run. + +- [#12297](https://github.com/Kilo-Org/kilocode/pull/12297) [`bcff5cb`](https://github.com/Kilo-Org/kilocode/commit/bcff5cb3608f9fc6a6a441405cf50694f6bf3efa) - Emit session queue state so remote clients can show queued messages. + +- [#12456](https://github.com/Kilo-Org/kilocode/pull/12456) [`3d648d7`](https://github.com/Kilo-Org/kilocode/commit/3d648d7fcdc186f86b2c63ab842e70acb1f0aee2) - Reference past chats inline with `@` in the prompt. Typing `@` now surfaces a "Past chats" option that opens a searchable picker of previous sessions (scoped to the current workspace/worktree, searched like the Agent Manager session search); selecting one attaches that session's transcript as context so the model can build on a prior conversation. Clicking the mention opens that session. Available in the CLI TUI and the VS Code extension. + +- [#12462](https://github.com/Kilo-Org/kilocode/pull/12462) [`8eeaa54`](https://github.com/Kilo-Org/kilocode/commit/8eeaa546aeec9c06d513248b42546ec779ab2178) Thanks [@hdcodedev](https://github.com/hdcodedev)! - Add a searchable open-tabs switcher to the sidebar tab bar. + +- [#12494](https://github.com/Kilo-Org/kilocode/pull/12494) [`85dbf44`](https://github.com/Kilo-Org/kilocode/commit/85dbf443af727524a90c1838eeecd37d5011bcaa) Thanks [@bagatao-anaconda](https://github.com/bagatao-anaconda)! - Show why a tool call was auto-approved. Expanding a tool call now explains whether it ran automatically or after your approval, and which rule allowed it — from your agent, the project config, your global config, or auto-approve (YOLO) mode. + +- [#12509](https://github.com/Kilo-Org/kilocode/pull/12509) [`99c04c7`](https://github.com/Kilo-Org/kilocode/commit/99c04c7163efb8cafa6e35f052b743c6dcf96f12) - Filter `/sessions` history to sessions in the current Agent Manager worktree. + +### Patch Changes + +- [#12486](https://github.com/Kilo-Org/kilocode/pull/12486) [`d0e8a86`](https://github.com/Kilo-Org/kilocode/commit/d0e8a86bfce001821441e8ac8d7398bfba6c93f6) Thanks [@hdcodedev](https://github.com/hdcodedev)! - Prevent Enter from activating the first result in searchable lists when no row is highlighted. + +- [#12511](https://github.com/Kilo-Org/kilocode/pull/12511) [`9e1b54d`](https://github.com/Kilo-Org/kilocode/commit/9e1b54d8754e670dd149a8536ed25b16e223fe2e) - Open plan implementation sessions immediately and submit recovered plan choices without requiring a second click. + +- [#12496](https://github.com/Kilo-Org/kilocode/pull/12496) [`2fcb137`](https://github.com/Kilo-Org/kilocode/commit/2fcb137ebcbf9101ca655804d0a61af2f222bbc5) - Preserve unexpected provider finish reasons and show the request and Gateway generation IDs when a response ends unexpectedly. + +- [#12488](https://github.com/Kilo-Org/kilocode/pull/12488) [`c25f041`](https://github.com/Kilo-Org/kilocode/commit/c25f041eb3922defc4dadb9ad7b2f8c8edb74fbd) - Show the request ID when a model response ends without a finish reason. + +- Updated dependencies [[`b367105`](https://github.com/Kilo-Org/kilocode/commit/b367105c8d648c8e05b62c2d27a28a95a4772f61), [`c1f057a`](https://github.com/Kilo-Org/kilocode/commit/c1f057ad5a2021cd57e003cad7d45e5e6b0b4cba), [`2fcb137`](https://github.com/Kilo-Org/kilocode/commit/2fcb137ebcbf9101ca655804d0a61af2f222bbc5), [`f715e2f`](https://github.com/Kilo-Org/kilocode/commit/f715e2f5fa4db5abe5c734e1c360e8da3367f3e5), [`dcc0d64`](https://github.com/Kilo-Org/kilocode/commit/dcc0d64a3249bdd3aa27d564759253126ff9a5fe)]: + - @kilocode/kilo-ui@7.5.0 + - @kilocode/sdk@7.5.0 + - @kilocode/kilo-gateway@7.4.16 + - @kilocode/plugin@7.4.16 + - @opencode-ai/ui@7.4.16 + - @opencode-ai/core@7.4.16 + - @kilocode/kilo-indexing@7.4.16 + +## 7.4.15 + +### Patch Changes + +- [#12422](https://github.com/Kilo-Org/kilocode/pull/12422) [`28d015f`](https://github.com/Kilo-Org/kilocode/commit/28d015f8fefd166348e4d4eb0b4c2ae0aa011a03) - Simplify project memory settings and activity visibility, replace direct editing with folder inspection, add nested memory slash-command completion and status views, improve empty-project handling, compact native tool-call summaries, and remove legacy memory audit logs. + +- [#12454](https://github.com/Kilo-Org/kilocode/pull/12454) [`2f389f9`](https://github.com/Kilo-Org/kilocode/commit/2f389f9fb13f88da9b74364f6c4c1ad7fd0bb09b) - Show a clear warning when reloading is blocked by a running session instead of a generic "Reload failed" error, and surface the server error message for other reload failures + +- [#11928](https://github.com/Kilo-Org/kilocode/pull/11928) [`c08302b`](https://github.com/Kilo-Org/kilocode/commit/c08302b47a4fc68e4efda8f722e87e3018601bd8) Thanks [@jhapate0704](https://github.com/jhapate0704)! - Fix scroll position not resetting when switching between diff files in the chat and virtual diff viewer + +- [#12414](https://github.com/Kilo-Org/kilocode/pull/12414) [`badf70d`](https://github.com/Kilo-Org/kilocode/commit/badf70dcedc9559769969c34aff9a63fcc9bdb5f) - Keep Linux sandbox setup working when a writable directory contains an unreadable subdirectory (for example a folder with mode 600); unreadable subdirectories are now protected with a read-only mount instead of failing every sandboxed tool call with an access error. + +- Updated dependencies [[`28d015f`](https://github.com/Kilo-Org/kilocode/commit/28d015f8fefd166348e4d4eb0b4c2ae0aa011a03)]: + - @kilocode/kilo-memory@7.4.14 + - @kilocode/kilo-ui@7.4.14 + ## 7.4.13 ### Minor Changes diff --git a/packages/kilo-vscode/knip.json b/packages/kilo-vscode/knip.json index 78b5a311058..18a96ea1108 100644 --- a/packages/kilo-vscode/knip.json +++ b/packages/kilo-vscode/knip.json @@ -8,6 +8,7 @@ "webview-ui/kiloclaw/index.tsx", "webview-ui/marketplace/index.tsx", "webview-ui/pierre-worker.ts", + "webview-ui/src/assets.d.ts", "webview-ui/src/index.tsx", "src/**/__tests__/**/*.{ts,spec.ts}", "src/**/*.test.ts", diff --git a/packages/kilo-vscode/package.json b/packages/kilo-vscode/package.json index 425654b2c4a..e67360acecc 100644 --- a/packages/kilo-vscode/package.json +++ b/packages/kilo-vscode/package.json @@ -2,7 +2,7 @@ "name": "kilo-code", "displayName": "Kilo Code: AI Coding Agent, Copilot, and Autocomplete", "description": "Open Source AI coding agent that generates code from natural language, automates tasks, and runs terminal commands. Features inline autocomplete, browser automation, automated refactoring, and custom modes for planning, coding, and debugging. Supports 500+ AI models including Claude (Anthropic), Gemini, Grok, GPT, Codex and GLM.", - "version": "7.4.13", + "version": "7.4.16", "icon": "assets/icons/logo-outline-black.png", "galleryBanner": { "color": "#FFFFFF", @@ -1129,6 +1129,11 @@ "default": true, "description": "Show the task timeline graph in the chat header" }, + "kilo-code.new.showTokenThroughput": { + "type": "boolean", + "default": false, + "description": "Show tokens-per-second (prompt-processing / text-generation) badges on assistant messages and the task header" + }, "kilo-code.new.chat.shiftTabCyclesVariant": { "type": "boolean", "default": true, @@ -1172,7 +1177,7 @@ "watch-tests": "tsc -p . -w --outDir out", "pretest": "bun run compile-tests && bun run compile && bun run lint", "check-types": "tsc --noEmit", - "check-types:webview": "bun script/typecheck.ts --project webview-ui/tsconfig.json", + "check-types:webview": "tsc --noEmit --project webview-ui/tsconfig.json", "typecheck": "bun run check-types && bun run check-types:webview", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/packages/kilo-vscode/script/local-bin.ts b/packages/kilo-vscode/script/local-bin.ts index 177a2d40778..9a2f11f550b 100644 --- a/packages/kilo-vscode/script/local-bin.ts +++ b/packages/kilo-vscode/script/local-bin.ts @@ -30,6 +30,7 @@ const forceRebuild = process.argv.includes("--force") const kiloVscodeDir = join(import.meta.dir, "..") const packagesDir = join(kiloVscodeDir, "..") +const repoDir = join(packagesDir, "..") const opencodeDir = join(packagesDir, "opencode") const coreDir = join(packagesDir, "core") const gatewayDir = join(packagesDir, "kilo-gateway") @@ -150,20 +151,20 @@ async function ensureBuiltBinary(): Promise { `No prebuilt binary found under ${relative(kiloVscodeDir, join(opencodeDir, "dist"))} - attempting build via bun.`, ) - const bunPath = Bun.which("bun") - if (!bunPath) { + if (!Bun.which("bun")) { throw new Error( `Bun is required to build the CLI binary, but was not found on PATH. ` + `Install bun, or build the CLI separately in ${opencodeDir} and re-run.`, ) } - // Ensure dependencies are installed before building. + // Use the repository-pinned Bun version throughout. Newer canaries can fail compilation + // and must not cause packaged snapshots to fall back to the browser-mode source wrapper. + const pkg = await Bun.file(join(repoDir, "package.json")).json() + const bun = String(pkg.packageManager) log("Installing dependencies in opencode package...") - await $`bun install --frozen-lockfile`.cwd(opencodeDir) - - // Build using the opencode package script. - await $`bun run build --single`.cwd(opencodeDir) + await $`bunx ${bun} install --frozen-lockfile`.cwd(opencodeDir) + await $`bunx ${bun} run build --single --skip-install`.cwd(opencodeDir) const built = await findKiloBinaryInOpencodeDist() if (!built) { @@ -205,7 +206,7 @@ async function writeSourceWrapper() { "#!/usr/bin/env bash", "set -euo pipefail", `cd ${JSON.stringify(opencodeDir)}`, - `exec ${JSON.stringify(bun)} --conditions=browser src/index.ts "$@"`, + `exec ${JSON.stringify(bun)} --conditions=node src/index.ts "$@"`, "", ].join("\n"), ) @@ -255,6 +256,7 @@ async function main() { } const sourceBinPath = await ensureBuiltBinary().catch(async (err) => { + if (forceRebuild) throw err await writeSourceWrapper() log(`Wrapper fallback reason: ${err instanceof Error ? err.message : String(err)}`) return null diff --git a/packages/kilo-vscode/script/typecheck.ts b/packages/kilo-vscode/script/typecheck.ts deleted file mode 100644 index ddab32a3e05..00000000000 --- a/packages/kilo-vscode/script/typecheck.ts +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env bun -/** - * Cross-platform typecheck script that runs tsc and filters errors. - * - * Replaces the previous bash/grep pipeline so `bun run typecheck` works - * on Windows without POSIX tools. - * - * Usage: - * bun script/typecheck.ts # check extension - * bun script/typecheck.ts --project webview-ui/tsconfig.json # check webview - * - * Filtering rules: - * - Only lines matching "error TS" are reported - * - Lines starting with ".." (parent node_modules) are excluded - * - For the webview project, "@pierre/diffs" errors are also excluded - */ - -import { $ } from "bun" - -const args = process.argv.slice(2) -const projectIdx = args.indexOf("--project") -const project = projectIdx !== -1 ? args[projectIdx + 1] : undefined -const webview = project?.includes("webview-ui") - -const tscArgs = project ? ["--noEmit", "--project", project] : ["--noEmit"] -const result = await $`tsc ${tscArgs}`.nothrow().quiet() -const output = result.stdout.toString() + result.stderr.toString() - -const errors = output - .split("\n") - .filter((line) => line.includes("error TS")) - .filter((line) => !line.startsWith("..")) - .filter((line) => !webview || !line.includes("@pierre/diffs")) - -if (errors.length > 0) { - console.error(errors.join("\n")) - process.exit(1) -} diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 03c2ebb0004..72f6f4378f3 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -63,6 +63,7 @@ import { handleSidebarWorktreeMessage } from "./kilo-provider/sidebar-worktree" import { parseMessageFiles, type MessageFile } from "./kilo-provider/message-files" import { renameSession } from "./kilo-provider/rename-session" import { handleFileSearch } from "./kilo-provider/file-search" +import { handleSessionSearch } from "./kilo-provider/session-search" import { handleFilePicker } from "./kilo-provider/file-picker" import { watchFontSizeConfig } from "./kilo-provider/font-size" import { getTerminalContents } from "./services/terminal/context" @@ -172,6 +173,7 @@ import { watchIndexingConfig, } from "./kilo-provider/indexing-settings" import { buildChatSettingsMessage, validChatSetting, watchChatConfig } from "./kilo-provider/chat-settings" +import { buildThroughputSettingMessage, watchThroughputConfig } from "./kilo-provider/throughput-settings" let maxCost = 0 @@ -289,6 +291,12 @@ export function unwrapSyncEvent(event: SSEPayload | RawSyncPayload): ProviderEve } } +type ContextRequestMessage = + | { type: "requestFileSearch"; query: string; requestId: string; sessionID?: string } + | { type: "requestSessionSearch"; requestId: string; sessionID?: string } + | { type: "requestFilePicker"; requestId: string } + | { type: "requestTerminalContext"; requestId: string; sessionID?: string } + export class KiloProvider implements vscode.WebviewViewProvider, TelemetryPropertiesProvider { public static readonly viewType = "kilo-code.SidebarProvider" private readonly instanceId = crypto.randomUUID() @@ -393,6 +401,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper private autocompleteConfigDisposable: vscode.Disposable | null = null private indexingConfigDisposable: vscode.Disposable | null = null private chatConfigDisposable: vscode.Disposable | null = null + private throughputConfigDisposable: vscode.Disposable | null = null private telemetryStateDisposable: vscode.Disposable | null = null private viewStateDisposable: vscode.Disposable | null = null private visibilityDisposable: vscode.Disposable | null = null @@ -759,7 +768,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } /** Register a session created externally and notify the webview. */ - public registerSession(session: Session): void { + public registerSession(session: Session, activate = false): void { this.stopCurrentSessionProcesses(session.id) this.setCurrentSession(session) this.contextSessionID = session.id @@ -767,6 +776,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage({ type: "sessionCreated", session: this.sessionToWebview(session), + ...(activate ? { activate: true } : {}), }) } @@ -917,6 +927,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.indexingConfigDisposable = watchIndexingConfig((msg) => this.postMessage(msg)) this.chatConfigDisposable?.dispose() this.chatConfigDisposable = watchChatConfig((msg) => this.postMessage(msg)) + this.throughputConfigDisposable?.dispose() + this.throughputConfigDisposable = watchThroughputConfig((msg) => this.postMessage(msg)) this.telemetryStateDisposable?.dispose() this.telemetryStateDisposable = watchTelemetryState((msg) => this.postMessage(msg)) this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => { @@ -1031,6 +1043,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "unrevertSession": this.checkpoint(message.sessionID, () => this.handleUnrevertSession(message.sessionID)) break + case "deleteMessage": + await this.handleDeleteMessage(message.sessionID, message.messageID) + break case "permissionResponse": await handlePermissionResponse( this.permissionCtx, @@ -1298,21 +1313,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper break } case "requestFileSearch": - await handleFileSearch({ - client: this.client, - message, - current: this.currentSession?.id, - context: this.contextSessionID, - dir: (id) => this.getWorkspaceDirectory(id), - open: (dir) => this.getOpenTabPaths(dir), - post: (msg) => this.postMessage(msg), - }) - break + case "requestSessionSearch": case "requestFilePicker": - await handleFilePicker({ requestId: message.requestId, post: (msg) => this.postMessage(msg) }) - break case "requestTerminalContext": - void this.handleTerminalContext(message.requestId) + await this.handleContextRequest(message) break case "chatCompletionAccepted": this.chatAutocomplete?.telemetry.captureAcceptSuggestion(message.suggestionLength) @@ -1351,6 +1355,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper case "requestTimelineSetting": this.sendTimelineSetting() break + case "requestThroughputSetting": + this.postMessage(buildThroughputSettingMessage()) + break case "requestNotifications": this.fetchAndSendNotifications().catch((e) => console.error("[Kilo New] fetchAndSendNotifications failed:", e), @@ -1734,6 +1741,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage({ type: "gitStatus", repo: this.cachedGitRepo }) this.sendNotificationSettings() this.sendTimelineSetting() + this.postMessage(buildThroughputSettingMessage()) this.postMessage({ type: "extensionDataReady" }) if (this.cachedGitRepo) this.startStatsPolling() @@ -2041,6 +2049,40 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.pendingSessionRefresh = ctx.pendingSessionRefresh } + private async handleContextRequest(message: ContextRequestMessage): Promise { + if (message.type === "requestFileSearch") { + await handleFileSearch({ + client: this.client, + message, + current: this.currentSession?.id, + context: this.contextSessionID, + dir: (id) => this.getWorkspaceDirectory(id), + open: (dir) => this.getOpenTabPaths(dir), + post: (msg) => this.postMessage(msg), + }) + return + } + if (message.type === "requestSessionSearch") { + await handleSessionSearch({ + client: this.client, + message, + current: this.currentSession?.id, + context: this.contextSessionID, + dir: (id) => this.getWorkspaceDirectory(id), + exclude: this.currentSession?.id, + post: (msg) => this.postMessage(msg), + }) + return + } + if (message.type === "requestFilePicker") { + await handleFilePicker({ requestId: message.requestId, post: (msg) => this.postMessage(msg) }) + return + } + if (message.type === "requestTerminalContext") { + void this.handleTerminalContext(message.requestId) + } + } + private async handleTerminalContext(requestId: string): Promise { try { const output = await getTerminalContents(-1) @@ -2133,6 +2175,27 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } + private async handleDeleteMessage(sessionID: string, messageID: string): Promise { + if (!this.client) { + this.postMessage({ type: "error", message: "Not connected to CLI backend", sessionID }) + return + } + + try { + await this.client.session.deleteMessage( + { sessionID, messageID, directory: this.getWorkspaceDirectory(sessionID) }, + { throwOnError: true }, + ) + } catch (error) { + console.error("[Kilo New] KiloProvider: Failed to delete message:", error) + this.postMessage({ + type: "error", + message: getErrorMessage(error) || "Failed to delete message", + sessionID, + }) + } + } + /** * Handle renaming a session. */ @@ -3724,6 +3787,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.sendBrowserSettings() this.sendNotificationSettings() this.sendTimelineSetting() + this.postMessage(buildThroughputSettingMessage()) this.sendWorkStyle() await ModelState.reset(this.client, (msg) => this.postMessage(msg)) @@ -3787,18 +3851,19 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper try { await this.client.instance.reload({ directory: dir }, { throwOnError: true }) } catch (err) { + // wrapClientError exposes the HTTP status via `cause`, not `response`. + const cause = err instanceof Error ? err.cause : undefined const status = - err && typeof err === "object" && "response" in err - ? (err as { response?: { status?: number } }).response?.status - : undefined + cause && typeof cause === "object" && "status" in cause ? (cause as { status?: number }).status : undefined if (status === 409) { vscode.window.showWarningMessage( "Cannot reload while a session is running. Wait for it to finish or abort it first.", ) - } else { - console.error("[Kilo New] handleReload: reload endpoint failed:", err) - vscode.window.showErrorMessage("Reload failed. See extension logs for details.") + return } + console.error("[Kilo New] handleReload: reload endpoint failed:", err) + const detail = err instanceof Error && err.message ? err.message : "See extension logs for details." + vscode.window.showErrorMessage(`Reload failed. ${detail}`) return } this.clearCommandsCache() @@ -3962,7 +4027,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper detail, }) } - void this.memory.fetch(sessionID, false) + void this.memory.fetch(sessionID) } return } @@ -4415,7 +4480,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.pendingFollowup = null this.trackDirectory(session.id, session.directory) for (const cb of this.followupListeners) cb(session, session.directory) - this.registerSession(session) + this.registerSession(session, true) void this.handleLoadMessages(session.id) return true } @@ -4517,6 +4582,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.autocompleteConfigDisposable?.dispose() this.indexingConfigDisposable?.dispose() this.chatConfigDisposable?.dispose() + this.throughputConfigDisposable?.dispose() this.telemetryStateDisposable?.dispose() this.autoApproveBridge?.dispose() this.visibleTaskStreams.clear() diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/question.ts b/packages/kilo-vscode/src/kilo-provider/handlers/question.ts index 477db768032..1704f77dbfe 100644 --- a/packages/kilo-vscode/src/kilo-provider/handlers/question.ts +++ b/packages/kilo-vscode/src/kilo-provider/handlers/question.ts @@ -28,6 +28,8 @@ interface QuestionRecovery { readonly complete: boolean } +type QuestionRoute = { kind: "retry"; dir: string } | { kind: "stale" } | { kind: "failed" } + function isNotFoundError(error: unknown): boolean { const record = (value: unknown) => value && typeof value === "object" ? (value as Record) : undefined @@ -41,18 +43,18 @@ function isNotFoundError(error: unknown): boolean { ) } -function stale(ctx: QuestionContext, requestID: string): void { +async function recover(ctx: QuestionContext, requestID: string): Promise { + const result = await fetchAndSendPendingQuestions(ctx, requestID) + if (!result) return { kind: "failed" } + if (result.seen.has(requestID)) { + const dir = ctx.getQuestionDirectory(requestID) + return dir ? { kind: "retry", dir } : { kind: "failed" } + } + // Absence only proves staleness when every directory was scanned. + if (!result.complete) return { kind: "failed" } ctx.clearQuestionDirectory(requestID) ctx.postMessage({ type: "questionResolved", requestID }) - void fetchAndSendPendingQuestions(ctx) -} - -async function recover(ctx: QuestionContext, requestID: string): Promise { - const result = await fetchAndSendPendingQuestions(ctx) - if (!result?.complete || result.seen.has(requestID)) return false - ctx.clearQuestionDirectory(requestID) - ctx.postMessage({ type: "questionResolved", requestID }) - return true + return { kind: "stale" } } /** @@ -61,7 +63,10 @@ async function recover(ctx: QuestionContext, requestID: string): Promise { +export async function fetchAndSendPendingQuestions( + ctx: QuestionContext, + omit?: string, +): Promise { if (!ctx.client) return try { for (;;) { @@ -94,6 +99,9 @@ export async function fetchAndSendPendingQuestions(ctx: QuestionContext): Promis if (ctx.getQuestionRevision() !== revision) continue for (const item of pending) { ctx.recordQuestionDirectory(item.question.id, item.dir) + // The omitted request is mid-reply; its card is still visible, so + // reposting it would only churn the webview. + if (item.question.id === omit) continue ctx.postMessage({ type: "questionRequest", question: { @@ -126,19 +134,26 @@ export async function handleQuestionReply( } const sid = sessionID ?? ctx.currentSessionId - const origin = ctx.getQuestionDirectory(requestID) - const dir = origin ?? ctx.getWorkspaceDirectory(sid) + const dir = ctx.getQuestionDirectory(requestID) ?? ctx.getWorkspaceDirectory(sid) try { await ctx.client.question.reply({ requestID, answers, directory: dir }, { throwOnError: true }) ctx.clearQuestionDirectory(requestID) return true } catch (error) { - if (isNotFoundError(error) && origin) { - stale(ctx, requestID) - return false + const route = isNotFoundError(error) ? await recover(ctx, requestID) : undefined + if (route?.kind === "stale") return false + if (route?.kind === "retry" && route.dir !== dir) { + try { + await ctx.client.question.reply({ requestID, answers, directory: route.dir }, { throwOnError: true }) + ctx.clearQuestionDirectory(requestID) + return true + } catch (retry) { + console.error("[Kilo New] KiloProvider: Failed to reply to recovered question:", retry) + ctx.postMessage({ type: "questionError", requestID }) + return false + } } - if (isNotFoundError(error) && (await recover(ctx, requestID))) return false console.error("[Kilo New] KiloProvider: Failed to reply to question:", error) ctx.postMessage({ type: "questionError", requestID }) return false @@ -157,19 +172,26 @@ export async function handleQuestionReject( } const sid = sessionID ?? ctx.currentSessionId - const origin = ctx.getQuestionDirectory(requestID) - const dir = origin ?? ctx.getWorkspaceDirectory(sid) + const dir = ctx.getQuestionDirectory(requestID) ?? ctx.getWorkspaceDirectory(sid) try { await ctx.client.question.reject({ requestID, directory: dir }, { throwOnError: true }) ctx.clearQuestionDirectory(requestID) return true } catch (error) { - if (isNotFoundError(error) && origin) { - stale(ctx, requestID) - return false + const route = isNotFoundError(error) ? await recover(ctx, requestID) : undefined + if (route?.kind === "stale") return false + if (route?.kind === "retry" && route.dir !== dir) { + try { + await ctx.client.question.reject({ requestID, directory: route.dir }, { throwOnError: true }) + ctx.clearQuestionDirectory(requestID) + return true + } catch (retry) { + console.error("[Kilo New] KiloProvider: Failed to reject recovered question:", retry) + ctx.postMessage({ type: "questionError", requestID }) + return false + } } - if (isNotFoundError(error) && (await recover(ctx, requestID))) return false console.error("[Kilo New] KiloProvider: Failed to reject question:", error) ctx.postMessage({ type: "questionError", requestID }) return false diff --git a/packages/kilo-vscode/src/kilo-provider/memory.ts b/packages/kilo-vscode/src/kilo-provider/memory.ts index 136d977da5c..c2340222247 100644 --- a/packages/kilo-vscode/src/kilo-provider/memory.ts +++ b/packages/kilo-vscode/src/kilo-provider/memory.ts @@ -1,11 +1,5 @@ import * as vscode from "vscode" -import * as path from "node:path" -import { - isMemoryOperation, - isMemoryPromptOperation, - type MemoryOperation, - type MemoryPromptOperation, -} from "@kilocode/kilo-memory/commands" +import { isMemoryOperation, type MemoryOperation } from "@kilocode/kilo-memory/commands" import { MemorySchema } from "@kilocode/kilo-memory/schema" import type { KiloClient, Session } from "@kilocode/sdk/v2/client" import { retry } from "../services/cli-backend/retry" @@ -14,6 +8,7 @@ import { getErrorMessage } from "../kilo-provider-utils" type MemorySourceFile = MemorySchema.Source type MemoryApi = KiloClient["memory"] const CACHE_LIMIT = 8 +const STORED_LIMIT = 16 const NO_PROJECT = "No active project for memory. Open a file in the target folder to manage its memory." export type KiloProviderMemoryMessage = { @@ -53,6 +48,20 @@ function memory(client: KiloClient | undefined): MemoryApi | undefined { return (client as { memory?: MemoryApi } | undefined)?.memory } +function count(text: string) { + return text.split("\n").filter((line) => line.trim().startsWith("- ")).length +} + +function stored(text: string) { + return text + .split("\n") + .filter((line) => line.trim()) + .map((line) => { + const marker = line.indexOf(":: ") + return marker === -1 ? line : line.slice(marker + 3) + }) +} + function request(input: Record): { value: KiloProviderMemoryMessage } | { error: string } { const op = operation(input.operation) if (!op) return { error: "Unknown memory operation" } @@ -102,14 +111,16 @@ export class KiloProviderMemory { async handle(message: Record): Promise { if (message.type === "requestMemory") { - this.fetch( - typeof message.sessionID === "string" ? message.sessionID : undefined, - message.includeSources === true, - ).catch((err: unknown) => console.error("[Kilo New] fetchAndSendMemory failed:", err)) + this.fetch(typeof message.sessionID === "string" ? message.sessionID : undefined).catch((err: unknown) => + console.error("[Kilo New] fetchAndSendMemory failed:", err), + ) return true } if (message.type === "memoryShow") { - await this.show(typeof message.sessionID === "string" ? message.sessionID : undefined) + await this.show( + typeof message.sessionID === "string" ? message.sessionID : undefined, + message.mode === "status" ? "status" : "show", + ) return true } if (message.type === "memoryOperation") { @@ -127,17 +138,11 @@ export class KiloProviderMemory { await this.run(parsed.value) return true } - if (message.type === "memoryPrompt") { - const op = isMemoryPromptOperation(message.operation) ? message.operation : undefined - if (!op) return true - await this.prompt(op, typeof message.sessionID === "string" ? message.sessionID : undefined) - return true - } return false } - fetch(sessionID?: string, includeSources = false): Promise { - return this.serial(() => this.load(sessionID, includeSources)) + fetch(sessionID?: string): Promise { + return this.serial(() => this.load(sessionID)) } /** Resolves once the serialized operation queue has drained. */ @@ -145,7 +150,7 @@ export class KiloProviderMemory { return this.tail } - private async load(sessionID?: string, includeSources = false): Promise { + private async load(sessionID?: string): Promise { try { const directory = this.input.dir(sessionID ?? this.input.session()?.id) const client = this.input.client() @@ -168,14 +173,10 @@ export class KiloProviderMemory { } const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true })) - const show = includeSources - ? (await retry(() => api.show({ directory }, { throwOnError: true }))).data - : undefined const msg = { type: "memoryLoaded", sessionID, status, - ...(show ? { show } : {}), } this.cache(directory, msg) this.input.post(msg) @@ -189,27 +190,11 @@ export class KiloProviderMemory { } } - async prompt(value: MemoryPromptOperation, sessionID?: string): Promise { - const title = value === "remember" ? "Remember in project memory" : "Forget project memory" - const placeHolder = value === "remember" ? "Project fact, command, or correction" : "Text to remove" - const text = await vscode.window.showInputBox({ title, placeHolder, ignoreFocusOut: true }) - if (!text?.trim()) { - // Clear the webview's pending state for this action when the input is dismissed. - this.input.post({ type: "memoryOperationResult", operation: value, sessionID, ok: true }) - return - } - await this.run({ - operation: value, - sessionID, - ...(value === "remember" ? { text: text.trim() } : { query: text.trim() }), - }) + show(sessionID?: string, mode: "status" | "show" = "show"): Promise { + return this.serial(() => this.doShow(sessionID, mode)) } - show(sessionID?: string): Promise { - return this.serial(() => this.doShow(sessionID)) - } - - private async doShow(sessionID?: string): Promise { + private async doShow(sessionID: string | undefined, mode: "status" | "show"): Promise { const client = this.input.client() if (!client) { this.input.post({ @@ -237,55 +222,56 @@ export class KiloProviderMemory { this.input.post({ type: "memoryLoaded", sessionID, error: NO_PROJECT }) return } - const { data: show } = await retry(() => api.show({ directory }, { throwOnError: true })) - const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true })) - const current = sessionID ?? this.input.session()?.id - const startup = - current && status.state.stats.lastInjectedSessionID === current ? status.state.stats.lastInjectedTokens : 0 - const content = [ - "# Kilo Memory", - "", - `Root: ${show.root}`, - `Enabled: ${show.state.enabled ? "yes" : "no"}`, - `Auto-save: ${show.state.autoConsolidate ? "on" : "off"}`, - `Startup context: ${show.state.autoInject ? "on" : "off"}`, - `Stored index tokens: ${status.index.estimatedTokens}`, - `Startup context tokens for this session: ${startup}`, - `Last auto-save model usage: ${status.state.stats.lastConsolidationTokens} tokens`, - "", - "## project.md", - show.sources.project.trim(), - "", - "## environment.md", - show.sources.environment.trim(), - "", - "## corrections.md", - show.sources.corrections.trim(), - "", - "## index.kmem", - show.index.trim(), - "", - "## items", - show.items.trim(), - "", - "## changes", - show.changes.trim(), - "", - "## decisions.jsonl", - show.decisions.trim(), - "", - ].join("\n") - await vscode.workspace - .openTextDocument({ content, language: "markdown" }) - .then((doc) => vscode.window.showTextDocument(doc, { preview: true })) + const [{ data: show }, { data: status }] = await Promise.all([ + retry(() => api.show({ directory }, { throwOnError: true })), + retry(() => api.status({ directory }, { throwOnError: true })), + ]) const msg = { type: "memoryLoaded", sessionID, status, - show, } this.cache(directory, msg) this.input.post(msg) + const items = stored(show.items) + if (mode === "show" && items.length === 0) { + void vscode.window.showInformationMessage( + "This project doesn't have any memory yet. It will start showing after you use Kilo.", + ) + return + } + const entries: vscode.QuickPickItem[] = [ + { + label: `${status.state.enabled ? "Enabled" : "Disabled"} · ${status.state.scope}`, + description: status.state.autoConsolidate ? "Auto-save on" : "Auto-save off", + }, + { label: "Storage", detail: status.root }, + { + label: "Sources", + description: `project.md ${count(show.sources.project)} · environment.md ${count(show.sources.environment)} · corrections.md ${count(show.sources.corrections)}`, + }, + { + label: "Index", + description: `${status.index.estimatedTokens.toLocaleString()} estimated tokens`, + }, + ] + if (mode === "show") { + const shown = items.slice(0, STORED_LIMIT) + entries.push( + { + label: "Stored memory", + description: + shown.length < items.length ? `${shown.length} of ${items.length} shown` : `${shown.length} shown`, + }, + ...shown.map((label) => ({ label })), + ) + } + void vscode.window.showQuickPick(entries, { + title: mode === "show" ? "Memory" : "Memory status", + placeHolder: mode === "show" ? "Stored project memory" : "Project memory status", + matchOnDescription: true, + matchOnDetail: true, + }) } catch (err) { console.error("[Kilo New] KiloProvider: Failed to show memory:", err) this.input.post({ @@ -358,31 +344,28 @@ export class KiloProviderMemory { return false } const data = await this.action(api, directory, message) - const refreshed = await Promise.all([ - retry(() => api.status({ directory }, { throwOnError: true })), - retry(() => api.show({ directory }, { throwOnError: true })), - ]).catch((err: unknown) => { - console.warn("[Kilo New] Memory changed but refresh failed:", err) - return undefined - }) - const status = refreshed?.[0].data - const show = refreshed?.[1].data + const refreshed = + message.operation === "status" + ? { data } + : await retry(() => api.status({ directory }, { throwOnError: true })).catch((err: unknown) => { + console.warn("[Kilo New] Memory changed but refresh failed:", err) + return undefined + }) + const status = refreshed?.data const result = { type: "memoryOperationResult", operation: message.operation, sessionID: message.sessionID, ok: true, ...(status ? { status } : {}), - ...(show ? { show } : {}), result: data, } this.input.post(result) - if (status && show) { + if (status) { const loaded = { type: "memoryLoaded", sessionID: message.sessionID, status, - show, } this.cache(directory, loaded) this.input.post(loaded) @@ -409,12 +392,11 @@ export class KiloProviderMemory { const op = message.operation if (op === "enable") return (await api.enable({ directory }, { throwOnError: true })).data if (op === "status") return (await api.status({ directory }, { throwOnError: true })).data - if (op === "edit") return this.edit(api, directory) + if (op === "inspect") return this.inspect(api, directory) if (op === "disable") return (await api.disable({ directory }, { throwOnError: true })).data if (op === "rebuild") return (await api.rebuild({ directory }, { throwOnError: true })).data if (op === "purge") return this.purge(api, directory, message) if (op === "auto") return this.auto(api, directory, message) - if (op === "verbose") return this.verbose(api, directory, message) if (op === "remember") return this.remember(api, directory, message) if (op === "correct") return this.correct(api, directory, message) return this.forget(api, directory, message) @@ -460,12 +442,10 @@ export class KiloProviderMemory { return (await api.forget({ directory, query, sessionID: message.sessionID }, { throwOnError: true })).data } - private async edit(api: MemoryApi, directory: string) { + private async inspect(api: MemoryApi, directory: string) { const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true })) if (!status.state.enabled) throw new Error("Memory is disabled. Run /memory on first.") - const uri = vscode.Uri.file(path.join(status.root, "project.md")) - const doc = await vscode.workspace.openTextDocument(uri) - await vscode.window.showTextDocument(doc, { preview: false }) + await vscode.commands.executeCommand("revealFileInOS", vscode.Uri.file(status.root)) return status } @@ -481,11 +461,4 @@ export class KiloProviderMemory { } throw new Error("Auto-save mode is required") } - - private async verbose(api: MemoryApi, directory: string, message: KiloProviderMemoryMessage) { - if (message.mode === "on" || message.mode === "off") { - return (await api.configure({ directory, verbose: message.mode === "on" }, { throwOnError: true })).data - } - throw new Error("Verbose mode is required") - } } diff --git a/packages/kilo-vscode/src/kilo-provider/message-files.ts b/packages/kilo-vscode/src/kilo-provider/message-files.ts index 108fd3ef809..e6c69955928 100644 --- a/packages/kilo-vscode/src/kilo-provider/message-files.ts +++ b/packages/kilo-vscode/src/kilo-provider/message-files.ts @@ -12,7 +12,8 @@ const source = z.object({ const file = z.object({ mime: z.string(), - url: z.string().refine((url) => url.startsWith("file://") || url.startsWith("data:")), + // session: URLs reference a past chat; the backend resolves them into transcript context + url: z.string().refine((url) => url.startsWith("file://") || url.startsWith("data:") || url.startsWith("session:")), filename: z.string().optional(), source: source.optional(), }) diff --git a/packages/kilo-vscode/src/kilo-provider/session-search.ts b/packages/kilo-vscode/src/kilo-provider/session-search.ts new file mode 100644 index 00000000000..7552fc68020 --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/session-search.ts @@ -0,0 +1,51 @@ +import type { KiloClient } from "@kilocode/sdk/v2/client" + +type Item = { + id: string + title: string + updated: number +} + +type Message = { + requestId: string + sessionID?: string +} + +type Input = { + client: KiloClient | null + message: Message + current?: string + context?: string + dir: (id?: string) => string + exclude?: string + post: (message: unknown) => void +} + +/** + * Past-chat mention search. Lists root sessions for the directory the current + * chat runs in (workspace root for the sidebar, the worktree for Agent Manager + * sessions) — the same directory-scoped `session.list` the session history and + * Agent Manager search are built on. Fuzzy title filtering happens in the + * webview (same mechanism as the Agent Manager sidebar search). + */ +export async function handleSessionSearch(input: Input): Promise { + const client = input.client + if (!client) { + input.post({ type: "sessionSearchResult", sessions: [], requestId: input.message.requestId }) + return + } + + const id = input.message.sessionID ?? input.current ?? input.context + const dir = input.dir(id) + + try { + const res = await client.session.list({ directory: dir, roots: true, limit: 50 }, { throwOnError: true }) + const sessions: Item[] = res.data + .filter((session) => session.id !== input.exclude && session.title) + .map((session) => ({ id: session.id, title: session.title, updated: session.time.updated })) + input.post({ type: "sessionSearchResult", sessions, requestId: input.message.requestId }) + } catch (err) { + console.error("[Kilo New] Session search failed:", err) + input.post({ type: "sessionSearchResult", sessions: [], requestId: input.message.requestId }) + } +} diff --git a/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts b/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts new file mode 100644 index 00000000000..ddac492302d --- /dev/null +++ b/packages/kilo-vscode/src/kilo-provider/throughput-settings.ts @@ -0,0 +1,19 @@ +import * as vscode from "vscode" + +type Post = (msg: unknown) => void + +export function buildThroughputSettingMessage() { + const config = vscode.workspace.getConfiguration("kilo-code.new") + return { + type: "throughputSettingLoaded" as const, + visible: config.get("showTokenThroughput", false), + } +} + +export function watchThroughputConfig(post: Post): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("kilo-code.new.showTokenThroughput")) { + post(buildThroughputSettingMessage()) + } + }) +} diff --git a/packages/kilo-vscode/tests/accessibility.spec.ts b/packages/kilo-vscode/tests/accessibility.spec.ts index 153eeb1c34e..151c71efb06 100644 --- a/packages/kilo-vscode/tests/accessibility.spec.ts +++ b/packages/kilo-vscode/tests/accessibility.spec.ts @@ -13,6 +13,7 @@ const STORIES = [ { id: "settings--providers-configure", name: "Settings / providers empty state" }, { id: "marketplace--empty-list", name: "Marketplace / empty state" }, { id: "agentmanager--sidebar-search-open", name: "Agent Manager / sidebar search" }, + { id: "session-tabs--switcher-open", name: "Session tabs / switcher" }, ] function url(id: string) { @@ -178,4 +179,53 @@ test.describe("webview accessibility ratchet", () => { ).toBeVisible() await expect(page.getByText("⌘F", { exact: true })).toBeVisible() }) + + test("Search lists do not select an unhighlighted result on Enter by default", async ({ page }) => { + await open(page, "agentmanager--sidebar-search-open") + + const input = page.getByPlaceholder("Search worktrees and sessions", { exact: true }) + const row = page.locator('[data-slot="list-item"]').first() + const selected = page.locator('[data-slot="sidebar-search-selection"]') + + await input.fill("Render") + await expect(row).toContainText("Render images in diff viewer") + await row.dispatchEvent("mousemove", { movementX: 1 }) + await expect(row).toHaveAttribute("data-active", "true") + await row.dispatchEvent("mouseleave") + await expect(page.locator('[data-slot="list-item"][data-active="true"]')).toHaveCount(0) + + await input.press("Enter") + await expect(selected).toHaveText("worktree:wt-search") + await expect(input).toBeFocused() + }) + + test("Session tab switcher restores chat focus after keyboard and mouse selection", async ({ page }) => { + await open(page, "session-tabs--switcher-open") + + const input = page.getByPlaceholder("Search open tabs") + const prompt = page.getByRole("textbox", { name: "Chat input" }) + await expect(page.locator('[data-slot="list-item"][data-active="true"]')).toHaveCount(0) + await expect(page.locator('[data-slot="list-item"][data-key="current"]')).toHaveAttribute("data-selected", "true") + await expect(page.locator('[data-slot="list-item"][data-key="refactor"]')).toHaveAttribute("data-selected", "false") + await input.press("ArrowDown") + await input.press("Enter") + await expect(prompt).toBeFocused() + + await page.getByRole("button", { name: "Show open tabs" }).click() + await page.locator('[data-slot="list-item"][data-key="current"]').click() + await expect(prompt).toBeFocused() + + // Enter without prior ArrowDown selects the first filtered result (noInitialSelection) + await page.getByRole("button", { name: "Show open tabs" }).click() + await input.fill("Review") + await input.press("Enter") + await expect(prompt).toBeFocused() + }) + + test("Search popovers expose accessible dialog names", async ({ page }) => { + for (const id of ["agentmanager--sidebar-search-open", "session-tabs--switcher-open"]) { + await open(page, id) + await expect(page.getByRole("dialog")).toHaveAccessibleName(/.+/) + } + }) }) diff --git a/packages/kilo-vscode/tests/fixtures/session-tab-switcher.tsx b/packages/kilo-vscode/tests/fixtures/session-tab-switcher.tsx new file mode 100644 index 00000000000..b1260dc8a27 --- /dev/null +++ b/packages/kilo-vscode/tests/fixtures/session-tab-switcher.tsx @@ -0,0 +1,194 @@ +import assert from "node:assert/strict" +import { Window } from "happy-dom" + +const window = new Window({ url: "http://localhost" }) +const style = window.getComputedStyle.bind(window) +Object.assign(globalThis, { + window, + document: window.document, + navigator: window.navigator, + Node: window.Node, + Element: window.Element, + HTMLElement: window.HTMLElement, + HTMLInputElement: window.HTMLInputElement, + HTMLTextAreaElement: window.HTMLTextAreaElement, + SVGElement: window.SVGElement, + MutationObserver: window.MutationObserver, + ResizeObserver: window.ResizeObserver, + CustomEvent: window.CustomEvent, + Event: window.Event, + FocusEvent: window.FocusEvent, + InputEvent: window.InputEvent, + KeyboardEvent: window.KeyboardEvent, + MouseEvent: window.MouseEvent, + PointerEvent: window.PointerEvent, + getComputedStyle: (node: Element) => { + const value = style(node) + Object.defineProperty(value, "animationName", { configurable: true, value: "none" }) + return value + }, + requestAnimationFrame: window.requestAnimationFrame.bind(window), + cancelAnimationFrame: window.cancelAnimationFrame.bind(window), +}) + +const { Show, createSignal } = await import("solid-js") +const { render } = await import("solid-js/web") +const { SessionTabSwitcher } = await import("../../webview-ui/src/components/chat/SessionTabSwitcher") + +const rows = [ + { id: "alpha", title: "Alpha", active: true, busy: false, pending: false }, + { id: "beta", title: "Beta", active: false, busy: true, pending: false }, + { id: "gamma", title: "Gamma", active: false, busy: false, pending: false }, +] +const [items, setItems] = createSignal(rows) +const selected: string[] = [] +const restored: boolean[] = [] +const closed: string[] = [] +const target = document.createElement("textarea") +const root = document.createElement("div") +document.body.append(root, target) + +const dispose = render( + () => ( + 1}> + selected.push(id)} + onRestore={() => { + restored.push(true) + target.focus() + }} + onClose={(id) => { + closed.push(id) + setItems((value) => value.filter((item) => item.id !== id)) + }} + portal={false} + /> + + ), + root, +) + +function query(selector: string, message: string) { + const node = root.querySelector(selector) + assert(node, message) + return node +} + +const settle = async () => { + await Promise.resolve() + await window.happyDOM.waitUntilComplete() +} + +const open = async () => { + query('[aria-label="Show open tabs"]', "Switcher trigger did not render").click() + await settle() + assert.equal(root.querySelector('[data-slot="list-item"][data-active="true"]'), null, "First tab was highlighted") + assert.equal( + query('[data-slot="list-item"][data-key="alpha"]', "Current tab did not render").getAttribute("data-selected"), + "true", + "Current tab was not selected", + ) +} + +async function closeFiltered() { + await open() + + const input = query('[data-slot="list-search"] input', "Switcher search did not render") + input.value = "be" + input.dispatchEvent(new InputEvent("input", { bubbles: true, data: "be", inputType: "insertText" })) + await settle() + + const close = query( + '[aria-label="Close tab: Beta"]', + "Filtered result close button did not render", + ) + assert.equal(close.tabIndex, 0, "Close button is not keyboard reachable") + close.click() + await settle() + + assert.deepEqual(closed, ["beta"], "Unexpected closed tabs") + assert.equal(input.value, "be", "Closing a result cleared the filter") + assert.equal(document.activeElement, input, "Search input was not refocused after closing a result") +} + +async function selectFiltered() { + setItems(rows) + await settle() + + query('[data-slot="list-item"][data-key="beta"]', "Filtered result did not return").click() + await settle() + + assert.deepEqual(selected, ["beta"], "Unexpected selected tabs") + assert.deepEqual(restored, [true], "Prompt focus was not restored") + assert.equal(document.activeElement, target, "Popover close stole focus from the prompt") +} + +async function enterSelectsFirst() { + setItems(rows) + selected.length = 0 + restored.length = 0 + await settle() + + await open() + + const input = query('[data-slot="list-search"] input', "Switcher search did not render") + input.value = "ga" + input.dispatchEvent(new InputEvent("input", { bubbles: true, data: "ga", inputType: "insertText" })) + await settle() + + input.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" })) + await settle() + + assert.deepEqual(selected, ["gamma"], "Enter did not select the first filtered result") + assert.deepEqual(restored, [true], "Prompt focus was not restored after Enter") + assert.equal(document.activeElement, target, "Popover close stole focus from the prompt") +} + +async function deleteReopened() { + await open() + + const alpha = query( + '[data-slot="list-item"][data-key="alpha"]', + "Switcher did not reset its filter when reopened", + ) + alpha.focus() + alpha.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Delete" })) + await settle() + + assert.deepEqual(closed, ["beta", "alpha"], "Keyboard close failed") +} + +async function closeToOne() { + closed.length = 0 + restored.length = 0 + + const beta = query( + '[data-slot="list-item"][data-key="beta"]', + "Switcher did not retain the remaining tabs", + ) + beta.focus() + beta.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Delete" })) + await settle() + + assert.deepEqual(closed, ["beta"], "Final visible close failed") + assert.deepEqual(restored, [true], "Prompt did not receive the focus handoff") + assert.equal(root.querySelector('[aria-label="Show open tabs"]'), null, "Switcher did not unmount") + assert.equal(document.activeElement, target, "Prompt was not focused after the switcher unmounted") +} + +await closeFiltered() +await selectFiltered() +await enterSelectsFirst() +await deleteReopened() +await closeToOne() + +dispose() diff --git a/packages/kilo-vscode/tests/history-accessibility.spec.ts b/packages/kilo-vscode/tests/history-accessibility.spec.ts index 554dd3ee00d..230bc7e47f9 100644 --- a/packages/kilo-vscode/tests/history-accessibility.spec.ts +++ b/packages/kilo-vscode/tests/history-accessibility.spec.ts @@ -99,4 +99,29 @@ test.describe("history session accessibility", () => { await expect(local).toHaveAttribute("aria-selected", "true") await expect(page.getByRole("tabpanel", { name: "Local" })).toBeVisible() }) + + test("filters sessions to the current worktree and includes it in keyboard navigation", async ({ page }) => { + await story(page, "history-sessionlist--worktree-sources") + + const local = page.getByRole("tab", { name: "Local" }) + const worktree = page.getByRole("tab", { name: "Worktree" }) + await local.focus() + await page.keyboard.press("End") + await expect(worktree).toBeFocused() + await page.keyboard.press("Enter") + + await expect(worktree).toHaveAttribute("aria-selected", "true") + await expect(page.getByRole("tabpanel", { name: "Worktree" })).toBeVisible() + const rows = page.locator('[data-slot="list-item"]') + await expect(rows.filter({ hasText: "Refactor authentication module" })).toBeVisible() + await expect(rows.filter({ hasText: "Fix TypeScript errors in webview" })).toBeVisible() + await expect(rows.filter({ hasText: "Add screenshot test coverage" })).toHaveCount(0) + + await rows.filter({ hasText: "Refactor authentication module" }).click() + await expect(page.locator('[data-slot="selected-session"]')).toHaveText("s1") + + await worktree.focus() + await page.keyboard.press("ArrowRight") + await expect(local).toBeFocused() + }) }) diff --git a/packages/kilo-vscode/tests/package.json b/packages/kilo-vscode/tests/package.json index 755953d04d7..38105c8a8f6 100644 --- a/packages/kilo-vscode/tests/package.json +++ b/packages/kilo-vscode/tests/package.json @@ -1,6 +1,6 @@ { "type": "module", - "version": "7.4.13", + "version": "7.4.16", "dependencies": {}, "devDependencies": {}, "peerDependencies": {} diff --git a/packages/kilo-vscode/tests/setup/vscode-mock.ts b/packages/kilo-vscode/tests/setup/vscode-mock.ts index a5914f7bffa..9d01986cc87 100644 --- a/packages/kilo-vscode/tests/setup/vscode-mock.ts +++ b/packages/kilo-vscode/tests/setup/vscode-mock.ts @@ -88,6 +88,7 @@ const mockVscode = { tabGroups: { all: [] }, showTextDocument: async () => {}, showInformationMessage: async () => undefined, + showQuickPick: async () => undefined, showErrorMessage: async () => undefined, showWarningMessage: async () => undefined, createTerminal: () => ({ show: noop, sendText: noop, dispose: noop }), diff --git a/packages/kilo-vscode/tests/unit/file-mention-utils.test.ts b/packages/kilo-vscode/tests/unit/file-mention-utils.test.ts index b8fac7f464b..30df1c12103 100644 --- a/packages/kilo-vscode/tests/unit/file-mention-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/file-mention-utils.test.ts @@ -5,11 +5,18 @@ import { buildTextAfterMentionSelect, buildFileAttachments, buildMentionResults, + buildSessionAttachments, filterMentionResults, getMentionRemovalRange, + getPastChatsMentionResult, isCursorAtMentionEnd, findMentionRange, + sessionMentionFilename, + sessionMentionText, + sessionMentionToken, + syncMentionedSessions, FILE_PICKER_RESULT, + PAST_CHATS_RESULT, TERMINAL_RESULT, GIT_CHANGES_RESULT, } from "../../webview-ui/src/hooks/file-mention-utils" @@ -89,6 +96,7 @@ describe("buildMentionResults", () => { expect(result).toEqual([ TERMINAL_RESULT, GIT_CHANGES_RESULT, + PAST_CHATS_RESULT, { type: "file", value: "src/index.ts" }, FILE_PICKER_RESULT, ]) @@ -552,3 +560,111 @@ describe("findMentionRange", () => { expect(findMentionRange(text, 4, paths)).toEqual({ start: 3, end: 6 }) }) }) + +describe("session mentions", () => { + const now = Date.now() + const sessions = [ + { id: "ses_a", title: "Fix auth bug", updated: now }, + { id: "ses_b", title: "Rotate signing keys", updated: now - 1000 }, + { id: "ses_c", title: "Refactor cache layer", updated: now - 2000 }, + ] + + describe("getPastChatsMentionResult", () => { + it("offers the past-chats picker for an empty query", () => { + expect(getPastChatsMentionResult("")).toEqual([PAST_CHATS_RESULT]) + }) + + it("offers the picker for alias prefixes", () => { + expect(getPastChatsMentionResult("pas")).toEqual([PAST_CHATS_RESULT]) + expect(getPastChatsMentionResult("sess")).toEqual([PAST_CHATS_RESULT]) + expect(getPastChatsMentionResult("hist")).toEqual([PAST_CHATS_RESULT]) + }) + + it("hides the picker for unrelated queries", () => { + expect(getPastChatsMentionResult("index")).toEqual([]) + }) + }) + + describe("sessionMentionText / filename", () => { + it("collapses whitespace in titles", () => { + expect(sessionMentionText("Fix\nauth bug")).toBe("Fix auth bug") + }) + + it("slugifies titles for the attachment filename", () => { + expect(sessionMentionFilename("Fix auth bug", "ses_a")).toBe("Fix-auth-bug.md") + }) + + it("falls back to the session id when the slug is empty", () => { + expect(sessionMentionFilename("???", "ses_a")).toBe("ses_a.md") + }) + + it("disambiguates sessions with the same title", () => { + const known = new Map([["Fix auth bug", sessions[0]!]]) + expect(sessionMentionToken({ ...sessions[1]!, title: "Fix auth bug" }, known)).toBe("Fix auth bug (2)") + }) + + it("reuses the token already assigned to a session", () => { + const known = new Map([["Fix auth bug (2)", sessions[1]!]]) + expect(sessionMentionToken(sessions[1]!, known)).toBe("Fix auth bug (2)") + }) + }) + + describe("buildMentionResults", () => { + it("offers the past-chats picker alongside the other special mentions", () => { + const result = buildMentionResults("", []) + expect(result[0]).toEqual(TERMINAL_RESULT) + expect(result).toContainEqual(PAST_CHATS_RESULT) + expect(result[result.length - 1]).toEqual(FILE_PICKER_RESULT) + }) + }) + + describe("filterMentionResults", () => { + it("keeps the past-chats picker for alias queries", () => { + const filtered = filterMentionResults("sess", buildMentionResults("", [])) + expect(filtered).toContainEqual(PAST_CHATS_RESULT) + }) + }) + + describe("syncMentionedSessions", () => { + it("drops sessions whose token is no longer present in the text", () => { + const prev = new Map([ + ["Fix auth bug", sessions[0]!], + ["Rotate signing keys", sessions[1]!], + ]) + const kept = syncMentionedSessions(prev, "see @Fix auth bug here") + expect(kept.has("Fix auth bug")).toBe(true) + expect(kept.has("Rotate signing keys")).toBe(false) + }) + }) + + describe("buildSessionAttachments", () => { + it("builds a session: attachment with span offsets and a readable filename", () => { + const mentioned = new Map([["Fix auth bug", sessions[0]!]]) + const attachments = buildSessionAttachments("check @Fix auth bug out", mentioned) + expect(attachments).toHaveLength(1) + const att = attachments[0]! + expect(att.mime).toBe("text/plain") + expect(att.url).toBe("session:ses_a") + expect(att.filename).toBe("Fix-auth-bug.md") + expect(att.source?.type).toBe("file") + expect(att.source?.text.value).toBe("@Fix auth bug") + expect(att.source?.text.start).toBe(6) + expect(att.source?.text.end).toBe(19) + }) + + it("skips sessions whose token is not present in the text", () => { + const mentioned = new Map([["Fix auth bug", sessions[0]!]]) + expect(buildSessionAttachments("nothing here", mentioned)).toEqual([]) + }) + + it("attaches distinct sessions whose titles collide", () => { + const mentioned = new Map([ + ["Fix auth bug", sessions[0]!], + ["Fix auth bug (2)", { ...sessions[1]!, title: "Fix auth bug" }], + ]) + const attachments = buildSessionAttachments("compare @Fix auth bug with @Fix auth bug (2)", mentioned) + expect(attachments.map((item) => item.url)).toEqual(["session:ses_a", "session:ses_b"]) + expect(attachments.map((item) => item.source?.text.value)).toEqual(["@Fix auth bug", "@Fix auth bug (2)"]) + }) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-followup.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-followup.test.ts index e22a391fee7..1965da54521 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-followup.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-followup.test.ts @@ -140,7 +140,7 @@ describe("KiloProvider follow-up sessions", () => { revert: null, summary: null, }, - draftID: undefined, + activate: true, }, ]) }) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts index f4f417fad0a..274cb8cc60d 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-memory-events.test.ts @@ -28,18 +28,6 @@ function status(root: string) { } } -function show(root: string) { - return { - root: `${root}/.kilo/memory`, - state: status(root).state, - sources: { project: "", environment: "", corrections: "" }, - index: "", - items: "", - changes: "", - decisions: "", - } -} - describe("KiloProvider memory events", () => { it("routes tracked background memory events to their session directory", async () => { const calls: string[] = [] @@ -151,10 +139,6 @@ describe("KiloProvider memory events", () => { calls.push(["disable", input.directory]) return { data: { root: `${input.directory}/.kilo/memory`, state: status(input.directory).state } } }, - show: async (input: { directory: string }) => { - calls.push(["show", input.directory]) - return { data: show(input.directory) } - }, }, } as unknown as KiloClient const posts: unknown[] = [] @@ -176,7 +160,6 @@ describe("KiloProvider memory events", () => { ["status", "/repo/project"], ["disable", "/repo/project"], ["status", "/repo/project"], - ["show", "/repo/project"], ]) expect(posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", sessionID: "ses_active" })) }) diff --git a/packages/kilo-vscode/tests/unit/kilo-provider-memory.test.ts b/packages/kilo-vscode/tests/unit/kilo-provider-memory.test.ts index 1d3fb20c11f..4769ccf2fe1 100644 --- a/packages/kilo-vscode/tests/unit/kilo-provider-memory.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-provider-memory.test.ts @@ -1,5 +1,6 @@ -import { describe, expect, it } from "bun:test" +import { describe, expect, it, spyOn } from "bun:test" import type { KiloClient } from "@kilocode/sdk/v2/client" +import * as vscode from "vscode" import { KiloProviderMemory } from "../../src/kilo-provider/memory" function subject(client: KiloClient | undefined) { @@ -18,6 +19,7 @@ function status(root: string) { root: `${root}/.kilo/memory`, state: { enabled: true, + scope: "project", autoConsolidate: true, stats: { lastInjectedSessionID: "", @@ -42,6 +44,89 @@ function show(root: string) { } describe("KiloProviderMemory", () => { + it("shows stored memory and explains empty projects", async () => { + const picker = spyOn(vscode.window, "showQuickPick") + const notice = spyOn(vscode.window, "showInformationMessage") + const full = status("/repo") + const view = show("/repo") + view.items = "record id=project.md:Facts:test :: Stored memory fact :: with context" + const stored = subject({ + memory: { + show: async () => ({ data: view }), + status: async () => ({ data: full }), + }, + } as unknown as KiloClient) + const empty = subject({ + memory: { + show: async () => ({ data: show("/empty") }), + status: async () => ({ data: status("/empty") }), + }, + } as unknown as KiloClient) + + try { + await stored.memory.show("ses_stored") + await empty.memory.show("ses_empty") + + expect(picker).toHaveBeenCalledTimes(1) + expect(picker.mock.calls[0]?.[0]).toContainEqual( + expect.objectContaining({ label: "Storage", detail: "/repo/.kilo/memory" }), + ) + expect(picker.mock.calls[0]?.[0]).toContainEqual( + expect.objectContaining({ label: "Stored memory fact :: with context" }), + ) + expect(notice).toHaveBeenCalledWith( + "This project doesn't have any memory yet. It will start showing after you use Kilo.", + ) + } finally { + picker.mockRestore() + notice.mockRestore() + } + }) + + it("shows the stored memory total when the list is truncated", async () => { + const picker = spyOn(vscode.window, "showQuickPick") + const view = show("/repo") + view.items = Array.from({ length: 17 }, (_, i) => `- id=item-${i} :: Fact ${i}`).join("\n") + const item = subject({ + memory: { + show: async () => ({ data: view }), + status: async () => ({ data: status("/repo") }), + }, + } as unknown as KiloClient) + + try { + await item.memory.show("ses_stored") + + expect(picker.mock.calls[0]?.[0]).toContainEqual( + expect.objectContaining({ label: "Stored memory", description: "16 of 17 shown" }), + ) + expect(picker.mock.calls[0]?.[0]).toHaveLength(21) + } finally { + picker.mockRestore() + } + }) + + it("routes inspect operations to the memory folder", async () => { + const reveal = spyOn(vscode.commands, "executeCommand") + const item = subject({ + memory: { + status: async () => ({ data: status("/repo") }), + show: async () => ({ data: show("/repo") }), + }, + } as unknown as KiloClient) + + try { + await item.memory.run({ operation: "inspect", sessionID: "ses_inspect" }) + + expect(reveal).toHaveBeenCalledWith("revealFileInOS", expect.objectContaining({ fsPath: "/repo/.kilo/memory" })) + expect(item.posts).toContainEqual( + expect.objectContaining({ type: "memoryOperationResult", operation: "inspect", ok: true }), + ) + } finally { + reveal.mockRestore() + } + }) + it("handles clients without memory endpoints gracefully", async () => { const item = subject({} as KiloClient) @@ -117,7 +202,7 @@ describe("KiloProviderMemory", () => { expect(posts[1]).toMatchObject({ type: "memoryLoaded", sessionID: "ses_8", - show: { root: "/repo/ses_8/.kilo/memory" }, + status: { root: "/repo/ses_8/.kilo/memory" }, }) }) @@ -158,34 +243,29 @@ describe("KiloProviderMemory", () => { it("routes status operations without mutating memory", async () => { const calls: string[] = [] const state = status("/repo") - const view = show("/repo") const item = subject({ memory: { status: async () => { calls.push("status") return { data: state } }, - show: async () => { - calls.push("show") - return { data: view } - }, }, } as unknown as KiloClient) await item.memory.run({ operation: "status", sessionID: "ses_memory" }) - expect(calls).toEqual(["status", "status", "show"]) + expect(calls).toEqual(["status"]) expect(item.posts).toContainEqual( - expect.objectContaining({ type: "memoryOperationResult", operation: "status", ok: true }), + expect.objectContaining({ type: "memoryOperationResult", operation: "status", ok: true, result: state }), ) + expect(item.posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", status: state })) }) - it("routes auto-save, verbose, and purge operations with explicit payloads", async () => { + it("routes auto-save and purge operations with explicit payloads", async () => { const calls: unknown[] = [] const state = status("/repo") const view = show("/repo") state.state.autoConsolidate = false - state.state.verbose = true const item = subject({ memory: { configure: async (input: unknown) => { @@ -202,14 +282,12 @@ describe("KiloProviderMemory", () => { } as unknown as KiloClient) await item.memory.run({ operation: "auto", mode: "off", sessionID: "ses_memory" }) - await item.memory.run({ operation: "verbose", mode: "on", sessionID: "ses_memory" }) await item.memory.run({ operation: "purge", confirm: true, sessionID: "ses_memory" }) expect(calls).toEqual([ ["configure", { directory: "/repo", autoConsolidate: false }], - ["configure", { directory: "/repo", verbose: true }], ["purge", { directory: "/repo", confirm: true }], ]) - expect(item.posts.filter((post) => (post as { type?: string }).type === "memoryOperationResult")).toHaveLength(3) + expect(item.posts.filter((post) => (post as { type?: string }).type === "memoryOperationResult")).toHaveLength(2) }) }) diff --git a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts index fbc8aa42555..7c226ecaba0 100644 --- a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts @@ -24,12 +24,22 @@ const DATA_CONTEXT_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/context/data const MESSAGE_PART_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/components/message-part.tsx") const KILO_MESSAGE_PART_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/message-part.tsx") const KILO_MESSAGE_HIGHLIGHT_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/message-highlight.ts") +const KILO_BASIC_TOOL_CSS_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/basic-tool.css") const KILO_MESSAGE_PART_CSS_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/message-part.css") const SHELL_ROLLING_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/shell-rolling-results.tsx") const ASSISTANT_MESSAGE_FILE = path.join( MONOREPO_ROOT, "packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx", ) +const TASK_HEADER_FILE = path.join(MONOREPO_ROOT, "packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx") +const CONTEXT_TAB_FILE = path.join( + MONOREPO_ROOT, + "packages/kilo-vscode/webview-ui/src/components/settings/ContextTab.tsx", +) +const PROMPT_INPUT_FILE = path.join( + MONOREPO_ROOT, + "packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx", +) const TRANSCRIPT_PARTS_FILE = path.join(MONOREPO_ROOT, "packages/kilo-vscode/webview-ui/src/utils/transcript-parts.ts") const CHAT_LAYOUT_FILE = path.join(MONOREPO_ROOT, "packages/kilo-vscode/webview-ui/src/styles/chat-layout.css") @@ -331,6 +341,55 @@ describe("AssistantMessage visible row contract (source)", () => { it("uses the plan exit card only when plan metadata is renderable", () => { expect(src).toContain("if (!planExitInfo(part)) return") }) + + it("uses the native recall tool without a separate memory badge", () => { + const tools = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8") + expect(src).not.toContain("assistant-memory-badge") + expect(tools).toContain("ToolRegistry.render(part.tool) ?? McpTool") + }) +}) + +describe("Native tool summary contract (source)", () => { + const tools = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8") + const css = fs.readFileSync(KILO_BASIC_TOOL_CSS_FILE, "utf-8") + + it("shows one secondary argument while preserving complete expanded input", () => { + const start = tools.indexOf("const inputArgs") + const end = tools.indexOf("const formatted", start) + expect(tools.slice(start, end)).toContain(".slice(0, 1)") + expect(tools).toContain("JSON.stringify(props.input, null, 2)") + }) + + it("gives the primary label remaining width and bounds secondary arguments", () => { + expect(css).toMatch(/\[data-slot="basic-tool-tool-info"\][\s\S]*?flex: 1 1 auto;/) + expect(css).toMatch(/\[data-slot="basic-tool-tool-subtitle"\][\s\S]*?flex: 1 1 auto;/) + expect(css).toMatch(/\[data-slot="basic-tool-tool-arg"\][\s\S]*?max-width: 24ch;/) + }) +}) + +describe("Memory control placement contract (source)", () => { + const header = fs.readFileSync(TASK_HEADER_FILE, "utf-8") + const settings = fs.readFileSync(CONTEXT_TAB_FILE, "utf-8") + const prompt = fs.readFileSync(PROMPT_INPUT_FILE, "utf-8") + + it("keeps memory controls out of the task header", () => { + expect(header).not.toContain("useMemory") + expect(header).not.toContain('name="memory"') + }) + + it("shows storage inspection in settings without a manual rebuild action", () => { + expect(settings).toContain("settings.context.memory.storage.title") + expect(settings).toContain("settings.context.memory.status.enabledTokens") + expect(settings).toContain("memory.inspect()") + expect(settings).not.toContain("memory.rebuild()") + expect(settings).not.toContain("lastOperationCount") + expect(settings).not.toContain("sessionTokens") + }) + + it("expands bare memory commands into inline completion", () => { + expect(prompt).toContain('const value = "/memory "') + expect(prompt).toContain("slash.onInput(value, value.length)") + }) }) describe("Assistant transcript spacing contract (source)", () => { diff --git a/packages/kilo-vscode/tests/unit/local-tabs.test.ts b/packages/kilo-vscode/tests/unit/local-tabs.test.ts index b8ff4d7fecd..058bd6b514f 100644 --- a/packages/kilo-vscode/tests/unit/local-tabs.test.ts +++ b/packages/kilo-vscode/tests/unit/local-tabs.test.ts @@ -14,6 +14,7 @@ import { restoreTabs, restoreTrackedTabs, showTabStrip, + tabsForCreatedSession, trackedSessionInventory, type LocalTabState, } from "../../webview-ui/src/utils/local-tabs" @@ -61,6 +62,25 @@ const tracked = () => ) describe("local session tabs", () => { + it("opens explicitly activated sessions in the foreground", () => { + expect(tabsForCreatedSession(state(["s1"], "s1"), "s2", undefined, true)).toEqual({ + ids: ["s1", "s2"], + active: "s2", + }) + }) + + it("promotes a matching pending draft into the created session", () => { + expect(tabsForCreatedSession(state([pending()], pending()), "s1", pending(), undefined)).toEqual({ + ids: ["s1"], + active: "s1", + }) + }) + + it("ignores created sessions without activation or a pending draft", () => { + expect(tabsForCreatedSession(state(["s1"], "s1"), "s2", undefined, undefined)).toBeUndefined() + expect(tabsForCreatedSession(state(["s1"], "s1"), "s2", "sidebar-pending:gone", undefined)).toBeUndefined() + }) + it("hides the tab strip when only one tab remains", () => { expect(showTabStrip([pending()])).toBe(false) expect(showTabStrip([pending(), "sidebar-pending:2"])).toBe(true) diff --git a/packages/kilo-vscode/tests/unit/memory-activity.test.ts b/packages/kilo-vscode/tests/unit/memory-activity.test.ts deleted file mode 100644 index 316175e256b..00000000000 --- a/packages/kilo-vscode/tests/unit/memory-activity.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, expect, it } from "bun:test" -import { MemoryMarkerMeta } from "@kilocode/kilo-memory/marker-meta" -import { addMemoryActivity, markerActivity } from "../../webview-ui/src/utils/memory-activity" - -describe("memory activity", () => { - it("accumulates saved events with their message and source references", () => { - const items = addMemoryActivity( - [], - { - type: "saved", - message: "Saved project memory", - operationCount: 4, - added: 3, - removed: 1, - sources: ["project.md:kilo_colors"], - }, - 10, - ) - - expect(items).toEqual([ - { - type: "saved", - at: 10, - tokens: 0, - count: 3, - items: ["Saved project memory"], - refs: ["project.md:kilo_colors"], - }, - ]) - expect(addMemoryActivity(items, { type: "recalled" }, 20)).toEqual(items) - }) - - it("ignores removal-only events and caps saved activity", () => { - const removed = { type: "saved" as const, message: "Memory updated · 1 removed", added: 0, removed: 1 } - expect(addMemoryActivity([], removed, 10)).toEqual([]) - - const items = Array.from({ length: 60 }).reduce( - (all, _, at) => addMemoryActivity(all, { type: "saved", added: 1 }, at), - [] as ReturnType, - ) - expect(items).toHaveLength(50) - expect(items[0]?.at).toBe(10) - expect(items.at(-1)?.at).toBe(59) - }) - - it("decodes loaded and recalled markers for activity summaries", () => { - const loaded = markerActivity( - [ - { - type: "text", - metadata: MemoryMarkerMeta.metadata( - { - type: "startup", - bytes: 10, - tokens: 42, - count: 1, - files: ["project.md"], - items: ["Use Kilo colors"], - }, - true, - ), - }, - ], - 10, - ) - const recalled = markerActivity( - [ - { - type: "text", - metadata: MemoryMarkerMeta.metadata( - { - type: "recall", - bytes: 10, - tokens: 8, - count: 2, - files: ["project.md"], - items: ["Prefer dark mode"], - }, - true, - ), - }, - ], - 20, - ) - - expect(loaded).toMatchObject({ type: "loaded", tokens: 42, count: 1, items: [] }) - expect(recalled).toMatchObject({ type: "recalled", tokens: 8, count: 2, items: ["Prefer dark mode"] }) - }) -}) diff --git a/packages/kilo-vscode/tests/unit/memory-command.test.ts b/packages/kilo-vscode/tests/unit/memory-command.test.ts index 6b160d62af0..d42a67aa866 100644 --- a/packages/kilo-vscode/tests/unit/memory-command.test.ts +++ b/packages/kilo-vscode/tests/unit/memory-command.test.ts @@ -4,7 +4,7 @@ import { parseMemoryCommand, type ParsedMemoryCommand } from "../../webview-ui/s type MemoryOperation = | "enable" | "status" - | "edit" + | "inspect" | "disable" | "rebuild" | "remember" @@ -12,7 +12,6 @@ type MemoryOperation = | "forget" | "purge" | "auto" - | "verbose" type Case = { name: string input: string @@ -43,7 +42,7 @@ function expected(item: Case): ParsedMemoryCommand | undefined { if (!item.query) throw new Error(`Missing query for fixture: ${item.name}`) return { kind: "operation", operation: item.operation, query: item.query } } - if (item.operation === "auto" || item.operation === "verbose") { + if (item.operation === "auto") { if (!item.mode) throw new Error(`Missing mode for fixture: ${item.name}`) return { kind: "operation", operation: item.operation, mode: item.mode } } diff --git a/packages/kilo-vscode/tests/unit/message-files.test.ts b/packages/kilo-vscode/tests/unit/message-files.test.ts index 106008353d4..19cbf041246 100644 --- a/packages/kilo-vscode/tests/unit/message-files.test.ts +++ b/packages/kilo-vscode/tests/unit/message-files.test.ts @@ -23,4 +23,22 @@ describe("parseMessageFiles", () => { it("rejects unsupported URLs", () => { expect(parseMessageFiles([{ mime: "text/plain", url: "https://example.com/file.txt" }])).toBeUndefined() }) + + it("accepts past-chat session attachments", () => { + const files = parseMessageFiles([ + { + mime: "text/plain", + url: "session:ses_07c08a2ddffeXample", + filename: "fix-auth-bug.md", + source: { + type: "file", + path: "session:ses_07c08a2ddffeXample", + text: { value: "@Fix auth bug", start: 0, end: 13 }, + }, + }, + ]) + + expect(files?.[0]?.url).toBe("session:ses_07c08a2ddffeXample") + expect(files?.[0]?.filename).toBe("fix-auth-bug.md") + }) }) diff --git a/packages/kilo-vscode/tests/unit/question-handler.test.ts b/packages/kilo-vscode/tests/unit/question-handler.test.ts index c097b71a8a7..10d66029d18 100644 --- a/packages/kilo-vscode/tests/unit/question-handler.test.ts +++ b/packages/kilo-vscode/tests/unit/question-handler.test.ts @@ -29,7 +29,7 @@ function ctx( dirs?: Map extra?: string[] pending?: Record - errors?: { list?: Record; reply?: unknown; reject?: unknown } + errors?: { list?: Record; reply?: unknown | unknown[]; reject?: unknown | unknown[] } changeOnList?: string removeOnList?: string } = {}, @@ -42,7 +42,11 @@ function ctx( const dirs = opts.dirs ?? new Map() let revision = 0 let changed = false + let reply = 0 + let reject = 0 const removed = new Set() + const failure = (value: unknown | unknown[] | undefined, index: number) => + Array.isArray(value) ? value[index] : value const client = { question: { list: async (args: { directory?: string }) => { @@ -63,12 +67,14 @@ function ctx( }, reply: async (args: unknown) => { replies.push(args) - if (opts.errors?.reply) throw opts.errors.reply + const error = failure(opts.errors?.reply, reply++) + if (error) throw error return { data: true } }, reject: async (args: unknown) => { rejects.push(args) - if (opts.errors?.reject) throw opts.errors.reject + const error = failure(opts.errors?.reject, reject++) + if (error) throw error return { data: true } }, }, @@ -167,26 +173,96 @@ describe("question handlers", () => { expect(messages).toContainEqual({ type: "questionResolved", requestID: "req-stale" }) }) - it("keeps fallback-directory 404s retryable while recovering the request route", async () => { + it("retries a reply through the recovered request directory", async () => { const error = new Error("Question request not found", { cause: { status: 404, body: { name: "NotFoundError" } }, }) const dir = "/workspace/.kilo/worktrees/origin" - const { fake, messages, questionDirs } = ctx({ + const { fake, messages, replies, questionDirs } = ctx({ tracked: ["ses-root"], extra: [dir], pending: { [dir]: [pending("req-misrouted", "ses-root")] }, - errors: { reply: error }, + errors: { reply: [error] }, }) + questionDirs.set("req-misrouted", "/workspace/.kilo/worktrees/stale") + + const ok = await handleQuestionReply(fake, "req-misrouted", [["Continue"]], "ses-root") + + expect(ok).toBe(true) + expect(replies).toEqual([ + { + requestID: "req-misrouted", + answers: [["Continue"]], + directory: "/workspace/.kilo/worktrees/stale", + }, + { + requestID: "req-misrouted", + answers: [["Continue"]], + directory: dir, + }, + ]) + expect(messages).not.toContainEqual({ type: "questionResolved", requestID: "req-misrouted" }) + expect(messages).not.toContainEqual({ type: "questionError", requestID: "req-misrouted" }) + expect(messages).not.toContainEqual({ + type: "questionRequest", + question: pending("req-misrouted", "ses-root"), + }) + expect(questionDirs.has("req-misrouted")).toBe(false) + }) + + it("retries even when an unrelated directory fails to list", async () => { + const error = new Error("Question request not found", { + cause: { status: 404, body: { name: "NotFoundError" } }, + }) + const dir = "/workspace/.kilo/worktrees/origin" + const failing = "/workspace/.kilo/worktrees/failing" + const { fake, messages, replies, questionDirs } = ctx({ + tracked: ["ses-root"], + extra: [dir, failing], + pending: { [dir]: [pending("req-misrouted", "ses-root")] }, + errors: { list: { [failing]: new Error("temporary failure") }, reply: [error] }, + }) + questionDirs.set("req-misrouted", "/workspace/.kilo/worktrees/stale") const spy = spyOn(console, "error").mockImplementation(() => {}) const ok = await handleQuestionReply(fake, "req-misrouted", [["Continue"]], "ses-root") spy.mockRestore() - expect(ok).toBe(false) - expect(messages).not.toContainEqual({ type: "questionResolved", requestID: "req-misrouted" }) - expect(messages).toContainEqual({ type: "questionError", requestID: "req-misrouted" }) - expect(questionDirs.get("req-misrouted")).toBe(dir) + expect(ok).toBe(true) + expect(replies.map((args) => (args as { directory: string }).directory)).toEqual([ + "/workspace/.kilo/worktrees/stale", + dir, + ]) + expect(messages).not.toContainEqual({ type: "questionError", requestID: "req-misrouted" }) + expect(questionDirs.has("req-misrouted")).toBe(false) + }) + + it("retries a reject through the recovered request directory", async () => { + const error = new Error("Question request not found", { + cause: { status: 404, body: { name: "NotFoundError" } }, + }) + const dir = "/workspace/.kilo/worktrees/origin" + const { fake, messages, rejects, questionDirs } = ctx({ + tracked: ["ses-root"], + extra: [dir], + pending: { [dir]: [pending("req-misrouted", "ses-root")] }, + errors: { reject: [error] }, + }) + questionDirs.set("req-misrouted", "/workspace/.kilo/worktrees/stale") + + const ok = await handleQuestionReject(fake, "req-misrouted", "ses-root") + + expect(ok).toBe(true) + expect(rejects).toEqual([ + { requestID: "req-misrouted", directory: "/workspace/.kilo/worktrees/stale" }, + { requestID: "req-misrouted", directory: dir }, + ]) + expect(messages).not.toContainEqual({ type: "questionError", requestID: "req-misrouted" }) + expect(messages).not.toContainEqual({ + type: "questionRequest", + question: pending("req-misrouted", "ses-root"), + }) + expect(questionDirs.has("req-misrouted")).toBe(false) }) it("removes a fallback question when recovery confirms it is stale", async () => { diff --git a/packages/kilo-vscode/tests/unit/session-outcome.test.ts b/packages/kilo-vscode/tests/unit/session-outcome.test.ts index ec30837e93b..8dd06b6dd69 100644 --- a/packages/kilo-vscode/tests/unit/session-outcome.test.ts +++ b/packages/kilo-vscode/tests/unit/session-outcome.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test" import { terminal } from "../../webview-ui/src/context/session-outcome" -import type { Message, TodoItem } from "../../webview-ui/src/types/messages" +import type { Message, Part, TodoItem } from "../../webview-ui/src/types/messages" function message(finish?: string, error?: Message["error"]): Message { return { @@ -43,11 +43,76 @@ describe("terminal", () => { expect(terminal({ reason: "completed", messages: [message("unknown")], todos: [] })?.kind).toBe("unknown") }) + it("includes the Vercel response ID for an unknown finish", () => { + expect( + terminal({ + reason: "completed", + messages: [ + message("unknown", { + name: "APIError", + data: { responseHeaders: { "X-Vercel-Id": "fra1::abc" } }, + }), + ], + todos: [], + hidden: () => true, + }), + ).toEqual({ + kind: "unknown", + tone: "warning", + finish: "unknown", + remaining: 0, + vercelID: "fra1::abc", + }) + }) + it("surfaces filtered and unexpected provider finishes", () => { expect(terminal({ reason: "completed", messages: [message("content-filter")], todos: [] })?.kind).toBe("filtered") expect(terminal({ reason: "completed", messages: [message("other")], todos: [] })?.kind).toBe("unexpected") }) + it("includes both request ids for unexpected provider finishes", () => { + const parts: Part[] = [ + { + id: "p1", + sessionID: "s1", + messageID: "m1", + type: "step-finish", + reason: "other", + generationID: "gen_test", + vercelID: "fra1::other", + }, + ] + expect( + terminal({ + reason: "completed", + messages: [message("other")], + todos: [], + parts: () => parts, + }), + ).toMatchObject({ + kind: "unexpected", + finish: "other", + vercelID: "fra1::other", + generationID: "gen_test", + }) + }) + + it("does not expose generation ids for other terminal outcomes", () => { + const parts: Part[] = [ + { + id: "p1", + sessionID: "s1", + messageID: "m1", + type: "step-finish", + reason: "unknown", + generationID: "gen_test", + }, + ] + expect( + terminal({ reason: "completed", messages: [message("unknown")], todos: [], parts: () => parts }), + ).not.toHaveProperty("generationID") + }) + it("surfaces interruption and failures without a rendered error", () => { expect(terminal({ reason: "interrupted", messages: [message("stop")], todos: [todo("pending")] })).toEqual({ kind: "interrupted", diff --git a/packages/kilo-vscode/tests/unit/session-tab-switcher.test.ts b/packages/kilo-vscode/tests/unit/session-tab-switcher.test.ts new file mode 100644 index 00000000000..2c621dc5858 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/session-tab-switcher.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "bun:test" +import { unlinkSync } from "node:fs" +import path from "node:path" +import { build } from "esbuild" +import { solidPlugin } from "esbuild-plugin-solid" + +const ROOT = path.resolve(import.meta.dir, "../..") +const WEBVIEW = path.join(ROOT, "webview-ui") +const FIXTURE = path.join(ROOT, "tests/fixtures/session-tab-switcher.tsx") + +describe("SessionTabSwitcher", () => { + it("preserves filtering and restores focus across tab actions", async () => { + const solid = path.dirname(Bun.resolveSync("solid-js/package.json", WEBVIEW)) + const aliases: Record = { + "solid-js": path.join(solid, "dist/solid.js"), + "solid-js/web": path.join(solid, "web/dist/web.js"), + "solid-js/store": path.join(solid, "store/dist/store.js"), + } + const dedupe = { + name: "solid-dedupe", + setup(ctx: Parameters[0]["plugins"]>[number]["setup"]>[0]) { + ctx.onResolve({ filter: /^solid-js(\/web|\/store)?$/ }, (args) => ({ path: aliases[args.path] })) + }, + } + const result = await build({ + entryPoints: [FIXTURE], + bundle: true, + conditions: ["browser"], + external: ["happy-dom"], + format: "esm", + logLevel: "silent", + platform: "node", + plugins: [dedupe, solidPlugin()], + target: "es2022", + write: false, + }) + const file = path.join(ROOT, `.session-tab-switcher-${crypto.randomUUID()}.mjs`) + await Bun.write(file, result.outputFiles[0]!.contents) + const child = Bun.spawnSync(["bun", file], { cwd: WEBVIEW, stdout: "pipe", stderr: "pipe" }) + unlinkSync(file) + + const output = child.stdout.toString() + child.stderr.toString() + expect(child.exitCode, output).toBe(0) + }) + + it("uses logical properties for RTL layout", async () => { + const css = await Bun.file(path.join(WEBVIEW, "src/styles/session-tabs.css")).text() + const start = css.indexOf(".session-tab-switcher-wrap") + const end = css.indexOf("/* Match tab context menus", start) + const switcher = css.slice(start, end) + + expect(switcher).toContain("border-inline-start") + expect(switcher).toContain("inset-inline-end") + expect(switcher).toContain("padding-inline") + expect(switcher).not.toMatch(/\b(?:left|right|margin-left|margin-right|border-left|border-right)\s*:/) + }) +}) diff --git a/packages/kilo-vscode/tests/unit/session-utils.test.ts b/packages/kilo-vscode/tests/unit/session-utils.test.ts index 5be45947682..43430379491 100644 --- a/packages/kilo-vscode/tests/unit/session-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/session-utils.test.ts @@ -4,6 +4,12 @@ import { calcTotalCost, calcContextUsage, calcTokenUsage, + aggregateMetrics, + latestMetrics, + messageMetrics, + messageThroughput, + sessionThroughput, + formatTG, buildFamilyCosts, buildFamilyParents, buildFamilyParentsFromTools, @@ -717,3 +723,263 @@ describe("collapseCostBreakdown", () => { expect(shown).toBe(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11) }) }) + +// ── Throughput aggregation ───────────────────────────────────────────── + +type StepFinishOverrides = { + metrics?: NonNullable + tokens?: { input: number; output: number; reasoning?: number; cache?: { read: number; write: number } } + time?: { start: number; end: number; elapsed: number } +} + +function stepFinish(id: string, metricsOrOverrides?: NonNullable | StepFinishOverrides): Part { + // Older call sites pass only metrics directly. Keep that signature so + // the existing latestMetrics / messageMetrics tests stay readable. + if ( + metricsOrOverrides && + "metrics" in metricsOrOverrides === false && + "tokens" in metricsOrOverrides === false && + "time" in metricsOrOverrides === false + ) { + return { + type: "step-finish", + id, + ...(metricsOrOverrides ? { metrics: metricsOrOverrides } : {}), + } + } + const overrides = (metricsOrOverrides ?? {}) as StepFinishOverrides + return { + type: "step-finish", + id, + ...(overrides.metrics ? { metrics: overrides.metrics } : {}), + ...(overrides.tokens ? { tokens: overrides.tokens } : {}), + ...(overrides.time ? { time: overrides.time } : {}), + } +} + +describe("latestMetrics", () => { + it("returns undefined when no step-finish parts carry metrics", () => { + const parts: Part[] = [ + { type: "step-start", id: "s1" }, + stepFinish("f1"), + { type: "text", id: "t1", text: "hello" }, + ] + expect(latestMetrics(parts)).toBeUndefined() + }) + + it("picks the last non-empty generation rate across every step in the session", () => { + const parts: Part[] = [ + stepFinish("f1", { prompt: 100, generation: 20, source: "computed" }), + { type: "text", id: "t1", text: "mid" }, + stepFinish("f2", { prompt: 412, generation: 38, source: "computed" }), + ] + expect(latestMetrics(parts)).toEqual({ generation: 38, source: "computed" }) + }) + + it("uses the latest computed value when earlier steps report lower rates", () => { + const parts: Part[] = [ + stepFinish("f1", { prompt: 500, generation: 50, source: "computed" }), + stepFinish("f2", { generation: 30, source: "computed" }), + ] + const result = latestMetrics(parts) + expect(result?.source).toBe("computed") + expect(result?.generation).toBe(30) + }) + + it("falls back to the only computed sample when no later one is present", () => { + const parts: Part[] = [stepFinish("f1", { generation: 12, source: "computed" }), stepFinish("f2")] + expect(latestMetrics(parts)).toEqual({ generation: 12, source: "computed" }) + }) + + it("ignores non-step-finish parts even when they look like metrics", () => { + const parts: Part[] = [ + { type: "text", id: "t1", text: "noise" }, + stepFinish("f1", { prompt: 200, generation: 22, source: "computed" }), + ] + expect(latestMetrics(parts)).toEqual({ generation: 22, source: "computed" }) + }) +}) + +describe("aggregateMetrics", () => { + // Historical alias of latestMetrics — kept so external callers and tests + // that still use the original name keep working. Behaviour matches: the + // last non-empty step-finish generation rate wins. + it("matches latestMetrics for the same input", () => { + const parts: Part[] = [ + stepFinish("f1", { generation: 25, source: "computed" }), + stepFinish("f2", { generation: 12, source: "computed" }), + ] + expect(aggregateMetrics(parts)).toEqual(latestMetrics(parts)) + }) +}) + +describe("messageMetrics", () => { + it("picks the last non-empty generation rate within a single assistant message", () => { + // An assistant turn that runs reasoning + answer produces two step-finish + // parts; the badge surfaces the final step's generation rate so the + // user sees the rate for the most recent reasoning or text generation + // in that turn. + const parts: Part[] = [ + stepFinish("f1", { generation: 25, source: "computed" }), + stepFinish("f2", { generation: 12, source: "computed" }), + ] + expect(messageMetrics(parts)).toEqual({ generation: 12, source: "computed" }) + }) + + it("matches latestMetrics behavior on the same input", () => { + const parts: Part[] = [ + stepFinish("f1", { generation: 8, source: "computed" }), + stepFinish("f2", { prompt: 99, generation: 33, source: "computed" }), + ] + expect(messageMetrics(parts)).toEqual(latestMetrics(parts)) + }) + + it("returns undefined when no throughput metrics are present", () => { + expect(messageMetrics([])).toBeUndefined() + expect(messageMetrics([{ type: "text", id: "t1", text: "no metrics here" }])).toBeUndefined() + }) +}) + +describe("throughput formatters", () => { + const locale = "en-US" + + it("renders the value with a t/s suffix", () => { + expect(formatTG(412, locale)).toBe("412 t/s") + expect(formatTG(28.7, locale)).toBe("28.7 t/s") + }) + + it("falls back to dash for missing or bogus values", () => { + expect(formatTG(undefined, locale)).toBe("–") + expect(formatTG(0, locale)).toBe("–") + expect(formatTG(-5, locale)).toBe("–") + expect(formatTG(Number.NaN, locale)).toBe("–") + expect(formatTG(Number.POSITIVE_INFINITY, locale)).toBe("–") + }) +}) + +// Weighted throughput — the value rendered beneath each assistant message +// after the v2 refactor. Behaves like a per-turn weighted average: total +// generated tokens across step-finish parts divided by total active +// model-generation duration, excluding tool-only or untimed steps. +describe("messageThroughput", () => { + it("returns undefined when no step-finish parts carry timing", () => { + const parts: Part[] = [ + { type: "step-start", id: "s1" }, + stepFinish("f1", { metrics: { generation: 100, source: "computed" } }), + ] + expect(messageThroughput(parts)).toBeUndefined() + }) + + it("computes a single-step rate from tokens and elapsed ms", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + ] + // (200 + 0) * 1000 / 1000 = 200 + expect(messageThroughput(parts)).toEqual({ generation: 200, source: "computed" }) + }) + + it("weights multiple steps by their elapsed time rather than averaging rates", () => { + // Discriminating case: weighted = (300 * 1000 / 5000) = 60 t/s, + // last-wins = 50 t/s. Confirms the formula doesn't just take the final + // step's value. + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 100, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + stepFinish("f2", { + tokens: { input: 10, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 1000, end: 5000, elapsed: 4000 }, + }), + ] + expect(messageThroughput(parts)).toEqual({ generation: 60, source: "computed" }) + }) + + it("includes reasoning tokens in the numerator", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 100, reasoning: 200, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + ] + // (100 + 200) * 1000 / 1000 = 300 + expect(messageThroughput(parts)).toEqual({ generation: 300, source: "computed" }) + }) + + it("ignores step-finish parts without timing", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + // No `time` field — older part shape, possibly replayed session. + stepFinish("f2", { metrics: { generation: 999, source: "computed" } }), + ] + expect(messageThroughput(parts)).toEqual({ generation: 200, source: "computed" }) + }) + + it("ignores tool-only steps that produced no output tokens", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 500, elapsed: 500 }, + }), + stepFinish("f2", { + tokens: { input: 10, output: 100, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 500, end: 1500, elapsed: 1000 }, + }), + ] + expect(messageThroughput(parts)).toEqual({ generation: 100, source: "computed" }) + }) + + it("returns undefined when only tool-only steps are present", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 500, elapsed: 500 }, + }), + ] + expect(messageThroughput(parts)).toBeUndefined() + }) + + it("returns undefined when timing is non-positive across all steps", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 100, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 0, elapsed: 0 }, + }), + ] + expect(messageThroughput(parts)).toBeUndefined() + }) +}) + +describe("sessionThroughput", () => { + it("aggregates the same way as messageThroughput across a flat part array", () => { + const parts: Part[] = [ + stepFinish("f1", { + tokens: { input: 10, output: 100, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 0, end: 1000, elapsed: 1000 }, + }), + stepFinish("f2", { + tokens: { input: 10, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 2000, end: 5000, elapsed: 3000 }, + }), + // From the "next" message — still rolled up correctly. + stepFinish("f3", { + tokens: { input: 10, output: 500, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { start: 6000, end: 11000, elapsed: 5000 }, + }), + ] + // (800 * 1000) / 9000 = 88.888... + const result = sessionThroughput(parts) + expect(result?.source).toBe("computed") + expect(result?.generation).toBeCloseTo((800 * 1000) / 9000, 5) + }) + + it("returns undefined for empty input", () => { + expect(sessionThroughput([])).toBeUndefined() + }) +}) diff --git a/packages/kilo-vscode/tests/unit/transcript-parts.test.ts b/packages/kilo-vscode/tests/unit/transcript-parts.test.ts index 351fa3a9990..8d0b9abccc3 100644 --- a/packages/kilo-vscode/tests/unit/transcript-parts.test.ts +++ b/packages/kilo-vscode/tests/unit/transcript-parts.test.ts @@ -31,6 +31,13 @@ const SCRIPT = ` state: { status: "completed", input: {}, output: "done", title: "Updated todos" }, }, { id: "read-running", type: "tool", tool: "read", state: { status: "running", input: {} } }, + { id: "memory-running", type: "tool", tool: "kilo_memory_recall", state: { status: "running", input: {} } }, + { + id: "memory-completed", + type: "tool", + tool: "kilo_memory_recall", + state: { status: "completed", input: {}, output: "memory", title: "Memory recalled" }, + }, ] const visible = parts.filter((part) => isRenderable(part, message)).map((part) => part.id) @@ -38,7 +45,14 @@ const SCRIPT = ` console.log("${FAIL}" + reason) process.exit(2) } - const expected = ["visible-text", "visible-reasoning", "todo-completed", "read-running"] + const expected = [ + "visible-text", + "visible-reasoning", + "todo-completed", + "read-running", + "memory-running", + "memory-completed", + ] if (visible.length !== expected.length || visible.some((id, index) => id !== expected[index])) { fail("did not exclude transcript-invisible parts") } diff --git a/packages/kilo-vscode/tests/unit/use-slash-command.test.ts b/packages/kilo-vscode/tests/unit/use-slash-command.test.ts index b243ab06fc7..0718b5ab294 100644 --- a/packages/kilo-vscode/tests/unit/use-slash-command.test.ts +++ b/packages/kilo-vscode/tests/unit/use-slash-command.test.ts @@ -27,6 +27,76 @@ function setup(sandbox: () => void, options: { enabled?: () => boolean; exclude? } describe("useSlashCommand sandbox action", () => { + it("opens project memory actions from the top-level command", () => { + const ctx = setup(() => {}) + const state = { text: "/memory" } + const textarea = { + value: state.text, + setSelectionRange: () => {}, + focus: () => {}, + } as unknown as HTMLTextAreaElement + + ctx.slash.onInput("/mem", 4) + + expect(ctx.slash.results()).toContainEqual( + expect.objectContaining({ name: "memory", description: "Manage project memory", hints: ["mem"] }), + ) + ctx.slash.select(ctx.slash.results()[0]!, textarea, (text) => (state.text = text)) + expect(state.text).toBe("/memory ") + expect(ctx.slash.results().map((command) => command.name)).toContain("memory inspect") + ctx.dispose() + }) + + it("offers memory actions after the parent command", () => { + const ctx = setup(() => {}) + + ctx.slash.onInput("/memory ", 8) + + expect(ctx.slash.results().map((command) => command.name)).toEqual([ + "memory status", + "memory show", + "memory on", + "memory off", + "memory inspect", + "memory rebuild", + "memory remember", + "memory correct", + "memory forget", + "memory auto on", + "memory auto off", + "memory purge confirm", + ]) + ctx.dispose() + }) + + it("keeps nested memory actions out of root hint matching", () => { + const ctx = setup(() => {}) + const nested = ctx.slash.commands().filter((command) => command.name.startsWith("memory ")) + + expect(nested.length).toBeGreaterThan(0) + expect(nested.every((command) => command.hints.length === 0)).toBe(true) + ctx.dispose() + }) + + it("completes nested memory actions and closes for free text", () => { + const ctx = setup(() => {}) + const state = { text: "/mem rem" } + const textarea = { + value: state.text, + setSelectionRange: () => {}, + focus: () => {}, + } as unknown as HTMLTextAreaElement + + ctx.slash.onInput(state.text, state.text.length) + expect(ctx.slash.results().map((command) => command.name)).toEqual(["memory remember"]) + ctx.slash.select(ctx.slash.results()[0]!, textarea, (text) => (state.text = text)) + expect(state.text).toBe("/memory remember ") + + ctx.slash.onInput("/memory remember durable fact", 31) + expect(ctx.slash.show()).toBe(false) + ctx.dispose() + }) + it("runs the sandbox toggle as a client command", () => { const state = { toggles: 0, text: "/sandbox", prevented: 0 } const ctx = setup(() => state.toggles++) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index a9f607b1565..60252b3977b 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -761,6 +761,16 @@ const AgentManagerContent: Component = () => { return sessionsForWorktree(sel) }) + const activeWorktreeSessionIds = createMemo | undefined>(() => { + const sel = selection() + if (!sel || sel === LOCAL) return undefined + return new Set( + managedSessions() + .filter((item) => item.worktreeId === sel) + .map((item) => item.id), + ) + }) + const activeTabs = createMemo((): SessionInfo[] => { const sel = selection() if (sel === LOCAL) return localSessions() @@ -2898,6 +2908,7 @@ const AgentManagerContent: Component = () => { openLocally(id) }} onBack={() => setHistory(false)} + worktreeSessionIds={activeWorktreeSessionIds} />
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx index 7da9c9cb79f..e228c0f7d86 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarSearchMenu.tsx @@ -3,6 +3,7 @@ import { Show, createEffect, createSignal } from "solid-js" import type { Accessor, Component } from "solid-js" import { Icon } from "@kilocode/kilo-ui/icon" +import { IconButton } from "@kilocode/kilo-ui/icon-button" import { List } from "@kilocode/kilo-ui/list" import type { ListRef } from "@kilocode/kilo-ui/list" import { Popover } from "@kilocode/kilo-ui/popover" @@ -71,16 +72,19 @@ export const SidebarSearchMenu: Component = (props) => { onOpenChange={close} modal={false} portal={props.portal} - class="am-sidebar-search-popover" - triggerAs="button" + class="search-menu-popover am-sidebar-search-popover" + contentLabel={props.labels.search} + triggerAs={IconButton} triggerProps={{ type: "button", - class: "am-sidebar-search-trigger", + icon: "magnifying-glass", + size: "normal", + variant: "ghost", + class: "search-menu-trigger", "aria-label": props.labels.search, }} - trigger={} > -