diff --git a/AGENTS.md b/AGENTS.md index 785ad5d24c2..8f3ed22f506 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -220,6 +220,21 @@ const bar = 2 // kilocode_change - new file ``` + +**JSX/TSX (inside JSX templates):** + + +```tsx +{/* kilocode_change */} +``` + + +```tsx +{/* kilocode_change start */} + +{/* kilocode_change end */} +``` + #### When markers are NOT needed Code in these paths is Kilo Code-specific and does NOT need `kilocode_change` markers: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 65565b37ab7..d7950ddfe8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,12 +72,10 @@ During development, `bun dev` is the local equivalent of the built `kilo` comman # Development (from project root) bun dev --help # Show all available commands bun dev serve # Start headless API server -bun dev web # Start server + open web interface # Production kilo --help # Show all available commands kilo serve # Start headless API server -kilo web # Start server + open web interface ``` ### Testing with a local backend diff --git a/packages/kilo-docs/pages/automate/agent-manager.md b/packages/kilo-docs/pages/automate/agent-manager.md index 5e2c539e6a1..c06f9f23099 100644 --- a/packages/kilo-docs/pages/automate/agent-manager.md +++ b/packages/kilo-docs/pages/automate/agent-manager.md @@ -31,6 +31,17 @@ The panel opens as an editor tab and stays active across focus changes. Each Agent Manager session runs in an isolated git worktree on a separate branch, keeping your main branch clean. +### PR Status Badges + +Worktree items in the sidebar display a **PR status badge** when the branch has an associated pull request: + +- **Open** — badge indicating the PR is open (its color can also reflect review and check status) +- **Merged** — purple badge indicating the PR has been merged +- **Closed** — red badge indicating the PR was closed without merging +- **Draft** — gray badge indicating the PR is in draft state + +The badge appears on the right side of each worktree item and updates automatically via polling. If the worktree's branch doesn't have a PR yet, no badge is shown. + ### Creating a New Worktree Session 1. Click **New Worktree** or press `Cmd+N` (macOS) / `Ctrl+N` (Windows/Linux) to create a new worktree diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/vscode/whats-new.md b/packages/kilo-docs/pages/code-with-ai/platforms/vscode/whats-new.md index 408d4e69652..1ed95d5f199 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/vscode/whats-new.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/vscode/whats-new.md @@ -37,7 +37,13 @@ See [Auto-Approving Actions](/docs/getting-started/settings/auto-approving-actio ### Is the context progress graph still available? -The context progress graph will be [added soon](https://github.com/Kilo-Org/kilocode/issues/8210) for users who like to see it. +Yes — the context progress graph (also known as the task timeline) is now available. It appears at the top of the chat panel and shows: + +- **Timeline bars** — colored bars representing session activity (different colors for read, write, tool, error, and text parts) +- **Context window progress** — a three-segment bar showing used, reserved, and available tokens, with a visual indicator when usage exceeds 50% +- **Token breakdown** — input, output, cache writes, and cache reads display + +You can expand or collapse the graph — your preference is saved in the `kilo-code.new.showTaskTimeline` setting. ### I like to closely monitor and approve the behavior of the agent. How can I do that better in the new version? diff --git a/packages/kilo-vscode/eslint.config.mjs b/packages/kilo-vscode/eslint.config.mjs index f82b5a9d931..5b01bdb44a9 100644 --- a/packages/kilo-vscode/eslint.config.mjs +++ b/packages/kilo-vscode/eslint.config.mjs @@ -34,7 +34,7 @@ export default [ { files: ["src/KiloProvider.ts"], rules: { - "max-lines": ["error", 3200], + "max-lines": ["error", 3300], }, }, eslintConfigPrettier, diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index bc7e48b8924..136c0408f3b 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -90,12 +90,26 @@ import { saveCustomProvider as saveCustomProviderAction, } from "./provider-actions" import { fetchOpenAIModels, FetchModelsError } from "./shared/fetch-models" +import type { Agent } from "@kilocode/sdk/v2/client" type KiloProviderOptions = { projectDirectory?: string | null slimEditMetadata?: boolean } +// Helper to map agent data to the subset of fields sent to the webview +const mapAgent = (a: Agent) => ({ + name: a.name, + displayName: a.displayName, + description: a.description, + mode: a.mode, + native: a.native, + hidden: a.hidden, + color: a.color, + deprecated: a.deprecated, + permission: a.permission, +}) + export class KiloProvider implements vscode.WebviewViewProvider, TelemetryPropertiesProvider { public static readonly viewType = "kilo-code.SidebarProvider" @@ -1621,15 +1635,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper const message = { type: "agentsLoaded", - agents: visible.map((a) => ({ - name: a.name, - displayName: a.displayName, - description: a.description, - mode: a.mode, - native: a.native, - color: a.color, - deprecated: a.deprecated, - })), + agents: visible.map(mapAgent), + allAgents: agents.map(mapAgent), defaultAgent, } this.cachedAgentsMessage = message diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts index 8078dad5b2c..3765785d96f 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts @@ -218,12 +218,12 @@ export class WorktreeStateManager { const removed = this.worktrees.delete(id) if (!removed) return [] - // Dissociate all sessions from this worktree (set worktreeId to null) + // Collect and remove all sessions belonging to this worktree const orphaned: ManagedSession[] = [] for (const s of this.sessions.values()) { if (s.worktreeId === id) { - s.worktreeId = null - orphaned.push(s) + orphaned.push({ ...s }) + this.sessions.delete(s.id) } } @@ -234,7 +234,7 @@ export class WorktreeStateManager { const idx = this.worktreeOrder.indexOf(id) if (idx !== -1) this.worktreeOrder.splice(idx, 1) - this.log(`Removed worktree ${id}, orphaned ${orphaned.length} sessions`) + this.log(`Removed worktree ${id}, removed ${orphaned.length} sessions`) void this.save() return orphaned } @@ -491,7 +491,13 @@ export class WorktreeStateManager { }) ?? wt.path this.worktrees.set(id, { id, ...wt, path: fixed }) } + let pruned = 0 for (const [id, s] of Object.entries(data.sessions ?? {})) { + // Skip orphaned sessions (null worktreeId or referencing a deleted worktree) + if (!s.worktreeId || !this.worktrees.has(s.worktreeId)) { + pruned++ + continue + } this.sessions.set(id, { id, ...s }) } for (const [id, sec] of Object.entries(data.sections ?? {})) { @@ -517,6 +523,10 @@ export class WorktreeStateManager { } this.defaultBase = data.defaultBaseBranch this.log(`Loaded state: ${this.worktrees.size} worktrees, ${this.sessions.size} sessions`) + if (pruned > 0) { + this.log(`Pruned ${pruned} orphaned sessions`) + void this.save() + } } catch (error) { const code = (error as NodeJS.ErrnoException).code if (code !== "ENOENT") { @@ -526,7 +536,7 @@ export class WorktreeStateManager { return migration } - /** Remove worktrees whose directories no longer exist on disk. */ + /** Remove worktrees whose directories no longer exist on disk and prune orphaned sessions. */ async validate(root: string): Promise { let changed = false for (const wt of [...this.worktrees.values()]) { @@ -537,7 +547,17 @@ export class WorktreeStateManager { changed = true } } - if (changed) await this.save() + // Prune orphaned sessions (worktreeId is null or references a deleted worktree) + for (const s of [...this.sessions.values()]) { + if (!s.worktreeId || !this.worktrees.has(s.worktreeId)) { + this.sessions.delete(s.id) + changed = true + } + } + if (changed) { + this.log(`Pruned orphaned sessions during validation`) + await this.save() + } } /** Wait for any in-flight save to complete without triggering a new one. */ diff --git a/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts b/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts index 5b0c2af1be2..714cf01522d 100644 --- a/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts @@ -41,7 +41,7 @@ describe("WorktreeStateManager", () => { expect(manager.findWorktreeByPath("/tmp/c")).toBeUndefined() }) - it("removes worktree and orphans sessions", () => { + it("removes worktree and deletes its sessions", () => { const wt = manager.addWorktree({ branch: "fix", path: "/tmp/fix", parentBranch: "main" }) manager.addSession("s1", wt.id) manager.addSession("s2", wt.id) @@ -49,9 +49,10 @@ describe("WorktreeStateManager", () => { const orphaned = manager.removeWorktree(wt.id) expect(orphaned).toHaveLength(2) expect(manager.getWorktrees()).toHaveLength(0) - // Sessions still exist but with null worktreeId - expect(manager.getSession("s1")?.worktreeId).toBeNull() - expect(manager.getSession("s2")?.worktreeId).toBeNull() + // Sessions are removed from state + expect(manager.getSession("s1")).toBeUndefined() + expect(manager.getSession("s2")).toBeUndefined() + expect(manager.getSessions()).toHaveLength(0) }) it("returns empty array when removing nonexistent worktree", () => { @@ -140,7 +141,7 @@ describe("WorktreeStateManager", () => { }) describe("persistence", () => { - it("saves and loads state", async () => { + it("saves and loads state, pruning orphaned sessions", async () => { const wt = manager.addWorktree({ branch: "fix", path: "/tmp/fix", parentBranch: "main" }) manager.addSession("s1", wt.id) manager.addSession("s2", null) @@ -153,9 +154,10 @@ describe("WorktreeStateManager", () => { expect(loaded.getWorktrees()).toHaveLength(1) expect(loaded.getWorktrees()[0].branch).toBe("fix") - expect(loaded.getSessions()).toHaveLength(2) + // s2 had null worktreeId so it gets pruned on load + expect(loaded.getSessions()).toHaveLength(1) expect(loaded.getSession("s1")?.worktreeId).toBe(wt.id) - expect(loaded.getSession("s2")?.worktreeId).toBeNull() + expect(loaded.getSession("s2")).toBeUndefined() }) it("load is a no-op when file does not exist", async () => { @@ -285,20 +287,35 @@ describe("WorktreeStateManager", () => { }) describe("validate", () => { - it("removes worktrees whose directories do not exist", async () => { + it("removes worktrees whose directories do not exist and prunes their sessions", async () => { const existing = path.join(root, "wt-exists") fs.mkdirSync(existing, { recursive: true }) manager.addWorktree({ branch: "exists", path: existing, parentBranch: "main" }) - manager.addWorktree({ branch: "gone", path: path.join(root, "wt-gone"), parentBranch: "main" }) - manager.addSession("s1", manager.getWorktrees()[1].id) + const gone = manager.addWorktree({ branch: "gone", path: path.join(root, "wt-gone"), parentBranch: "main" }) + manager.addSession("s1", gone.id) await manager.validate(root) expect(manager.getWorktrees()).toHaveLength(1) expect(manager.getWorktrees()[0].branch).toBe("exists") - // Session orphaned (worktreeId set to null) - expect(manager.getSession("s1")?.worktreeId).toBeNull() + // Session removed along with its worktree + expect(manager.getSession("s1")).toBeUndefined() + }) + + it("prunes orphaned sessions with null worktreeId on validate", async () => { + const existing = path.join(root, "wt-exists") + fs.mkdirSync(existing, { recursive: true }) + + const wt = manager.addWorktree({ branch: "exists", path: existing, parentBranch: "main" }) + manager.addSession("s1", wt.id) + manager.addSession("s2", null) + + await manager.validate(root) + + // s1 stays (its worktree exists), s2 is pruned (null worktreeId) + expect(manager.getSession("s1")).toBeTruthy() + expect(manager.getSession("s2")).toBeUndefined() }) it("resolves relative paths against root", async () => { @@ -319,8 +336,9 @@ describe("WorktreeStateManager", () => { for (let i = 0; i < 20; i++) { manager.addWorktree({ branch: `b-${i}`, path: `/tmp/b-${i}`, parentBranch: "main" }) } + const wts = manager.getWorktrees() for (let i = 0; i < 20; i++) { - manager.addSession(`s-${i}`, null) + manager.addSession(`s-${i}`, wts[i]!.id) } // Wait for all fire-and-forget saves to settle @@ -355,9 +373,9 @@ describe("WorktreeStateManager", () => { expect(loaded.getWorktrees()).toHaveLength(1) expect(loaded.getWorktrees()[0].branch).toBe("keep") - // s2 was orphaned when wt2 was removed, s1 and s3 belong to wt1 + // s2 was removed when wt2 was removed, s1 and s3 belong to wt1 expect(loaded.getSession("s1")?.worktreeId).toBe(wt1.id) - expect(loaded.getSession("s2")?.worktreeId).toBeNull() + expect(loaded.getSession("s2")).toBeUndefined() expect(loaded.getSession("s3")?.worktreeId).toBe(wt1.id) }) @@ -419,7 +437,7 @@ describe("WorktreeStateManager", () => { expect(manager.getSessions()).toHaveLength(0) }) - it("handles partial data with missing worktrees key", async () => { + it("handles partial data with missing worktrees key and prunes orphaned sessions", async () => { const file = path.join(root, ".kilo", "agent-manager.json") fs.writeFileSync( file, @@ -430,7 +448,8 @@ describe("WorktreeStateManager", () => { await manager.load() expect(manager.getWorktrees()).toHaveLength(0) - expect(manager.getSessions()).toHaveLength(1) + // Orphaned session with null worktreeId is pruned on load + expect(manager.getSessions()).toHaveLength(0) }) }) diff --git a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx index 5349789bae3..ed5583a9e3c 100644 --- a/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/settings/AgentBehaviourTab.tsx @@ -82,7 +82,12 @@ const AgentBehaviourTab: Component = () => { }) const agentNames = createMemo(() => { - const names = session.agents().map((a) => a.name) + // Exclude server-side hidden internal modes (compaction, title, summary) + // from the list. Config-only agents are still added below. + const names = session + .allAgents() + .filter((a) => !a.hidden) + .map((a) => a.name) // Also include any agents from config that might not be in the agent list const agents = Object.keys(config().agent ?? {}) for (const name of agents) { @@ -93,10 +98,15 @@ const AgentBehaviourTab: Component = () => { return names.sort() }) - const defaultAgentOptions = createMemo(() => [ - { value: "", label: language.t("common.default") }, - ...agentNames().map((name) => ({ value: name, label: name })), - ]) + // Default-agent picker must only show visible primary agents (not subagents + // or hidden modes) since the CLI rejects those as default_agent values. + const defaultAgentOptions = createMemo(() => { + const visible = session.agents().map((a) => a.name) + return [ + { value: "", label: language.t("common.default") }, + ...visible.map((name) => ({ value: name, label: name })), + ] + }) const instructions = () => config().instructions ?? [] @@ -185,7 +195,7 @@ const AgentBehaviourTab: Component = () => { )) } - const removableModes = createMemo(() => session.agents().filter((a) => !a.native)) + const removableModes = createMemo(() => session.allAgents().filter((a) => !a.native)) const confirmRemoveMode = (agent: AgentInfo) => { dialog.show(() => ( @@ -354,7 +364,7 @@ const AgentBehaviourTab: Component = () => { {(name, index) => { - const agent = () => session.agents().find((a) => a.name === name) + const agent = () => session.allAgents().find((a) => a.name === name) const isCustom = () => !agent()?.native const agentCfg = () => config().agent?.[name] ?? {} const disabled = () => agentCfg().disable ?? false @@ -396,6 +406,19 @@ const AgentBehaviourTab: Component = () => { custom + + + {language.t("settings.agentBehaviour.badge.subagent")} + + = (props) => { // agent() may be undefined for modes that only exist in the config draft (just // created, not yet saved). This is fine — native defaults to false (correct for // custom modes) and all fields read from cfg() which comes from config context. - const agent = () => session.agents().find((a) => a.name === props.name) + const agent = () => session.allAgents().find((a) => a.name === props.name) const native = () => agent()?.native ?? false + const [expanded, setExpanded] = createSignal(false) const cfg = createMemo(() => config().agent?.[props.name] ?? {}) @@ -230,6 +231,18 @@ const ModeEditView: Component = (props) => { + {/* Calculated permissions (read-only, collapsible) */} + + {(rules) => ( + setExpanded((v) => !v)} + /> + )} + + {language.t("settings.agentBehaviour.editMode.back")} @@ -239,4 +252,201 @@ const ModeEditView: Component = (props) => { ) } +// --------------------------------------------------------------------------- +// Collapsible permissions ruleset display +// --------------------------------------------------------------------------- + +const ACTION_COLORS: Record = { + allow: { bg: "var(--vscode-terminal-ansiGreen, #3fb950)", fg: "var(--vscode-editor-background, #1e1e1e)" }, + ask: { bg: "var(--vscode-editorWarning-foreground, #cca700)", fg: "var(--vscode-editor-background, #1e1e1e)" }, + deny: { bg: "var(--vscode-errorForeground, #f85149)", fg: "var(--vscode-editor-background, #fff)" }, + unknown: { bg: "var(--vscode-descriptionForeground, #8b949e)", fg: "var(--vscode-editor-background, #1e1e1e)" }, +} + +interface RulesetProps { + agent: string + rules: PermissionRuleItem[] + expanded: boolean + onToggle: () => void +} + +const PermissionRuleset: Component = (props) => { + const language = useLanguage() + const [copied, setCopied] = createSignal(false) + + // Compute effective action per unique tool by finding the last rule with pattern "*" + // NOTE: This assumes the CLI uses "*" as the wildcard pattern for catch-all rules. + // If the CLI convention changes (e.g. to "**" or another pattern), this will need updating. + const summary = createMemo(() => { + const tools = new Map() + for (const rule of props.rules) { + if (rule.pattern === "*") { + tools.set(rule.permission, rule.action) + } + } + return [...tools.entries()].sort((a, b) => a[0].localeCompare(b[0])) + }) + + const copy = (e: MouseEvent) => { + e.stopPropagation() + const data = { agent: props.agent, rules: props.rules } + navigator.clipboard.writeText(JSON.stringify(data, null, 2)) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( + + + { + e.stopPropagation() + props.onToggle() + }} + /> + + {language.t("settings.agentBehaviour.permissions.title")} + + + {language.t("settings.agentBehaviour.permissions.count", { count: String(props.rules.length) })} + + + + + + + + {/* Summary: effective action per tool for wildcard pattern */} + 0}> + + + {language.t("settings.agentBehaviour.permissions.effective")} + + + + {([tool, action]) => { + const colors = ACTION_COLORS[action] ?? ACTION_COLORS.unknown + return ( + + {tool}: {action} + + ) + }} + + + + + + {/* Full ruleset table */} + + + + + + {language.t("settings.agentBehaviour.permissions.col.tool")} + + + {language.t("settings.agentBehaviour.permissions.col.pattern")} + + + {language.t("settings.agentBehaviour.permissions.col.action")} + + + + + + {(rule, idx) => { + const colors = ACTION_COLORS[rule.action] ?? ACTION_COLORS.unknown + return ( + 0 ? "1px solid var(--border-weak-base, var(--vscode-panel-border))" : "none", + }} + > + {rule.permission} + {rule.pattern} + + + {rule.action} + + + + ) + }} + + + + + + + {language.t("settings.agentBehaviour.permissions.hint")} + + + + ) +} + export default ModeEditView diff --git a/packages/kilo-vscode/webview-ui/src/context/session.tsx b/packages/kilo-vscode/webview-ui/src/context/session.tsx index 3ce48123df2..4907de27b30 100644 --- a/packages/kilo-vscode/webview-ui/src/context/session.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/session.tsx @@ -134,6 +134,7 @@ interface SessionContextValue { // Agent/mode selection (per-session) agents: Accessor + allAgents: Accessor removeMode: (name: string) => void removeMcp: (name: string) => void @@ -255,6 +256,7 @@ export const SessionProvider: ParentComponent = (props) => { // Agents (modes) loaded from the CLI backend const [agents, setAgents] = createSignal([]) + const [allAgents, setAllAgents] = createSignal([]) const [defaultAgent, setDefaultAgent] = createSignal("code") // Skills loaded from the CLI backend @@ -468,6 +470,7 @@ export const SessionProvider: ParentComponent = (props) => { return } setAgents(message.agents) + setAllAgents(message.allAgents ?? message.agents) setDefaultAgent(message.defaultAgent) const names = new Set(message.agents.map((a) => a.name)) @@ -1808,6 +1811,7 @@ export const SessionProvider: ParentComponent = (props) => { costBreakdown, contextUsage, agents, + allAgents, skills, refreshSkills, removeSkill, diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 1911d9160d2..4aaca97a6c0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -1117,6 +1117,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "هذا وضع مضمن. لا يمكن تغيير تعريفه الأساسي، ولكن يمكنك تكوين التجاوزات أدناه.", "settings.agentBehaviour.editMode.promptOverride": "تجاوز موجه مخصص لهذا الوضع المدمج", + "settings.agentBehaviour.badge.subagent": "وكيل فرعي", + "settings.agentBehaviour.permissions.title": "الأذونات المحسوبة", + "settings.agentBehaviour.permissions.count": "{{count}} قواعد", + "settings.agentBehaviour.permissions.effective": "الفعال (حرف بدل):", + "settings.agentBehaviour.permissions.col.tool": "الأداة", + "settings.agentBehaviour.permissions.col.pattern": "النمط", + "settings.agentBehaviour.permissions.col.action": "الإجراء", + "settings.agentBehaviour.permissions.copy": "نسخ الأذونات كـ JSON", + "settings.agentBehaviour.permissions.hint": + "يتم تقييم القواعد بالترتيب — القاعدة المطابقة الأخيرة هي التي تُطبق. هذه هي مجموعة القواعد المحلولة من خلفية CLI.", "settings.agentBehaviour.removeMode.title": "إزالة الوضع", "settings.agentBehaviour.removeMode.confirm": 'هل تريد إزالة الوضع "{{name}}"؟ سيؤدي هذا إلى تعطيل الوضع عن طريق تحديث الإعدادات.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 723bd0ace71..399efbf5530 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -1136,6 +1136,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "Este é um modo embutido. Sua definição base não pode ser alterada, mas você pode configurar as sobrescritas abaixo.", "settings.agentBehaviour.editMode.promptOverride": "Substituição de prompt personalizado para este modo integrado", + "settings.agentBehaviour.badge.subagent": "subagente", + "settings.agentBehaviour.permissions.title": "Permissões Calculadas", + "settings.agentBehaviour.permissions.count": "{{count}} regras", + "settings.agentBehaviour.permissions.effective": "Efetiva (curinga):", + "settings.agentBehaviour.permissions.col.tool": "Ferramenta", + "settings.agentBehaviour.permissions.col.pattern": "Padrão", + "settings.agentBehaviour.permissions.col.action": "Ação", + "settings.agentBehaviour.permissions.copy": "Copiar permissões como JSON", + "settings.agentBehaviour.permissions.hint": + "As regras são avaliadas em ordem — a última regra correspondente vence. Este é o conjunto de regras resolvido do backend da CLI.", "settings.agentBehaviour.removeMode.title": "Remover modo", "settings.agentBehaviour.removeMode.confirm": 'Remover o modo "{{name}}"? Isso desativará o modo atualizando sua configuração.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 414701e0653..be7bfbaa460 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -1134,6 +1134,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "Ovo je ugrađeni način rada. Njegova osnovna definicija ne može se mijenjati, ali ispod možete konfigurirati nadjačavanja.", "settings.agentBehaviour.editMode.promptOverride": "Prilagođeno nadjačavanje prompta za ovaj ugrađeni mod", + "settings.agentBehaviour.badge.subagent": "podagent", + "settings.agentBehaviour.permissions.title": "Izračunate dozvole", + "settings.agentBehaviour.permissions.count": "{{count}} pravila", + "settings.agentBehaviour.permissions.effective": "Efektivno (wildcard):", + "settings.agentBehaviour.permissions.col.tool": "Alat", + "settings.agentBehaviour.permissions.col.pattern": "Uzorak", + "settings.agentBehaviour.permissions.col.action": "Akcija", + "settings.agentBehaviour.permissions.copy": "Kopiraj dozvole kao JSON", + "settings.agentBehaviour.permissions.hint": + "Pravila se evaluiraju po redoslijedu — zadnje pravilo koje se podudara pobjeđuje. Ovo je riješeni skup pravila iz CLI backenda.", "settings.agentBehaviour.removeMode.title": "Ukloni mod", "settings.agentBehaviour.removeMode.confirm": 'Ukloniti mod "{{name}}"? Ovo će onemogućiti mod ažuriranjem vaše konfiguracije.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index c7a0ee2bb4f..625836b01f8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -1127,6 +1127,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "Dette er en indbygget tilstand. Dens grundlæggende definition kan ikke ændres, men du kan konfigurere tilsidesættelser nedenfor.", "settings.agentBehaviour.editMode.promptOverride": "Tilpasset prompt-tilsidesættelse for denne indbyggede tilstand", + "settings.agentBehaviour.badge.subagent": "subagent", + "settings.agentBehaviour.permissions.title": "Beregnede tilladelser", + "settings.agentBehaviour.permissions.count": "{{count}} regler", + "settings.agentBehaviour.permissions.effective": "Gældende (wildcard):", + "settings.agentBehaviour.permissions.col.tool": "Værktøj", + "settings.agentBehaviour.permissions.col.pattern": "Mønster", + "settings.agentBehaviour.permissions.col.action": "Handling", + "settings.agentBehaviour.permissions.copy": "Kopiér tilladelser som JSON", + "settings.agentBehaviour.permissions.hint": + "Reglerne evalueres i rækkefølge — den sidst matchende regel vinder. Dette er det endelige regelsæt fra CLI-backenden.", "settings.agentBehaviour.removeMode.title": "Fjern tilstand", "settings.agentBehaviour.removeMode.confirm": 'Vil du fjerne tilstanden "{{name}}"? Dette vil deaktivere tilstanden ved at opdatere din konfiguration.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 139cc84bfa5..3e9b7e263c0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -1149,6 +1149,16 @@ export const dict = { "Dies ist ein integrierter Modus. Seine Basisdefinition kann nicht geändert werden, aber Sie können unten Überschreibungen konfigurieren.", "settings.agentBehaviour.editMode.promptOverride": "Benutzerdefinierte Prompt-Überschreibung für diesen eingebauten Modus", + "settings.agentBehaviour.badge.subagent": "Subagent", + "settings.agentBehaviour.permissions.title": "Berechnete Berechtigungen", + "settings.agentBehaviour.permissions.count": "{{count}} Regeln", + "settings.agentBehaviour.permissions.effective": "Effektiv (Platzhalter):", + "settings.agentBehaviour.permissions.col.tool": "Werkzeug", + "settings.agentBehaviour.permissions.col.pattern": "Muster", + "settings.agentBehaviour.permissions.col.action": "Aktion", + "settings.agentBehaviour.permissions.copy": "Berechtigungen als JSON kopieren", + "settings.agentBehaviour.permissions.hint": + "Regeln werden der Reihe nach ausgewertet — die letzte übereinstimmende Regel gewinnt. Dies ist das aufgelöste Regelwerk vom CLI-Backend.", "settings.agentBehaviour.removeMode.title": "Modus entfernen", "settings.agentBehaviour.removeMode.confirm": 'Modus "{{name}}" entfernen? Dadurch wird der Modus durch Aktualisierung Ihrer Konfiguration deaktiviert.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index f375960517c..d25ab051951 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -1178,6 +1178,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "This is a built-in mode. Its base definition cannot be changed, but you can configure overrides below.", "settings.agentBehaviour.editMode.promptOverride": "Custom prompt override for this built-in mode", + "settings.agentBehaviour.badge.subagent": "subagent", + "settings.agentBehaviour.permissions.title": "Calculated Permissions", + "settings.agentBehaviour.permissions.count": "{{count}} rules", + "settings.agentBehaviour.permissions.effective": "Effective (wildcard):", + "settings.agentBehaviour.permissions.col.tool": "Tool", + "settings.agentBehaviour.permissions.col.pattern": "Pattern", + "settings.agentBehaviour.permissions.col.action": "Action", + "settings.agentBehaviour.permissions.copy": "Copy permissions as JSON", + "settings.agentBehaviour.permissions.hint": + "Rules are evaluated in order — last matching rule wins. This is the resolved ruleset from the CLI backend.", "settings.autoApprove.description": "Define how tools are allowed to run. Most tools default to Allow. doom_loop and external_directory default to Ask.", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index b05fadfa265..5b15425315c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -1141,6 +1141,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "Este es un modo integrado. Su definición base no se puede cambiar, pero puedes configurar sobreescrituras a continuación.", "settings.agentBehaviour.editMode.promptOverride": "Anulación de prompt personalizado para este modo integrado", + "settings.agentBehaviour.badge.subagent": "subagente", + "settings.agentBehaviour.permissions.title": "Permisos calculados", + "settings.agentBehaviour.permissions.count": "{{count}} reglas", + "settings.agentBehaviour.permissions.effective": "Efectivo (comodín):", + "settings.agentBehaviour.permissions.col.tool": "Herramienta", + "settings.agentBehaviour.permissions.col.pattern": "Patrón", + "settings.agentBehaviour.permissions.col.action": "Acción", + "settings.agentBehaviour.permissions.copy": "Copiar permisos como JSON", + "settings.agentBehaviour.permissions.hint": + "Las reglas se evalúan en orden — gana la última regla coincidente. Este es el conjunto de reglas resuelto desde el backend de la CLI.", "settings.agentBehaviour.removeMode.title": "Eliminar modo", "settings.agentBehaviour.removeMode.confirm": '¿Eliminar el modo "{{name}}"? Esto desactivará el modo actualizando su configuración.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index d9ee62fe576..ffcb99b0b91 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -1153,6 +1153,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "Il s'agit d'un mode intégré. Sa définition de base ne peut pas être modifiée, mais vous pouvez configurer des surcharges ci-dessous.", "settings.agentBehaviour.editMode.promptOverride": "Remplacement de prompt personnalisé pour ce mode intégré", + "settings.agentBehaviour.badge.subagent": "sous-agent", + "settings.agentBehaviour.permissions.title": "Permissions calculées", + "settings.agentBehaviour.permissions.count": "{{count}} règles", + "settings.agentBehaviour.permissions.effective": "Effectif (wildcard) :", + "settings.agentBehaviour.permissions.col.tool": "Outil", + "settings.agentBehaviour.permissions.col.pattern": "Motif", + "settings.agentBehaviour.permissions.col.action": "Action", + "settings.agentBehaviour.permissions.copy": "Copier les permissions en JSON", + "settings.agentBehaviour.permissions.hint": + "Les règles sont évaluées dans l'ordre — la dernière règle correspondante l'emporte. Il s'agit de l'ensemble de règles résolu depuis le backend CLI.", "settings.agentBehaviour.removeMode.title": "Supprimer le mode", "settings.agentBehaviour.removeMode.confirm": 'Supprimer le mode "{{name}}" ? Cela désactivera le mode en mettant à jour votre configuration.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 2466dd4acab..89c95a7eaa7 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -1129,6 +1129,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "これは組み込みモードです。基本定義は変更できませんが、以下でオーバーライドを設定できます。", "settings.agentBehaviour.editMode.promptOverride": "この組み込みモードのカスタムプロンプト上書き", + "settings.agentBehaviour.badge.subagent": "サブエージェント", + "settings.agentBehaviour.permissions.title": "計算された権限", + "settings.agentBehaviour.permissions.count": "{{count}} 個のルール", + "settings.agentBehaviour.permissions.effective": "有効(ワイルドカード):", + "settings.agentBehaviour.permissions.col.tool": "ツール", + "settings.agentBehaviour.permissions.col.pattern": "パターン", + "settings.agentBehaviour.permissions.col.action": "アクション", + "settings.agentBehaviour.permissions.copy": "権限をJSONとしてコピー", + "settings.agentBehaviour.permissions.hint": + "ルールは順番に評価され、最後に一致したルールが適用されます。これはCLIバックエンドから解決されたルールセットです。", "settings.agentBehaviour.removeMode.title": "モードを削除", "settings.agentBehaviour.removeMode.confirm": 'モード "{{name}}" を削除しますか?設定を更新してモードを無効にします。', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index 2ebaca8dedc..a6c1afd36dd 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -1123,6 +1123,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "이것은 내장 모드입니다. 기본 정의는 변경할 수 없지만, 아래에서 재정의를 구성할 수 있습니다.", "settings.agentBehaviour.editMode.promptOverride": "이 내장 모드에 대한 사용자 지정 프롬프트 재정의", + "settings.agentBehaviour.badge.subagent": "서브에이전트", + "settings.agentBehaviour.permissions.title": "계산된 권한", + "settings.agentBehaviour.permissions.count": "{{count}}개 규칙", + "settings.agentBehaviour.permissions.effective": "유효 (와일드카드):", + "settings.agentBehaviour.permissions.col.tool": "도구", + "settings.agentBehaviour.permissions.col.pattern": "패턴", + "settings.agentBehaviour.permissions.col.action": "작업", + "settings.agentBehaviour.permissions.copy": "권한을 JSON으로 복사", + "settings.agentBehaviour.permissions.hint": + "규칙은 순서대로 평가되며, 마지막에 일치하는 규칙이 적용됩니다. 이것은 CLI 백엔드에서 확인된 규칙 세트입니다.", "settings.agentBehaviour.removeMode.title": "모드 제거", "settings.agentBehaviour.removeMode.confirm": '모드 "{{name}}"을(를) 제거하시겠습니까? 구성을 업데이트하여 모드를 비활성화합니다.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index fb18d132f80..77e2aab3b7a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -1382,5 +1382,15 @@ export const dict = { "Dit is een ingebouwde modus. De basisdefinitie kan niet worden gewijzigd, maar u kunt hieronder overrides configureren.", "settings.agentBehaviour.editMode.prompt": "Systeemprompt", "settings.agentBehaviour.editMode.promptOverride": "Aangepaste systeemprompt voor deze ingebouwde modus", + "settings.agentBehaviour.badge.subagent": "subagent", + "settings.agentBehaviour.permissions.title": "Berekende machtigingen", + "settings.agentBehaviour.permissions.count": "{{count}} regels", + "settings.agentBehaviour.permissions.effective": "Effectief (wildcard):", + "settings.agentBehaviour.permissions.col.tool": "Tool", + "settings.agentBehaviour.permissions.col.pattern": "Patroon", + "settings.agentBehaviour.permissions.col.action": "Actie", + "settings.agentBehaviour.permissions.copy": "Machtigingen kopiëren als JSON", + "settings.agentBehaviour.permissions.hint": + "Regels worden op volgorde geëvalueerd — de laatst overeenkomende regel wint. Dit is de opgeloste regelset van de CLI backend.", "settings.agentBehaviour.editMode.save": "Klaar", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index f99649fca0f..b06456e47b5 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -1130,6 +1130,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "Dette er en innebygd modus. Grunndefinisjonen kan ikke endres, men du kan konfigurere overstyringer nedenfor.", "settings.agentBehaviour.editMode.promptOverride": "Tilpasset prompt-overstyring for denne innebygde modusen", + "settings.agentBehaviour.badge.subagent": "subagent", + "settings.agentBehaviour.permissions.title": "Beregnede tillatelser", + "settings.agentBehaviour.permissions.count": "{{count}} regler", + "settings.agentBehaviour.permissions.effective": "Gjeldende (wildcard):", + "settings.agentBehaviour.permissions.col.tool": "Verktøy", + "settings.agentBehaviour.permissions.col.pattern": "Mønster", + "settings.agentBehaviour.permissions.col.action": "Handling", + "settings.agentBehaviour.permissions.copy": "Kopier tillatelser som JSON", + "settings.agentBehaviour.permissions.hint": + "Reglene evalueres i rekkefølge — siste matchende regel vinner. Dette er det gjeldende regelsettet fra CLI-backenden.", "settings.agentBehaviour.removeMode.title": "Fjern modus", "settings.agentBehaviour.removeMode.confirm": 'Vil du fjerne modusen "{{name}}"? Dette vil deaktivere modusen ved å oppdatere konfigurasjonen din.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 896cbff05d0..856ea582178 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -1133,6 +1133,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "To jest wbudowany tryb. Jego podstawowa definicja nie może zostać zmieniona, ale poniżej możesz skonfigurować nadpisania.", "settings.agentBehaviour.editMode.promptOverride": "Niestandardowe nadpisanie promptu dla tego wbudowanego trybu", + "settings.agentBehaviour.badge.subagent": "podagent", + "settings.agentBehaviour.permissions.title": "Obliczone uprawnienia", + "settings.agentBehaviour.permissions.count": "{{count}} reguł", + "settings.agentBehaviour.permissions.effective": "Efektywne (wieloznacznik):", + "settings.agentBehaviour.permissions.col.tool": "Narzędzie", + "settings.agentBehaviour.permissions.col.pattern": "Wzorzec", + "settings.agentBehaviour.permissions.col.action": "Akcja", + "settings.agentBehaviour.permissions.copy": "Kopiuj uprawnienia jako JSON", + "settings.agentBehaviour.permissions.hint": + "Reguły są sprawdzane po kolei — ostatnia pasująca reguła wygrywa. To jest wynikowy zestaw reguł z backendu CLI.", "settings.agentBehaviour.removeMode.title": "Usuń tryb", "settings.agentBehaviour.removeMode.confirm": 'Usunąć tryb "{{name}}"? Spowoduje to wyłączenie trybu poprzez aktualizację konfiguracji.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 2d8a1bb2cc3..1eff6b800d1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -1136,6 +1136,16 @@ export const dict = { "Это встроенный режим. Его базовое определение нельзя изменить, но вы можете настроить переопределения ниже.", "settings.agentBehaviour.editMode.promptOverride": "Пользовательское переопределение промпта для этого встроенного режима", + "settings.agentBehaviour.badge.subagent": "субагент", + "settings.agentBehaviour.permissions.title": "Вычисленные разрешения", + "settings.agentBehaviour.permissions.count": "{{count}} правил", + "settings.agentBehaviour.permissions.effective": "Действующие (wildcard):", + "settings.agentBehaviour.permissions.col.tool": "Инструмент", + "settings.agentBehaviour.permissions.col.pattern": "Шаблон", + "settings.agentBehaviour.permissions.col.action": "Действие", + "settings.agentBehaviour.permissions.copy": "Скопировать разрешения как JSON", + "settings.agentBehaviour.permissions.hint": + "Правила оцениваются по порядку — побеждает последнее совпавшее правило. Это разрешенный набор правил из бэкенда CLI.", "settings.agentBehaviour.removeMode.title": "Удалить режим", "settings.agentBehaviour.removeMode.confirm": 'Удалить режим "{{name}}"? Это отключит режим, обновив вашу конфигурацию.', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index cffeb9fd8b4..cf6f01dde73 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -1118,6 +1118,16 @@ export const dict = { "settings.agentBehaviour.editMode.native": "นี่คือโหมดในตัว ไม่สามารถเปลี่ยนคำจำกัดความพื้นฐานได้ แต่คุณสามารถกำหนดค่าการแทนที่ได้ที่ด้านล่าง", "settings.agentBehaviour.editMode.promptOverride": "การแทนที่ prompt แบบกำหนดเองสำหรับโหมดในตัวนี้", + "settings.agentBehaviour.badge.subagent": "เอเจนต์ย่อย", + "settings.agentBehaviour.permissions.title": "สิทธิ์ที่คำนวณแล้ว", + "settings.agentBehaviour.permissions.count": "{{count}} กฎ", + "settings.agentBehaviour.permissions.effective": "มีผล (ไวลด์การ์ด):", + "settings.agentBehaviour.permissions.col.tool": "เครื่องมือ", + "settings.agentBehaviour.permissions.col.pattern": "รูปแบบ", + "settings.agentBehaviour.permissions.col.action": "การดำเนินการ", + "settings.agentBehaviour.permissions.copy": "คัดลอกสิทธิ์เป็น JSON", + "settings.agentBehaviour.permissions.hint": + "กฎจะถูกประเมินตามลำดับ — กฎที่ตรงกันล่าสุดจะมีผล นี่คือชุดกฎที่ประมวลผลแล้วจากแบ็กเอนด์ CLI", "settings.agentBehaviour.removeMode.title": "ลบโหมด", "settings.agentBehaviour.removeMode.confirm": 'ต้องการลบโหมด "{{name}}" หรือไม่? การดำเนินการนี้จะปิดใช้งานโหมดโดยอัปเดตการกำหนดค่าของคุณ', diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index 0a0e15abd92..9f472eb53b0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -1375,5 +1375,15 @@ export const dict = { "Bu yerleşik bir moddur. Temel tanımı değiştirilemez, ancak aşağıdan geçersiz kılmaları yapılandırabilirsiniz.", "settings.agentBehaviour.editMode.prompt": "Sistem İstemi", "settings.agentBehaviour.editMode.promptOverride": "Bu yerleşik mod için özel sistem istemi geçersiz kılma", + "settings.agentBehaviour.badge.subagent": "alt ajan", + "settings.agentBehaviour.permissions.title": "Hesaplanan İzinler", + "settings.agentBehaviour.permissions.count": "{{count}} kural", + "settings.agentBehaviour.permissions.effective": "Geçerli (joker karakter):", + "settings.agentBehaviour.permissions.col.tool": "Araç", + "settings.agentBehaviour.permissions.col.pattern": "Desen", + "settings.agentBehaviour.permissions.col.action": "Eylem", + "settings.agentBehaviour.permissions.copy": "İzinleri JSON olarak kopyala", + "settings.agentBehaviour.permissions.hint": + "Kurallar sırayla değerlendirilir — son eşleşen kural kazanır. Bu, CLI arka ucundan çözümlenen kural kümesidir.", "settings.agentBehaviour.editMode.save": "Tamam", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 0eb7b024046..c922d79a42b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -1377,5 +1377,15 @@ export const dict = { "settings.agentBehaviour.editMode.prompt": "Системний запит", "settings.agentBehaviour.editMode.promptOverride": "Власне перевизначення системного запиту для цього вбудованого режиму", + "settings.agentBehaviour.badge.subagent": "субагент", + "settings.agentBehaviour.permissions.title": "Обчислені дозволи", + "settings.agentBehaviour.permissions.count": "{{count}} правил", + "settings.agentBehaviour.permissions.effective": "Ефективні (шаблон):", + "settings.agentBehaviour.permissions.col.tool": "Інструмент", + "settings.agentBehaviour.permissions.col.pattern": "Шаблон", + "settings.agentBehaviour.permissions.col.action": "Дія", + "settings.agentBehaviour.permissions.copy": "Копіювати дозволи як JSON", + "settings.agentBehaviour.permissions.hint": + "Правила оцінюються по порядку — останнє відповідне правило має пріоритет. Це розрахований набір правил з CLI бекенду.", "settings.agentBehaviour.editMode.save": "Готово", } diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 7cc9ef99319..a8ec3a8960d 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -1105,6 +1105,15 @@ export const dict = { "settings.agentBehaviour.editMode.back": "返回列表", "settings.agentBehaviour.editMode.native": "这是一个内置模式。它的基础定义无法更改,但您可以在下方配置覆盖项。", "settings.agentBehaviour.editMode.promptOverride": "此内置模式的自定义提示覆盖", + "settings.agentBehaviour.badge.subagent": "子代理", + "settings.agentBehaviour.permissions.title": "计算出的权限", + "settings.agentBehaviour.permissions.count": "{{count}} 条规则", + "settings.agentBehaviour.permissions.effective": "生效 (通配符):", + "settings.agentBehaviour.permissions.col.tool": "工具", + "settings.agentBehaviour.permissions.col.pattern": "模式", + "settings.agentBehaviour.permissions.col.action": "操作", + "settings.agentBehaviour.permissions.copy": "复制权限为 JSON", + "settings.agentBehaviour.permissions.hint": "规则按顺序评估 — 最后匹配的规则生效。这是从 CLI 后端解析出的规则集。", "settings.agentBehaviour.removeMode.title": "移除模式", "settings.agentBehaviour.removeMode.confirm": '移除模式 "{{name}}" 吗?这将通过更新配置来禁用该模式。', "settings.agentBehaviour.removeMode.button": "移除", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 71a73f5ee6a..a9cefcac819 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -1108,6 +1108,16 @@ export const dict = { "settings.agentBehaviour.editMode.back": "返回列表", "settings.agentBehaviour.editMode.native": "這是一個內建模式。它的基礎定義無法更改,但您可以在下方設定覆寫項。", "settings.agentBehaviour.editMode.promptOverride": "此內建模式的自訂提示覆寫", + "settings.agentBehaviour.badge.subagent": "子代理", + "settings.agentBehaviour.permissions.title": "已計算的權限", + "settings.agentBehaviour.permissions.count": "{{count}} 條規則", + "settings.agentBehaviour.permissions.effective": "生效(萬用字元):", + "settings.agentBehaviour.permissions.col.tool": "工具", + "settings.agentBehaviour.permissions.col.pattern": "模式", + "settings.agentBehaviour.permissions.col.action": "動作", + "settings.agentBehaviour.permissions.copy": "複製權限為 JSON", + "settings.agentBehaviour.permissions.hint": + "規則會按順序評估 — 最後符合的規則為準。這是來自 CLI 後端的已解析規則集。", "settings.agentBehaviour.removeMode.title": "移除模式", "settings.agentBehaviour.removeMode.confirm": '要移除模式 "{{name}}" 嗎?這將透過更新設定來停用該模式。', "settings.agentBehaviour.removeMode.button": "移除", diff --git a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx index 0df3bf82cdd..43eed3f123a 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/StoryProviders.tsx @@ -163,6 +163,7 @@ export function mockSessionValue(overrides?: { costBreakdown: () => [], contextUsage: () => undefined, agents: () => [{ name: "code", description: "Code mode", mode: "primary" as const }], + allAgents: () => [{ name: "code", description: "Code mode", mode: "primary" as const }], skills: () => [], refreshSkills: noop, removeSkill: noop, diff --git a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx index 5563a5dfed4..77db67c4e78 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/settings.stories.tsx @@ -63,6 +63,7 @@ export const AgentBehaviourAgents: Story = { const session = { ...mockSessionValue({ id: "agents-story", status: "idle" }), agents: () => MOCK_AGENTS, + allAgents: () => MOCK_AGENTS, removeMode: noop, removeMcp: noop, skills: () => [], @@ -87,6 +88,7 @@ export const AgentBehaviourEditCustomMode: Story = { const session = { ...mockSessionValue({ id: "edit-mode-story", status: "idle" }), agents: () => MOCK_AGENTS, + allAgents: () => MOCK_AGENTS, removeMode: noop, removeMcp: noop, skills: () => [], @@ -179,6 +181,7 @@ export const AgentBehaviourWorkflows: Story = { const session = { ...mockSessionValue({ id: "workflows-story", status: "idle" }), agents: () => MOCK_AGENTS, + allAgents: () => MOCK_AGENTS, removeMode: noop, removeMcp: noop, skills: () => [], @@ -201,6 +204,7 @@ export const AgentBehaviourWorkflowsEmpty: Story = { const session = { ...mockSessionValue({ id: "workflows-empty-story", status: "idle" }), agents: () => MOCK_AGENTS, + allAgents: () => MOCK_AGENTS, removeMode: noop, removeMcp: noop, skills: () => [], @@ -298,6 +302,7 @@ export const ModeEditExport: Story = { const session = { ...mockSessionValue({ id: "export-story", status: "idle" }), agents: () => MOCK_AGENTS, + allAgents: () => MOCK_AGENTS, removeMode: noop, removeMcp: noop, skills: () => [], diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index b894c9a36bf..7961342540f 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -213,6 +213,13 @@ export interface SlashCommandInfo { hints: string[] } +// A single resolved permission rule from the CLI backend (matches PermissionNext.Rule) +export interface PermissionRuleItem { + permission: string + pattern: string + action: PermissionLevel +} + // Agent/mode info from CLI backend export interface AgentInfo { name: string @@ -223,6 +230,7 @@ export interface AgentInfo { hidden?: boolean deprecated?: boolean color?: string + permission?: PermissionRuleItem[] } // Server info @@ -656,6 +664,7 @@ export interface ProvidersLoadedMessage { export interface AgentsLoadedMessage { type: "agentsLoaded" agents: AgentInfo[] + allAgents: AgentInfo[] defaultAgent: string } diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index f8935c321bb..a78fb92ffaf 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1044,6 +1044,7 @@ export function Prompt(props: PromptProps) { part.includes("kilocode")) +} + +function isSource(file: string) { + return SOURCE_EXTS.has(path.extname(file)) +} + +const MARKER_PREFIX = /(?:\/\/|\{?\s*\/\*)\s*kilocode_change\b/ + +function hasMarker(line: string) { + return MARKER_PREFIX.test(line) +} + +function coveredLines(text: string): Set { + const lines = text.split(/\r?\n/) + const covered = new Set() + + const first = lines.find((x) => x.trim() !== "") + if (first?.match(/(?:\/\/|\{?\s*\/\*)\s*kilocode_change\s*-\s*new\s*file\b/)) { + for (let i = 1; i <= lines.length; i++) covered.add(i) + return covered + } + + let block = false + for (let i = 0; i < lines.length; i++) { + const n = i + 1 + const line = lines[i] ?? "" + + if (line.match(/(?:\/\/|\{?\s*\/\*)\s*kilocode_change\s+start\b/)) { + block = true + covered.add(n) + continue + } + + if (line.match(/(?:\/\/|\{?\s*\/\*)\s*kilocode_change\s+end\b/)) { + covered.add(n) + block = false + continue + } + + if (block) { + covered.add(n) + continue + } + + if (hasMarker(line)) covered.add(n) + } + + return covered +} + +function checkLine(line: string, covered: Set, n: number): boolean { + const trim = line.trim() + if (!trim) return true + if (hasMarker(trim)) return true + return covered.has(n) +} + +// ─── hasMarker tests ────────────────────────────────────────────────────────── + +describe("hasMarker", () => { + const cases: Array<[string, boolean]> = [ + // JS-style inline + ["// kilocode_change", true], + [" // kilocode_change", true], + ["const x = 1 // kilocode_change", true], + ["// kilocode_change start", true], + ["// kilocode_change end", true], + ["// kilocode_change - new file", true], + ["// kilocode_change", true], + ["// kilocode_change ", true], + + // JSX-style inline + ["{/* kilocode_change */}", true], + [" {/* kilocode_change */}", true], + ["{/* kilocode_change start */}", true], + ["{/* kilocode_change end */}", true], + ["{/* kilocode_change - new file */}", true], + ["{/* kilocode_change - KiloNews added */}", true], + ["{/* kilocode_change */}", true], + ["{/* kilocode_change */}", true], + + // bare /* */ style + ["/* kilocode_change */", true], + [" /* kilocode_change */", true], + ["/* kilocode_change start */", true], + ["/* kilocode_change end */", true], + + // Non-markers + ["const x = 1", false], + ["{label}", false], + ["// some other comment", false], + ["{/* just a comment */}", false], + ["/* something else */", false], + // typo variants — should NOT match (missing word boundary) + ["// kilocode_changes", false], + ["// kilocode_changelog", false], + ["/* kilocode_change_log */", false], + ["{/* kilocode_changes */}", false], + ["// kilocode_changeable", false], + ["", false], + [" ", false], + ] + + test.each(cases)("input %j → %j", (input, expected) => { + expect(hasMarker(input)).toBe(expected) + }) +}) + +// ─── isExempt tests ─────────────────────────────────────────────────────────── + +describe("isExempt", () => { + const cases: Array<[string, boolean]> = [ + // exempt — "kilocode" in path + ["packages/opencode/src/kilocode/foo.ts", true], + ["packages/opencode/test/kilocode/bar.test.ts", true], + ["packages/opencode/src/some/kilocode/deep/path.ts", true], + ["packages/opencode/src/kilocode/deep/nested/file.tsx", true], + // exempt — "kilocode" in filename + ["packages/opencode/src/foo/kilocode.ts", true], + ["packages/opencode/src/bar/kilocode.test.ts", true], + ["packages/opencode/src/file.kilocode.ts", true], + // exempt — case-insensitive + ["packages/opencode/src/KiloCode/foo.ts", true], + ["packages/opencode/src/KILOCODE/bar.ts", true], + // NOT exempt + ["packages/opencode/src/index.ts", false], + ["packages/opencode/src/cli/cmd/tui/routes/home.tsx", false], + ["packages/opencode/src/cli/cmd/tui/routes/session/index.tsx", false], + ["packages/opencode/src/tool/registry.ts", false], + ["packages/opencode/src/config/config.ts", false], + ["packages/opencode/src/indexing/search-service.ts", false], + // kilocode_change is not the same as kilocode + ["packages/opencode/src/check-opencode-annotations.ts", false], + ] + + test.each(cases)("%j → exempt=%j", (file, expected) => { + expect(isExempt(file)).toBe(expected) + }) +}) + +// ─── isSource tests ─────────────────────────────────────────────────────────── + +describe("isSource", () => { + const cases: Array<[string, boolean]> = [ + ["foo.ts", true], + ["foo.tsx", true], + ["foo/bar.tsx", true], + ["foo.js", true], + ["foo.jsx", true], + [".json", false], + [".md", false], + [".txt", false], + ["Makefile", false], + ["foo.go", false], + ["foo.rs", false], + ] + + test.each(cases)("%j → isSource=%j", (file, expected) => { + expect(isSource(file)).toBe(expected) + }) +}) + +// ─── coveredLines tests ─────────────────────────────────────────────────────── + +describe("coveredLines", () => { + test("empty file", () => { + const covered = coveredLines("") + expect(covered.size).toBe(0) + }) + + test("file with only whitespace", () => { + const covered = coveredLines(" \n\n \n") + expect(covered.size).toBe(0) + }) + + test("whole-file JS annotation", () => { + const covered = coveredLines("// kilocode_change - new file\nexport const x = 1\nexport const y = 2") + expect(covered).toEqual(new Set([1, 2, 3])) + }) + + test("whole-file JSX annotation", () => { + const covered = coveredLines("{/* kilocode_change - new file */}\nexport const x = 1\nexport const y = 2") + expect(covered).toEqual(new Set([1, 2, 3])) + }) + + test("JS block markers", () => { + const text = [ + "const a = 1", + "// kilocode_change start", + "const b = 2", + "const c = 3", + "// kilocode_change end", + "const d = 4", + ].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([2, 3, 4, 5])) // block markers + content + }) + + test("JSX block markers", () => { + const text = [ + "const a = 1", + "{/* kilocode_change start */}", + "const b = 2", + "const c = 3", + "{/* kilocode_change end */}", + "const d = 4", + ].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([2, 3, 4, 5])) + }) + + test("mixed JS and JSX block markers (nested)", () => { + const text = [ + "// kilocode_change start", + "{/* kilocode_change start */}", + "const b = 2", + "{/* kilocode_change end */}", + "// kilocode_change end", + ].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1, 2, 3, 4, 5])) + }) + + test("bare /* */ block markers", () => { + const text = ["/* kilocode_change start */", "const b = 2", "/* kilocode_change end */"].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1, 2, 3])) + }) + + test("inline JS marker covers only that line", () => { + const text = ["const a = 1", "const b = 2 // kilocode_change", "const c = 3"].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([2])) + }) + + test("inline JSX marker covers only that line", () => { + const text = ["const a = 1", "{/* kilocode_change */}", "const c = 3"].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([2])) + }) + + test("inline JS marker with code on same line", () => { + const text = "const url = Flag.KILO_MODELS_URL || 'https://models.dev' // kilocode_change\n" + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1])) + }) + + test("JSX block marker with descriptive suffix", () => { + const text = [ + "{/* kilocode_change start - Kilo-specific error display */}", + "", + "{/* kilocode_change end */}", + ].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1, 2, 3])) + }) + + test("multiple independent blocks", () => { + const text = [ + "// kilocode_change start", + "const a = 1", + "// kilocode_change end", + "const b = 2", + "{/* kilocode_change start */}", + "const c = 3", + "{/* kilocode_change end */}", + "const d = 4", + ].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1, 2, 3, 5, 6, 7])) + }) + + test("marker line with extra text after marker is still covered", () => { + const text = [ + "const a = 1", + "// kilocode_change start - this is kilo specific", + "const b = 2", + "// kilocode_change end", + ].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([2, 3, 4])) + }) + + test("nested block — inner block ends, outer continues", () => { + const text = [ + "// kilocode_change start", + "{/* kilocode_change start */}", + "const b = 2", + "{/* kilocode_change end */}", + "const c = 3", + "// kilocode_change end", + ].join("\n") + const covered = coveredLines(text) + // Line 1: start, block=true + // Line 2: inner start, block=true (covered by block) + // Line 3: covered by block + // Line 4: inner end, block=false, covered by end marker + // Line 5: NOT covered (block is false, no inline marker) + // Line 6: outer end, block already false, covered by end marker + expect(covered).toEqual(new Set([1, 2, 3, 4, 6])) + }) + + test("whitespace before marker is handled", () => { + const text = [" {/* kilocode_change start */}", " const b = 2", " {/* kilocode_change end */}"].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1, 2, 3])) + }) +}) + +// ─── checkLine integration tests ────────────────────────────────────────────── +// Simulates what the main loop does for each added line + +describe("checkLine (main loop simulation)", () => { + function check(text: string, addedLines: number[]): string[] { + const covered = coveredLines(text) + const lines = text.split(/\r?\n/) + const violations: string[] = [] + for (const n of addedLines) { + const line = lines[n - 1] ?? "" + const trim = line.trim() + if (!trim) continue + if (hasMarker(trim)) continue + if (!covered.has(n)) violations.push(`line ${n}: ${trim}`) + } + return violations + } + + test("covered line reports no violation", () => { + const text = ["// kilocode_change start", "const kilo = 1", "// kilocode_change end"].join("\n") + expect(check(text, [2])).toEqual([]) + }) + + test("uncovered line reports violation", () => { + const text = ["const uncovered = 1", "const also_uncovered = 2"].join("\n") + expect(check(text, [1, 2])).toEqual(["line 1: const uncovered = 1", "line 2: const also_uncovered = 2"]) + }) + + test("empty lines are skipped", () => { + const text = ["const x = 1", "", " ", "", "const y = 2"].join("\n") + expect(check(text, [1, 2, 3, 4, 5])).toEqual(["line 1: const x = 1", "line 5: const y = 2"]) + }) + + test("marker lines are skipped even if uncovered", () => { + // This shouldn't normally happen, but the loop should skip it + const text = ["{/* kilocode_change */}", "{/* kilocode_change start */}"].join("\n") + expect(check(text, [1, 2])).toEqual([]) + }) + + test("real-world TSX home.tsx pattern", () => { + const text = [ + '', + " {/* kilocode_change start */}", + " ", + " {indexingLabel()}", + " ", + " {/* kilocode_change end */}", + "", + ].join("\n") + // Only the first and last lines (opening/closing box) should be uncovered + expect(check(text, [1, 7])).toEqual([`line 1: `, `line 7: `]) + // Middle lines are covered + expect(check(text, [2, 3, 4, 5, 6])).toEqual([]) + }) + + test("real-world TSX session index.tsx pattern", () => { + const text = [ + "const foo = 1", + "{/* kilocode_change start */}", + '', + "", + "", + "{/* kilocode_change end */}", + "const bar = 2", + ].join("\n") + // Lines 1 and 7 are uncovered (not in any block) + expect(check(text, [1, 7])).toEqual(["line 1: const foo = 1", "line 7: const bar = 2"]) + // Lines 2-6 are covered + expect(check(text, [2, 3, 4, 5, 6])).toEqual([]) + }) + + test("real-world TSX sidebar.tsx pattern", () => { + const text = [ + "", + " {/* kilocode_change start */}", + " ", + " {/* kilocode_change end */}", + "", + " {/* kilocode_change start */}", + " other content", + " {/* kilocode_change end */}", + ].join("\n") + expect(check(text, [1, 5])).toEqual(["line 1: ", "line 5: "]) + expect(check(text, [2, 3, 4, 6, 7, 8])).toEqual([]) + }) + + test("real-world TSX permission.tsx inline pattern", () => { + const text = [ + "{/* kilocode_change */}", + "", + "{/* kilocode_change */}", + "", + ].join("\n") + expect(check(text, [2, 4])).toEqual(["line 2: ", "line 4: "]) + expect(check(text, [1, 3])).toEqual([]) + }) + + test("JS-style session/index.tsx pattern (from existing codebase)", () => { + const text = ["const foo = 1", "", "{/* kilocode_change */}", "", ""].join("\n") + // Line 2 () is NOT covered — it's between and the marker + expect(check(text, [2, 4])).toEqual(["line 2: ", "line 4: "]) + expect(check(text, [3])).toEqual([]) + }) + + test("whole-file annotated file — no violations even for unmarked lines", () => { + const text = [ + "// kilocode_change - new file", + "export const kiloFeature = true", + "export const alsoKilo = 123", + "export const notMarked = 'oops'", + ].join("\n") + expect(check(text, [2, 3, 4])).toEqual([]) + }) +}) + +// ─── Regex edge cases ───────────────────────────────────────────────────────── + +describe("MARKER_PREFIX regex edge cases", () => { + test("handles { followed immediately by /*", () => { + expect(hasMarker("{/* kilocode_change */}")).toBe(true) + }) + + test("handles { followed by whitespace then /*", () => { + expect(hasMarker("{ /* kilocode_change */}")).toBe(true) + }) + + test("handles just /* with no brace", () => { + expect(hasMarker("/* kilocode_change */")).toBe(true) + }) + + test("handles // with no spaces", () => { + expect(hasMarker("//kilocode_change")).toBe(true) + }) + + test("handles // with lots of spaces", () => { + expect(hasMarker("// kilocode_change")).toBe(true) + }) + + test("does not match {/* without kilocode_change", () => { + expect(hasMarker("{/* some other comment */}")).toBe(false) + }) + + test("does not match /* without kilocode_change", () => { + expect(hasMarker("/* just a comment */")).toBe(false) + }) + + test("does not match kilocode_changes (word boundary)", () => { + expect(hasMarker("// kilocode_changes")).toBe(false) + expect(hasMarker("// kilocode_changelog")).toBe(false) + expect(hasMarker("{/* kilocode_changes */}")).toBe(false) + expect(hasMarker("// kilocode_changeable")).toBe(false) + }) +}) + +// ─── isExempt — Windows paths ───────────────────────────────────────────────── + +describe("isExempt — Windows backslash paths", () => { + test("Windows paths with backslashes", () => { + expect(isExempt("packages\\opencode\\src\\kilocode\\foo.ts")).toBe(true) + expect(isExempt("packages\\opencode\\test\\kilocode\\bar.test.ts")).toBe(true) + expect(isExempt("packages\\opencode\\src\\index.ts")).toBe(false) + }) +}) + +// ─── coveredLines — additional patterns ─────────────────────────────────────── + +describe("coveredLines — additional patterns", () => { + test("block with descriptive suffix is still recognized", () => { + const text = [ + "{/* kilocode_change start - Kilo-specific indexing display */}", + "", + "{/* kilocode_change end */}", + ].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1, 2, 3])) + }) + + test("empty file content", () => { + const covered = coveredLines("// kilocode_change start\n \n// kilocode_change end") + expect(covered).toEqual(new Set([1, 2, 3])) + }) + + test("multiple separate JS inline markers", () => { + const text = [ + "const a = 1 // kilocode_change", + "const b = 2", + "const c = 3 // kilocode_change", + "const d = 4", + ].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1, 3])) + }) + + test("consecutive block markers (no content)", () => { + const text = ["// kilocode_change start", "// kilocode_change end"].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1, 2])) + }) + + test("block immediately followed by another start", () => { + const text = [ + "// kilocode_change start", + "const a = 1", + "// kilocode_change end", + "{/* kilocode_change start */}", + "const b = 2", + "{/* kilocode_change end */}", + ].join("\n") + const covered = coveredLines(text) + expect(covered).toEqual(new Set([1, 2, 3, 4, 5, 6])) + }) + + test("trailing empty line after block end is not covered", () => { + const text = "// kilocode_change start\nconst a = 1\n// kilocode_change end\n\n" + const covered = coveredLines(text) + // Block ends at line 3; trailing empty line 4 is outside the block + expect(covered).toEqual(new Set([1, 2, 3])) + }) +}) + +// ─── checkLine — additional patterns ───────────────────────────────────────── + +describe("checkLine — additional patterns", () => { + function check(text: string, addedLines: number[]): string[] { + const covered = coveredLines(text) + const lines = text.split(/\r?\n/) + const violations: string[] = [] + for (const n of addedLines) { + const line = lines[n - 1] ?? "" + const trim = line.trim() + if (!trim) continue + if (hasMarker(trim)) continue + if (!covered.has(n)) violations.push(`line ${n}: ${trim}`) + } + return violations + } + + test("real-world dialog-status.tsx pattern — multiple inline blocks", () => { + // Based on actual file: packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx + const text = [ + "{/* kilocode_change start */}", + "", + "{/* kilocode_change end */}", + "const normal = 1", + " {/* kilocode_change start */}", + " ", + " {/* kilocode_change end */}", + ].join("\n") + // Lines 4 is uncovered + expect(check(text, [4])).toEqual(["line 4: const normal = 1"]) + // Lines 1-3 and 5-7 are covered + expect(check(text, [1, 2, 3, 5, 6, 7])).toEqual([]) + }) + + test("real-world TUI routes — line between marker and code should be uncovered", () => { + // A common mistake: putting code on a different line from the marker + const text = ["{/* kilocode_change start */}", "", "", "", "{/* kilocode_change end */}"].join("\n") + // Empty lines (2, 4) are skipped + expect(check(text, [3])).toEqual([]) + // All non-empty lines (1, 3, 5) are covered + expect(check(text, [1, 3, 5])).toEqual([]) + }) + + test("end marker on same line as content is covered", () => { + const text = "const a = 1\n{/* kilocode_change end */} // block already closed, still covered\n" + const covered = coveredLines(text) + expect(covered).toEqual(new Set([2])) + }) + + test("end marker closes block correctly", () => { + const text = [ + "// kilocode_change start", + "const a = 1", + "// kilocode_change end", + "const b = 2", // uncovered + ].join("\n") + expect(check(text, [1, 2, 3, 4])).toEqual(["line 4: const b = 2"]) + }) +}) diff --git a/script/check-opencode-annotations.ts b/script/check-opencode-annotations.ts index 314b399d460..175f2b2159c 100644 --- a/script/check-opencode-annotations.ts +++ b/script/check-opencode-annotations.ts @@ -9,12 +9,14 @@ * bun run script/check-opencode-annotations.ts --base # diff against * * A line is "covered" if it: - * - contains // kilocode_change (inline annotation) - * - falls inside a // kilocode_change start/end block (block annotation) - * - is in a file whose first non-empty line is (whole-file annotation) + * - contains a kilocode_change marker comment (inline annotation) + * - falls inside a kilocode_change start/end block (block annotation) + * - is in a file whose first non-empty line is (whole-file annotation) * // kilocode_change - new file - * - is empty / whitespace-only (skipped) - * - is itself a marker line (auto-covered) + * - is empty / whitespace-only (skipped) + * - is itself a marker line (auto-covered) + * + * Both JS (//) and JSX ({/ * ... * /}) comment styles are recognized. * * Exempt paths (no markers needed — entirely Kilo-specific): * - packages/opencode/src/kilocode/** @@ -65,13 +67,20 @@ function addedLines(file: string): Set { return out } +// Matches the start of a kilocode_change marker in both JS (//) and JSX ({/* */}) comments +const MARKER_PREFIX = /(?:\/\/|\{?\s*\/\*)\s*kilocode_change\b/ + +function hasMarker(line: string) { + return MARKER_PREFIX.test(line) +} + function coveredLines(text: string): { lines: string[]; covered: Set } { const lines = text.split(/\r?\n/) const covered = new Set() // Whole-file annotation: first non-empty line is "// kilocode_change - new file" const first = lines.find((x) => x.trim() !== "") - if (first?.match(/\/\/\s*kilocode_change\s*-\s*new\s*file\b/)) { + if (first?.match(/(?:\/\/|\{?\s*\/\*)\s*kilocode_change\s*-\s*new\s*file\b/)) { for (let i = 1; i <= lines.length; i++) covered.add(i) return { lines, covered } } @@ -81,13 +90,13 @@ function coveredLines(text: string): { lines: string[]; covered: Set } { const n = i + 1 const line = lines[i] ?? "" - if (line.match(/\/\/\s*kilocode_change\s+start\b/)) { + if (line.match(/(?:\/\/|\{?\s*\/\*)\s*kilocode_change\s+start\b/)) { block = true covered.add(n) continue } - if (line.match(/\/\/\s*kilocode_change\s+end\b/)) { + if (line.match(/(?:\/\/|\{?\s*\/\*)\s*kilocode_change\s+end\b/)) { covered.add(n) block = false continue @@ -98,7 +107,7 @@ function coveredLines(text: string): { lines: string[]; covered: Set } { continue } - if (line.match(/\/\/\s*kilocode_change\b/)) covered.add(n) + if (hasMarker(line)) covered.add(n) } return { lines, covered } @@ -127,7 +136,7 @@ for (const file of files) { const line = lines[n - 1] ?? "" const trim = line.trim() if (!trim) continue - if (trim.match(/\/\/\s*kilocode_change\b/)) continue + if (hasMarker(trim)) continue if (!covered.has(n)) violations.push(` ${file}:${n}: ${trim}`) } } @@ -153,6 +162,12 @@ console.error( " ...", " // kilocode_change end", "", + "JSX/TSX (inside JSX templates):", + " {/* kilocode_change */}", + " {/* kilocode_change start */}", + " ...", + " {/* kilocode_change end */}", + "", "New file:", " // kilocode_change - new file", "",