mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
Merge branch 'main' into feat/allow-everything
This commit is contained in:
@@ -220,6 +220,21 @@ const bar = 2
|
||||
// kilocode_change - new file
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
**JSX/TSX (inside JSX templates):**
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```tsx
|
||||
{/* kilocode_change */}
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```tsx
|
||||
{/* kilocode_change start */}
|
||||
<MyComponent />
|
||||
{/* kilocode_change end */}
|
||||
```
|
||||
|
||||
#### When markers are NOT needed
|
||||
|
||||
Code in these paths is Kilo Code-specific and does NOT need `kilocode_change` markers:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export default [
|
||||
{
|
||||
files: ["src/KiloProvider.ts"],
|
||||
rules: {
|
||||
"max-lines": ["error", 3200],
|
||||
"max-lines": ["error", 3300],
|
||||
},
|
||||
},
|
||||
eslintConfigPrettier,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<void> {
|
||||
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. */
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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<SelectOption[]>(() => [
|
||||
{ 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<SelectOption[]>(() => {
|
||||
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 = () => {
|
||||
<Card style={{ "margin-bottom": "12px" }}>
|
||||
<For each={agentNames()}>
|
||||
{(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
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={agent()?.mode === "subagent"}>
|
||||
<span
|
||||
style={{
|
||||
"font-size": "10px",
|
||||
padding: "1px 5px",
|
||||
"border-radius": "3px",
|
||||
background: "var(--bg-subtle-base, var(--vscode-badge-background))",
|
||||
color: "var(--text-weak-base, var(--vscode-badge-foreground))",
|
||||
}}
|
||||
>
|
||||
{language.t("settings.agentBehaviour.badge.subagent")}
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={hidden()}>
|
||||
<span
|
||||
style={{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Show, createMemo } from "solid-js"
|
||||
import { Component, Show, For, createMemo, createSignal } from "solid-js"
|
||||
import { TextField } from "@kilocode/kilo-ui/text-field"
|
||||
import { Switch } from "@kilocode/kilo-ui/switch"
|
||||
import { Card } from "@kilocode/kilo-ui/card"
|
||||
@@ -8,7 +8,7 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button"
|
||||
import { useConfig } from "../../context/config"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import type { AgentConfig, AgentInfo } from "../../types/messages"
|
||||
import type { AgentConfig, AgentInfo, PermissionRuleItem } from "../../types/messages"
|
||||
import SettingsRow from "./SettingsRow"
|
||||
import { buildExport } from "./mode-io"
|
||||
|
||||
@@ -26,8 +26,9 @@ const ModeEditView: Component<Props> = (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<AgentConfig>(() => config().agent?.[props.name] ?? {})
|
||||
|
||||
@@ -230,6 +231,18 @@ const ModeEditView: Component<Props> = (props) => {
|
||||
</SettingsRow>
|
||||
</Card>
|
||||
|
||||
{/* Calculated permissions (read-only, collapsible) */}
|
||||
<Show when={agent()?.permission} keyed>
|
||||
{(rules) => (
|
||||
<PermissionRuleset
|
||||
agent={props.name}
|
||||
rules={rules}
|
||||
expanded={expanded()}
|
||||
onToggle={() => setExpanded((v) => !v)}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div style={{ display: "flex", "justify-content": "flex-end" }}>
|
||||
<Button variant="ghost" onClick={props.onBack}>
|
||||
{language.t("settings.agentBehaviour.editMode.back")}
|
||||
@@ -239,4 +252,201 @@ const ModeEditView: Component<Props> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Collapsible permissions ruleset display
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const ACTION_COLORS: Record<string, { bg: string; fg: string }> = {
|
||||
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<RulesetProps> = (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<string, PermissionRuleItem["action"]>()
|
||||
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 (
|
||||
<Card style={{ "margin-bottom": "12px" }}>
|
||||
<div
|
||||
style={{ display: "flex", "align-items": "center", cursor: "pointer", "user-select": "none" }}
|
||||
onClick={props.onToggle}
|
||||
>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="ghost"
|
||||
icon={props.expanded ? "chevron-down" : "chevron-right"}
|
||||
onClick={(e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
props.onToggle()
|
||||
}}
|
||||
/>
|
||||
<span data-slot="settings-row-label-title" style={{ "margin-left": "4px" }}>
|
||||
{language.t("settings.agentBehaviour.permissions.title")}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
"margin-left": "8px",
|
||||
"font-size": "11px",
|
||||
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
|
||||
}}
|
||||
>
|
||||
{language.t("settings.agentBehaviour.permissions.count", { count: String(props.rules.length) })}
|
||||
</span>
|
||||
<div style={{ "margin-left": "auto" }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
variant="ghost"
|
||||
icon={copied() ? "check" : "copy"}
|
||||
title={language.t("settings.agentBehaviour.permissions.copy")}
|
||||
onClick={copy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={props.expanded}>
|
||||
{/* Summary: effective action per tool for wildcard pattern */}
|
||||
<Show when={summary().length > 0}>
|
||||
<div style={{ "margin-top": "8px", "margin-bottom": "8px" }}>
|
||||
<div
|
||||
style={{
|
||||
"font-size": "11px",
|
||||
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
|
||||
"margin-bottom": "4px",
|
||||
}}
|
||||
>
|
||||
{language.t("settings.agentBehaviour.permissions.effective")}
|
||||
</div>
|
||||
<div style={{ display: "flex", "flex-wrap": "wrap", gap: "4px" }}>
|
||||
<For each={summary()}>
|
||||
{([tool, action]) => {
|
||||
const colors = ACTION_COLORS[action] ?? ACTION_COLORS.unknown
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
"font-size": "11px",
|
||||
padding: "2px 6px",
|
||||
"border-radius": "3px",
|
||||
background: colors.bg,
|
||||
color: colors.fg,
|
||||
"font-family": "var(--vscode-editor-font-family, monospace)",
|
||||
}}
|
||||
>
|
||||
{tool}: {action}
|
||||
</span>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Full ruleset table */}
|
||||
<div
|
||||
style={{
|
||||
"margin-top": "8px",
|
||||
"font-size": "11px",
|
||||
"font-family": "var(--vscode-editor-font-family, monospace)",
|
||||
"max-height": "300px",
|
||||
"overflow-y": "auto",
|
||||
border: "1px solid var(--border-weak-base, var(--vscode-panel-border))",
|
||||
"border-radius": "4px",
|
||||
}}
|
||||
>
|
||||
<table style={{ width: "100%", "border-collapse": "collapse" }}>
|
||||
<thead>
|
||||
<tr
|
||||
style={{
|
||||
background: "var(--bg-subtle-base, var(--vscode-editorWidget-background))",
|
||||
position: "sticky",
|
||||
top: "0",
|
||||
}}
|
||||
>
|
||||
<th style={{ padding: "4px 8px", "text-align": "left", "font-weight": "600" }}>
|
||||
{language.t("settings.agentBehaviour.permissions.col.tool")}
|
||||
</th>
|
||||
<th style={{ padding: "4px 8px", "text-align": "left", "font-weight": "600" }}>
|
||||
{language.t("settings.agentBehaviour.permissions.col.pattern")}
|
||||
</th>
|
||||
<th style={{ padding: "4px 8px", "text-align": "left", "font-weight": "600" }}>
|
||||
{language.t("settings.agentBehaviour.permissions.col.action")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<For each={props.rules}>
|
||||
{(rule, idx) => {
|
||||
const colors = ACTION_COLORS[rule.action] ?? ACTION_COLORS.unknown
|
||||
return (
|
||||
<tr
|
||||
style={{
|
||||
"border-top":
|
||||
idx() > 0 ? "1px solid var(--border-weak-base, var(--vscode-panel-border))" : "none",
|
||||
}}
|
||||
>
|
||||
<td style={{ padding: "3px 8px" }}>{rule.permission}</td>
|
||||
<td style={{ padding: "3px 8px", color: "var(--text-weak-base)" }}>{rule.pattern}</td>
|
||||
<td style={{ padding: "3px 8px" }}>
|
||||
<span
|
||||
style={{
|
||||
padding: "1px 4px",
|
||||
"border-radius": "2px",
|
||||
background: colors.bg,
|
||||
color: colors.fg,
|
||||
}}
|
||||
>
|
||||
{rule.action}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
"margin-top": "6px",
|
||||
"font-size": "10px",
|
||||
color: "var(--text-weak-base, var(--vscode-descriptionForeground))",
|
||||
}}
|
||||
>
|
||||
{language.t("settings.agentBehaviour.permissions.hint")}
|
||||
</div>
|
||||
</Show>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModeEditView
|
||||
|
||||
@@ -134,6 +134,7 @@ interface SessionContextValue {
|
||||
|
||||
// Agent/mode selection (per-session)
|
||||
agents: Accessor<AgentInfo[]>
|
||||
allAgents: Accessor<AgentInfo[]>
|
||||
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<AgentInfo[]>([])
|
||||
const [allAgents, setAllAgents] = createSignal<AgentInfo[]>([])
|
||||
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,
|
||||
|
||||
+10
@@ -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}}"؟ سيؤدي هذا إلى تعطيل الوضع عن طريق تحديث الإعدادات.',
|
||||
|
||||
+10
@@ -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.',
|
||||
|
||||
+10
@@ -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.',
|
||||
|
||||
+10
@@ -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.',
|
||||
|
||||
+10
@@ -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.',
|
||||
|
||||
@@ -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.",
|
||||
|
||||
+10
@@ -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.',
|
||||
|
||||
+10
@@ -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.',
|
||||
|
||||
+10
@@ -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}}" を削除しますか?設定を更新してモードを無効にします。',
|
||||
|
||||
+10
@@ -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}}"을(를) 제거하시겠습니까? 구성을 업데이트하여 모드를 비활성화합니다.',
|
||||
|
||||
+10
@@ -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",
|
||||
}
|
||||
|
||||
+10
@@ -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.',
|
||||
|
||||
+10
@@ -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.',
|
||||
|
||||
+10
@@ -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}}"? Это отключит режим, обновив вашу конфигурацию.',
|
||||
|
||||
+10
@@ -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}}" หรือไม่? การดำเนินการนี้จะปิดใช้งานโหมดโดยอัปเดตการกำหนดค่าของคุณ',
|
||||
|
||||
+10
@@ -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",
|
||||
}
|
||||
|
||||
+10
@@ -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": "Готово",
|
||||
}
|
||||
|
||||
+9
@@ -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": "移除",
|
||||
|
||||
+10
@@ -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": "移除",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: () => [],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1044,6 +1044,7 @@ export function Prompt(props: PromptProps) {
|
||||
</box>
|
||||
<box
|
||||
height={1}
|
||||
flexShrink={0} // kilocode_change - prevent border box from shrinking in narrow terminals (#6309)
|
||||
border={["left"]}
|
||||
borderColor={highlight()}
|
||||
customBorderChars={{
|
||||
|
||||
@@ -152,7 +152,7 @@ export namespace ToolRegistry {
|
||||
const usePatch =
|
||||
model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4")
|
||||
if (t.id === "apply_patch") return usePatch
|
||||
if (t.id === "edit" || t.id === "write") return !usePatch
|
||||
if (t.id === "edit") return !usePatch
|
||||
|
||||
return true
|
||||
})
|
||||
@@ -174,4 +174,4 @@ export namespace ToolRegistry {
|
||||
)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"version": "7.2.0",
|
||||
"scripts": {
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {},
|
||||
"peerDependencies": {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,596 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "node:path"
|
||||
|
||||
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"])
|
||||
|
||||
function isExempt(file: string) {
|
||||
const norm = file.replaceAll("\\", "/").toLowerCase()
|
||||
return norm.split("/").some((part) => 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<number> {
|
||||
const lines = text.split(/\r?\n/)
|
||||
const covered = new Set<number>()
|
||||
|
||||
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<number>, 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],
|
||||
["<text fg={color}>{label}</text>", 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 */}",
|
||||
"<ErrorDisplay />",
|
||||
"{/* 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 = [
|
||||
'<box width="100%" maxWidth={75}>',
|
||||
" {/* kilocode_change start */}",
|
||||
" <Show when={indexingOn()}>",
|
||||
" <text fg={indexingColor()}>{indexingLabel()}</text>",
|
||||
" </Show>",
|
||||
" {/* kilocode_change end */}",
|
||||
"</box>",
|
||||
].join("\n")
|
||||
// Only the first and last lines (opening/closing box) should be uncovered
|
||||
expect(check(text, [1, 7])).toEqual([`line 1: <box width="100%" maxWidth={75}>`, `line 7: </box>`])
|
||||
// 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 */}",
|
||||
'<Match when={props.part.tool === "semantic_search"}>',
|
||||
"<SemanticSearch {...toolprops} />",
|
||||
"</Match>",
|
||||
"{/* 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 = [
|
||||
"<box>",
|
||||
" {/* kilocode_change start */}",
|
||||
" <SessionTree />",
|
||||
" {/* kilocode_change end */}",
|
||||
"</box>",
|
||||
" {/* kilocode_change start */}",
|
||||
" <div>other content</div>",
|
||||
" {/* kilocode_change end */}",
|
||||
].join("\n")
|
||||
expect(check(text, [1, 5])).toEqual(["line 1: <box>", "line 5: </box>"])
|
||||
expect(check(text, [2, 3, 4, 6, 7, 8])).toEqual([])
|
||||
})
|
||||
|
||||
test("real-world TSX permission.tsx inline pattern", () => {
|
||||
const text = [
|
||||
"{/* kilocode_change */}",
|
||||
"<PermissionDeniedCard />",
|
||||
"{/* kilocode_change */}",
|
||||
"<AnotherKiloComponent />",
|
||||
].join("\n")
|
||||
expect(check(text, [2, 4])).toEqual(["line 2: <PermissionDeniedCard />", "line 4: <AnotherKiloComponent />"])
|
||||
expect(check(text, [1, 3])).toEqual([])
|
||||
})
|
||||
|
||||
test("JS-style session/index.tsx pattern (from existing codebase)", () => {
|
||||
const text = ["const foo = 1", "<Toast />", "{/* kilocode_change */}", "<Footer />", "</box>"].join("\n")
|
||||
// Line 2 (<Toast />) is NOT covered — it's between <Toast /> and the marker
|
||||
expect(check(text, [2, 4])).toEqual(["line 2: <Toast />", "line 4: <Footer />"])
|
||||
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 */}",
|
||||
"<IndexingStatus />",
|
||||
"{/* 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 */}",
|
||||
"<KiloDialog>",
|
||||
"{/* kilocode_change end */}",
|
||||
"const normal = 1",
|
||||
" {/* kilocode_change start */}",
|
||||
" <KiloDialog />",
|
||||
" {/* 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 */}", "", "<KiloIndexing />", "", "{/* 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"])
|
||||
})
|
||||
})
|
||||
@@ -9,12 +9,14 @@
|
||||
* bun run script/check-opencode-annotations.ts --base <ref> # diff against <ref>
|
||||
*
|
||||
* 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<number> {
|
||||
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<number> } {
|
||||
const lines = text.split(/\r?\n/)
|
||||
const covered = new Set<number>()
|
||||
|
||||
// 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<number> } {
|
||||
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<number> } {
|
||||
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",
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user