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)} + /> + )} + +