diff --git a/.changeset/agent-manager-collapsible-sidebar.md b/.changeset/agent-manager-collapsible-sidebar.md new file mode 100644 index 00000000000..921f175b225 --- /dev/null +++ b/.changeset/agent-manager-collapsible-sidebar.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add a collapsible sidebar in the Agent Manager. The toggle button sits left of the tab title and the collapsed state persists across reloads and restarts. Starting a new session or worktree automatically reopens the sidebar so the new entry is visible. diff --git a/.changeset/hide-subagent-sessions.md b/.changeset/hide-subagent-sessions.md new file mode 100644 index 00000000000..eb7a95948e5 --- /dev/null +++ b/.changeset/hide-subagent-sessions.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Hide Task tool subagent sessions from Agent Manager session lists. diff --git a/.changeset/local-worktree-revert-diff.md b/.changeset/local-worktree-revert-diff.md new file mode 100644 index 00000000000..26c094336b3 --- /dev/null +++ b/.changeset/local-worktree-revert-diff.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Move worktree file revert diff status checks into the VS Code extension host so Kilo no longer asks the CLI server to run git diff commands through Bun during workspace and Agent Manager reverts. diff --git a/.changeset/model-picker-search-focus.md b/.changeset/model-picker-search-focus.md new file mode 100644 index 00000000000..33e7b6a635f --- /dev/null +++ b/.changeset/model-picker-search-focus.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep the New Worktree model picker open when interacting with its search field. diff --git a/.changeset/persist-chat-tool-cards.md b/.changeset/persist-chat-tool-cards.md new file mode 100644 index 00000000000..4e88b114edf --- /dev/null +++ b/.changeset/persist-chat-tool-cards.md @@ -0,0 +1,6 @@ +--- +"kilo-code": patch +"@kilocode/kilo-ui": patch +--- + +Keep chat tool cards open or closed when streaming updates remount them. diff --git a/.changeset/shell-syntax-highlighting.md b/.changeset/shell-syntax-highlighting.md new file mode 100644 index 00000000000..038f51dd685 --- /dev/null +++ b/.changeset/shell-syntax-highlighting.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add syntax highlighting to shell command output with Shiki, labeled "Command" and "Output" sections, per-section copy buttons, and an "Open in Editor" action that opens the full untruncated output in a VS Code editor tab. diff --git a/.changeset/terminals-split-diff-sidebar.md b/.changeset/terminals-split-diff-sidebar.md new file mode 100644 index 00000000000..8474c11fc35 --- /dev/null +++ b/.changeset/terminals-split-diff-sidebar.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Support viewing Agent Manager terminal tabs alongside the right diff sidebar, and send review comments to the active terminal. diff --git a/.kilo/agent/upstream-merge.md b/.kilo/agent/upstream-merge.md index d8aff1fd6c0..e81c9cea361 100644 --- a/.kilo/agent/upstream-merge.md +++ b/.kilo/agent/upstream-merge.md @@ -44,6 +44,12 @@ permission: Resolve the manual part of an upstream merge. +**Do not load the `kilocode-merge-minimizer` skill.** That skill is for +authoring new Kilo changes against shared upstream files; during an upstream +merge it gives the wrong guidance (it nudges toward extracting Kilo logic out +of conflict regions, which is exactly the opposite of what merge resolution +needs). Follow the rules in this agent file instead. + The user will provide the upstream version (for example `v1.1.50` or `1.1.50`) in their first message. If they don't, infer it from the current branch name, from `upstream-merge-report-.md`, or from the newest relevant report diff --git a/packages/kilo-docs/components/BrowserFrame/index.tsx b/packages/kilo-docs/components/BrowserFrame/index.tsx index bc96def1cf3..d1a80ae074b 100644 --- a/packages/kilo-docs/components/BrowserFrame/index.tsx +++ b/packages/kilo-docs/components/BrowserFrame/index.tsx @@ -87,7 +87,23 @@ export function BrowserFrame({ )} {/* Content */} -
{children}
+ {/* + * The `browser-frame-content` class strips the default vertical margin + * applied by descendant `Image` figures (1.5rem top/bottom). The + * BrowserFrame already provides its own outer spacing via the `figure` + * wrapper, and the inner image should sit flush against the chrome. + */} +
+ {children} +
+ {caption && (
({ + padding: "10px 18px", + borderRadius: "8px", + fontSize: "12px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 600, + textAlign: "center" as const, + minWidth: "140px", + background: `rgba(${rgb},0.1)`, + border: `1px solid rgba(${rgb},0.5)`, + color, +}) + +const gt = laneStyle("59,130,246", "#3b82f6") +const wl = laneStyle("248,160,32", "#f8a020") +const val = laneStyle("167,139,250", "#a78bfa") +const rep = laneStyle("34,197,94", "#22c55e") + +const nodes: Node[] = [ + { + id: "browse", + position: { x: 0, y: 0 }, + data: { label: "Mayor browse" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: gt, + }, + { + id: "wl-return", + position: { x: 250, y: 0 }, + data: { label: "Wasteland returns\nwanted items" }, + sourcePosition: "bottom" as Position, + targetPosition: "left" as Position, + style: wl, + }, + { + id: "claim", + position: { x: 0, y: 100 }, + data: { label: "Mayor claim" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: gt, + }, + { + id: "wl-lock", + position: { x: 250, y: 100 }, + data: { label: "Wasteland locks\nitem → DoltHub PR" }, + sourcePosition: "bottom" as Position, + targetPosition: "left" as Position, + style: wl, + }, + { + id: "work", + position: { x: 0, y: 210 }, + data: { label: "Polecats work\n→ push branch" }, + sourcePosition: "right" as Position, + targetPosition: "top" as Position, + style: gt, + }, + { + id: "done", + position: { x: 0, y: 320 }, + data: { label: "Mayor done\n(evidence)" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: gt, + }, + { + id: "wl-attach", + position: { x: 250, y: 320 }, + data: { label: "Wasteland attaches\nevidence to PR" }, + sourcePosition: "bottom" as Position, + targetPosition: "left" as Position, + style: wl, + }, + { + id: "review", + position: { x: 500, y: 320 }, + data: { label: "Validator\nreviews PR" }, + sourcePosition: "bottom" as Position, + targetPosition: "left" as Position, + style: val, + }, + { + id: "stamp", + position: { x: 500, y: 450 }, + data: { label: "Stamps:\nquality · reliability\n· creativity" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: val, + }, + { + id: "reputation", + position: { x: 250, y: 560 }, + data: { label: "Reputation ledger\nupdates" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: rep, + }, + { + id: "identity", + position: { x: 0, y: 560 }, + data: { label: "Portable\nidentity" }, + sourcePosition: "top" as Position, + targetPosition: "right" as Position, + style: rep, + }, +] + +const labelStyle = { fontSize: 10, fontFamily: "'JetBrains Mono', monospace", fill: "#888" } + +const edges: Edge[] = [ + { + id: "browse-return", + source: "browse", + target: "wl-return", + type: "smoothstep", + animated: true, + label: "1 browse", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "claim-lock", + source: "claim", + target: "wl-lock", + type: "smoothstep", + animated: true, + label: "2 claim", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "lock-work", + source: "wl-lock", + target: "work", + type: "smoothstep", + label: "3 work", + labelStyle, + style: { strokeWidth: 2, stroke: "#f8a020" }, + }, + { + id: "work-done", + source: "work", + target: "done", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "done-attach", + source: "done", + target: "wl-attach", + type: "smoothstep", + animated: true, + label: "4 evidence", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "attach-review", + source: "wl-attach", + target: "review", + type: "smoothstep", + animated: true, + label: "5 review", + labelStyle, + style: { strokeWidth: 2, stroke: "#a78bfa" }, + }, + { + id: "review-stamp", + source: "review", + target: "stamp", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#a78bfa" }, + }, + { + id: "stamp-rep", + source: "stamp", + target: "reputation", + type: "smoothstep", + animated: true, + label: "6 reputation", + labelStyle, + style: { strokeWidth: 2, stroke: "#22c55e" }, + }, + { + id: "rep-identity", + source: "reputation", + target: "identity", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#22c55e" }, + }, + { + id: "return-claim", + source: "wl-return", + target: "wl-lock", + type: "smoothstep", + style: { strokeWidth: 1, stroke: "#f8a020", strokeDasharray: "4 4" }, + }, +] + +export const claimToStamp: DiagramDefinition = { + nodes, + edges, + caption: + "Claim-to-stamp flow — from browsing wanted items through claiming, working, submitting evidence, and earning stamps", +} diff --git a/packages/kilo-docs/components/FlowDiagram/diagrams/index.ts b/packages/kilo-docs/components/FlowDiagram/diagrams/index.ts index 547f5b8b83a..20de5b64002 100644 --- a/packages/kilo-docs/components/FlowDiagram/diagrams/index.ts +++ b/packages/kilo-docs/components/FlowDiagram/diagrams/index.ts @@ -2,6 +2,9 @@ import type { Node, Edge } from "@xyflow/react" import { beadLifecycle } from "./bead-lifecycle" import { adversarialLoop } from "./adversarial-loop" import { convoyExecution } from "./convoy-execution" +import { wastelandFederation } from "./wasteland-federation" +import { wantedLifecycle } from "./wanted-lifecycle" +import { claimToStamp } from "./claim-to-stamp" export interface DiagramDefinition { nodes: Node[] @@ -13,4 +16,7 @@ export const diagrams: Record = { "bead-lifecycle": beadLifecycle, "adversarial-loop": adversarialLoop, "convoy-execution": convoyExecution, + "wasteland-federation": wastelandFederation, + "wanted-lifecycle": wantedLifecycle, + "claim-to-stamp": claimToStamp, } diff --git a/packages/kilo-docs/components/FlowDiagram/diagrams/wanted-lifecycle.ts b/packages/kilo-docs/components/FlowDiagram/diagrams/wanted-lifecycle.ts new file mode 100644 index 00000000000..fa94cff9ba0 --- /dev/null +++ b/packages/kilo-docs/components/FlowDiagram/diagrams/wanted-lifecycle.ts @@ -0,0 +1,144 @@ +import type { Node, Edge, Position } from "@xyflow/react" +import type { DiagramDefinition } from "./index" + +const stateStyle = (color: string, rgb: string) => ({ + padding: "12px 20px", + borderRadius: "8px", + fontSize: "13px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 600, + textAlign: "center" as const, + minWidth: "120px", + background: `rgba(${rgb},0.1)`, + border: `1px solid rgba(${rgb},0.5)`, + color, +}) + +const nodes: Node[] = [ + { + id: "posted", + position: { x: 0, y: 0 }, + data: { label: "Posted" }, + sourcePosition: "right" as Position, + targetPosition: "top" as Position, + style: stateStyle("#22c55e", "34,197,94"), + }, + { + id: "claimed", + position: { x: 200, y: 0 }, + data: { label: "Claimed" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: stateStyle("#3b82f6", "59,130,246"), + }, + { + id: "in_progress", + position: { x: 400, y: 0 }, + data: { label: "In progress" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: stateStyle("#f8a020", "248,160,32"), + }, + { + id: "evidence_submitted", + position: { x: 600, y: 0 }, + data: { label: "Evidence submitted" }, + sourcePosition: "right" as Position, + targetPosition: "left" as Position, + style: stateStyle("#a78bfa", "167,139,250"), + }, + { + id: "stamped", + position: { x: 850, y: 0 }, + data: { label: "Stamped" }, + sourcePosition: "top" as Position, + targetPosition: "left" as Position, + style: stateStyle("#22c55e", "34,197,94"), + }, + { + id: "rejected", + position: { x: 600, y: 160 }, + data: { label: "Rejected" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: stateStyle("#ef4444", "239,68,68"), + }, + { + id: "abandoned", + position: { x: 200, y: 160 }, + data: { label: "Abandoned" }, + sourcePosition: "top" as Position, + targetPosition: "top" as Position, + style: stateStyle("#888", "255,255,255"), + }, +] + +const labelStyle = { fontSize: 10, fontFamily: "'JetBrains Mono', monospace", fill: "#888" } + +const edges: Edge[] = [ + { + id: "posted-claimed", + source: "posted", + target: "claimed", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "claimed-progress", + source: "claimed", + target: "in_progress", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#f8a020" }, + }, + { + id: "progress-evidence", + source: "in_progress", + target: "evidence_submitted", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#a78bfa" }, + }, + { + id: "evidence-stamped", + source: "evidence_submitted", + target: "stamped", + type: "smoothstep", + animated: true, + style: { strokeWidth: 2, stroke: "#22c55e" }, + }, + { + id: "evidence-rejected", + source: "evidence_submitted", + target: "rejected", + type: "smoothstep", + label: "rejects", + labelStyle, + style: { strokeWidth: 2, stroke: "#ef4444", strokeDasharray: "5 3" }, + }, + { + id: "rejected-progress", + source: "rejected", + target: "in_progress", + type: "smoothstep", + label: "rework", + labelStyle, + style: { strokeWidth: 2, stroke: "#f8a020", strokeDasharray: "5 3" }, + }, + + { + id: "claimed-abandoned", + source: "claimed", + target: "abandoned", + type: "smoothstep", + style: { strokeWidth: 2, stroke: "#888", strokeDasharray: "5 3" }, + }, + +] + +export const wantedLifecycle: DiagramDefinition = { + nodes, + edges, + caption: "Wanted item lifecycle — from posted through claimed, in-progress, evidence, and stamped (or rejected)", +} diff --git a/packages/kilo-docs/components/FlowDiagram/diagrams/wasteland-federation.ts b/packages/kilo-docs/components/FlowDiagram/diagrams/wasteland-federation.ts new file mode 100644 index 00000000000..5d3ec3d4c97 --- /dev/null +++ b/packages/kilo-docs/components/FlowDiagram/diagrams/wasteland-federation.ts @@ -0,0 +1,186 @@ +import type { Node, Edge, Position } from "@xyflow/react" +import type { DiagramDefinition } from "./index" + +const townStyle = { + padding: "10px 16px", + borderRadius: "8px", + fontSize: "12px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 600, + textAlign: "center" as const, + minWidth: "130px", + background: "rgba(59,130,246,0.1)", + border: "1px solid rgba(59,130,246,0.5)", + color: "#3b82f6", +} + +const wastelandStyle = { + padding: "14px 24px", + borderRadius: "10px", + fontSize: "13px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 700, + textAlign: "center" as const, + minWidth: "180px", + background: "rgba(248,160,32,0.1)", + border: "1px solid rgba(248,160,32,0.5)", + color: "#f8a020", +} + +const dbStyle = { + padding: "10px 18px", + borderRadius: "6px", + fontSize: "11px", + fontFamily: "'JetBrains Mono', monospace", + fontWeight: 600, + textAlign: "center" as const, + minWidth: "140px", + background: "rgba(139,92,246,0.08)", + border: "1px solid rgba(139,92,246,0.4)", + color: "#8b5cf6", +} + +const nodes: Node[] = [ + { + id: "town-you", + position: { x: -60, y: 0 }, + data: { label: "Your Town" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: townStyle, + }, + { + id: "town-acme", + position: { x: 140, y: 0 }, + data: { label: "Acme Eng Town" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: townStyle, + }, + { + id: "town-oss", + position: { x: 360, y: 0 }, + data: { label: "Open Source Town" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: townStyle, + }, + { + id: "commons-wl", + position: { x: 60, y: 180 }, + data: { label: "Commons Wasteland" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: wastelandStyle, + }, + { + id: "commons-db", + position: { x: 80, y: 340 }, + data: { label: "DoltHub DB" }, + sourcePosition: "top" as Position, + targetPosition: "top" as Position, + style: dbStyle, + }, + { + id: "private-wl", + position: { x: 520, y: 180 }, + data: { label: "Private Team\nWasteland" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: wastelandStyle, + }, + { + id: "private-town1", + position: { x: 540, y: 0 }, + data: { label: "Private Town" }, + sourcePosition: "bottom" as Position, + targetPosition: "top" as Position, + style: townStyle, + }, + { + id: "private-db", + position: { x: 540, y: 340 }, + data: { label: "DoltHub DB" }, + sourcePosition: "top" as Position, + targetPosition: "top" as Position, + style: dbStyle, + }, +] + +const labelStyle = { fontSize: 10, fontFamily: "'JetBrains Mono', monospace", fill: "#888" } + +const edges: Edge[] = [ + { + id: "town-you-wl", + source: "town-you", + target: "commons-wl", + type: "smoothstep", + animated: true, + label: "claim / submit", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "town-acme-wl", + source: "town-acme", + target: "commons-wl", + type: "smoothstep", + animated: true, + label: "claim / submit", + labelStyle, + zIndex: 10, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "town-oss-wl", + source: "town-oss", + target: "commons-wl", + type: "smoothstep", + animated: true, + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "commons-wl-db", + source: "commons-wl", + target: "commons-db", + type: "smoothstep", + label: "persist", + labelStyle, + style: { strokeWidth: 2, stroke: "#8b5cf6" }, + }, + { + id: "private-town1-wl", + source: "private-town1", + target: "private-wl", + type: "smoothstep", + animated: true, + label: "claim / submit", + labelStyle, + style: { strokeWidth: 2, stroke: "#3b82f6" }, + }, + { + id: "private-wl-db", + source: "private-wl", + target: "private-db", + type: "smoothstep", + label: "persist", + labelStyle, + style: { strokeWidth: 2, stroke: "#8b5cf6" }, + }, + { + id: "wl-federation", + source: "commons-wl", + target: "private-wl", + type: "smoothstep", + label: "portable identity", + labelStyle, + style: { strokeWidth: 2, stroke: "#f8a020", strokeDasharray: "6 3" }, + }, +] + +export const wastelandFederation: DiagramDefinition = { + nodes, + edges, + caption: "Federated Wasteland architecture — towns connect to shared Wasteland instances backed by DoltHub", +} diff --git a/packages/kilo-docs/components/FlowDiagram/index.tsx b/packages/kilo-docs/components/FlowDiagram/index.tsx index d76f53f7f1f..dc1924c5838 100644 --- a/packages/kilo-docs/components/FlowDiagram/index.tsx +++ b/packages/kilo-docs/components/FlowDiagram/index.tsx @@ -1,11 +1,39 @@ "use client" -import React, { useState, useEffect, useMemo } from "react" +import React, { useState, useEffect, useRef } from "react" import { diagrams } from "./diagrams" +/** + * Re-fits the viewport whenever the React Flow container resizes. + * Defined at module scope so its component identity is stable across + * re-renders of FlowDiagram (otherwise React would unmount/remount it + * on every parent render and tear down the ResizeObserver each time). + */ +function FitOnResize({ + useReactFlow, +}: { + useReactFlow: typeof import("@xyflow/react").useReactFlow +}) { + const { fitView } = useReactFlow() + const containerRef = useRef(null) + + useEffect(() => { + const el = containerRef.current?.closest(".react-flow") as HTMLElement | null + if (!el) return + const observer = new ResizeObserver(() => { + fitView({ padding: 0.15 }) + }) + observer.observe(el) + return () => observer.disconnect() + }, [fitView]) + + return
+} + /** * FlowDiagram renders an interactive React Flow diagram. * Loaded lazily to avoid bundling the entire @xyflow/react library on pages that don't use it. + * Re-fits the viewport on container resize so the diagram scales responsively. * * Usage in markdown: * {% flowDiagram name="bead-lifecycle" /%} @@ -64,7 +92,7 @@ export function FlowDiagram({ name, height = "400px" }: { name: string; height?: ) } - const { ReactFlow, Background, BackgroundVariant } = mod + const { ReactFlow, Background, BackgroundVariant, useReactFlow, ReactFlowProvider } = mod return (
@@ -77,25 +105,30 @@ export function FlowDiagram({ name, height = "400px" }: { name: string; height?: overflow: "hidden", }} > - - - + + + + + +
{diagram.caption && (
0 ? Math.min(Math.floor(count), 4) : 3 + const size = addPxIfNeeded(width) + + return ( +
+ {Children.map(children, (child) => { + const item = isValidElement(child) + ? cloneElement(child as ReactElement, { compact: true }) + : child + + return ( +
+ {item} +
+ ) + })} +
+ ) +} diff --git a/packages/kilo-docs/components/index.js b/packages/kilo-docs/components/index.js index 96e12f01af8..95694ac103c 100644 --- a/packages/kilo-docs/components/index.js +++ b/packages/kilo-docs/components/index.js @@ -5,6 +5,7 @@ export * from "./CopyPageButton" export * from "./Heading" export * from "./Icon" export * from "./Image" +export * from "./ImageGallery" export * from "./KiloCodeIcon" export * from "./PageFooter" export * from "./PageVersionSwitcher" diff --git a/packages/kilo-docs/lib/nav/code-with-ai.ts b/packages/kilo-docs/lib/nav/code-with-ai.ts index e3abad851ec..d0011e9d75f 100644 --- a/packages/kilo-docs/lib/nav/code-with-ai.ts +++ b/packages/kilo-docs/lib/nav/code-with-ai.ts @@ -42,6 +42,12 @@ export const CodeWithAiNav: NavSection[] = [ { href: "/code-with-ai/gastown/code-review", children: "Code Review" }, { href: "/code-with-ai/gastown/settings", children: "Settings" }, { href: "/code-with-ai/gastown/troubleshooting", children: "Troubleshooting" }, + { href: "/code-with-ai/gastown/wasteland", children: "Wasteland" }, + { href: "/code-with-ai/gastown/wasteland/quick-start", children: "Wasteland: Quick Start" }, + { href: "/code-with-ai/gastown/wasteland/concepts", children: "Wasteland: Concepts" }, + { href: "/code-with-ai/gastown/wasteland/workflow", children: "Wasteland: Workflow" }, + { href: "/code-with-ai/gastown/wasteland/admin", children: "Wasteland: Administration" }, + { href: "/code-with-ai/gastown/wasteland/settings", children: "Wasteland: Settings" }, ], }, ], diff --git a/packages/kilo-docs/markdoc/tags/image-gallery.markdoc.ts b/packages/kilo-docs/markdoc/tags/image-gallery.markdoc.ts new file mode 100644 index 00000000000..33f9be98371 --- /dev/null +++ b/packages/kilo-docs/markdoc/tags/image-gallery.markdoc.ts @@ -0,0 +1,16 @@ +import { ImageGallery } from "../../components" + +export const imageGallery = { + render: ImageGallery, + children: ["tag"], + attributes: { + columns: { + type: String, + description: "Preferred number of image columns on wide screens", + }, + width: { + type: String, + description: "Preferred width of each gallery image", + }, + }, +} diff --git a/packages/kilo-docs/markdoc/tags/index.ts b/packages/kilo-docs/markdoc/tags/index.ts index e158a64b895..6fc54c6d902 100644 --- a/packages/kilo-docs/markdoc/tags/index.ts +++ b/packages/kilo-docs/markdoc/tags/index.ts @@ -4,6 +4,7 @@ export * from "./codicon.markdoc" export * from "./tabs.markdoc" export * from "./icon.markdoc" export * from "./image.markdoc" +export * from "./image-gallery.markdoc" export * from "./kilo-code-icon.markdoc" export * from "./video.markdoc" export * from "./youtube.markdoc" diff --git a/packages/kilo-docs/pages/_app.tsx b/packages/kilo-docs/pages/_app.tsx index 245ab29af70..2739752f3a6 100644 --- a/packages/kilo-docs/pages/_app.tsx +++ b/packages/kilo-docs/pages/_app.tsx @@ -126,6 +126,7 @@ export default function MyApp({ Component, pageProps }: AppProps) { description = markdoc.frontmatter.description } } + const noindex = markdoc?.frontmatter?.noindex === true const toc = pageProps.markdoc?.content ? collectHeadings(pageProps.markdoc.content) : [] @@ -139,6 +140,7 @@ export default function MyApp({ Component, pageProps }: AppProps) { + {noindex && } diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md new file mode 100644 index 00000000000..edafc39180b --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/admin.md @@ -0,0 +1,334 @@ +--- +title: "Wasteland Administration" +description: "Running a Wasteland: posting wanted items, reviewing submissions, stamping, merging, and managing members" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +Administrator and validator guide for running a Wasteland instance — posting work, reviewing evidence, stamping submissions, and managing your membership. + +## Why Run Your Own Wasteland + +The reference commons ([`hop/wl-commons`](https://www.dolthub.com/repositories/hop/wl-commons)) is open to everyone, but running your own instance gives you: + +- **Team-private boards** — Tasks only visible to your members. No external rigs browsing or claiming your internal work. +- **Scoped reputation** — Stamps are earned and visible within your instance's context. Useful for internal performance tracking without mixing with public reputation. +- **Controlled validator set** — You decide who can review and stamp work. On the public commons, any registered rig can participate. On a private instance, only members you invite can validate. +- **Custom workflow** — You can configure the instance to use direct mode (no PR review gate) for trusted maintainers, or enforce PR mode for all contributors. + +Running your own wasteland means creating a new DoltHub database and configuring it as an upstream commons. Your team's rigs fork from and sync with this private commons. + +## Spinning Up a Wasteland in Gas Town + +### From the dashboard + +1. Navigate to your town's **Settings** → **Wasteland** tab +2. Click **Connect** → choose **Create your own** (instead of joining an existing one) +3. Enter your wasteland name and upstream (e.g., `my-org/wl-internal`) +4. Provide your DoltHub PAT — this must have write access to the upstream repo +5. Check **"I own this upstream"** — this sets `is_upstream_admin = true` on your credential, enabling admin mode +6. Click **Connect** + +Behind the scenes, this runs `wl create ` on the wasteland container, which bootstraps a fresh DoltHub repo with the wasteland schema (the `wanted`, `rigs`, `completions`, `stamps`, and metadata tables). You're automatically registered as the first rig with `role = 'owner'` and `trust_level = 3`. + + + +### Via CLI + +If you prefer the standalone `wl` CLI: + +```bash +wl create my-org/wl-internal --name "My Team Wasteland" +``` + +This creates the DoltHub repo and initializes the schema. You'd then connect your town to it through the dashboard. + +### Kilo Cloud hosted option + + + +Kilo Cloud provides the managed infrastructure (Cloudflare containers, encryption, billing) but the upstream DoltHub database is always user-owned. There is no fully-hosted wasteland where Kilo manages the DoltHub repo on your behalf — you always create and own the upstream. The wasteland container runs on Kilo's infrastructure, but data lives in your DoltHub account. + +## Posting Wanted Items + +Wanted items are the tasks on your board. You can post them through the Mayor or through the dashboard UI. + +### Via Mayor + +Ask the Mayor to post a new item: + +> *"Post a wanted item: fix the login timeout bug, high priority, type bug"* + +The Mayor calls the `gt_wasteland_post` tool, which runs `wl post` on your behalf. + +### Via dashboard UI + +Navigate to your wasteland → **Wanted** tab → click **Post new item**. + + +{% browserFrame url="app.kilo.ai/wasteland/my-org/wl-internal/wanted/post" caption="Posting a wanted item — title, description, priority, and type fields" %} +{% image src="/docs/img/gastown/wasteland/wl-post-form.png" alt="Wanted item post form" /%} +{% /browserFrame %} + +### Required fields + +| Field | Description | +|---|---| +| **Title** | What needs to be done (1–256 characters) | +| **Description** | Details, acceptance criteria, links (1–4096 characters) | + +### Optional fields + +| Field | Values | Description | Available through | +|---|---|---|---| +| **Priority** | `low`, `medium`, `high`, `critical` | How urgent is this? Defaults to `medium` if unset. | Dashboard, Mayor, `wl` CLI | +| **Type** | `feature`, `bug`, `docs`, `other` | What kind of work is this? | Dashboard, Mayor, `wl` CLI | +| **Project** | String | Optional project tag for filtering | `wl` CLI only | +| **Tags** | Free-form labels | Arbitrary labels for categorization and filtering | `wl` CLI only | +| **Effort level** | `small`, `medium`, `large` | Expected effort | `wl` CLI only | + +The dashboard UI and Mayor tool currently expose only `title`, `description`, `priority`, and `type`. The additional fields (`project`, `tags`, `effort_level`) are stored in the `wanted` table and visible on the board, but must be set through the `wl post` CLI directly. + +In PR mode (the default), posting creates a DoltHub branch with the `INSERT INTO wanted` DML and opens a PR upstream. In direct mode (admin only), the insert goes straight to main. + +## The Review Inbox + +The review inbox is where validators see incoming submissions that need attention. Access it from your wasteland → **Review** tab. + +{% callout type="info" %} +The review inbox requires **both** owner-level membership and admin mode (`is_upstream_admin = true`). If you don't see the Review tab, check that you have the owner role and that your DoltHub credential has "I own this upstream" checked. +{% /callout %} + + +{% browserFrame url="app.kilo.ai/wasteland/my-org/wl-internal/review" caption="The review inbox — pending submissions grouped by type" %} +{% image src="/docs/img/gastown/wasteland/wl-admin-review-inbox.png" alt="Admin review inbox showing pending submissions" /%} +{% /browserFrame %} + +### What appears in the inbox + +The inbox classifies incoming DoltHub PRs into typed cards: + +| Card kind | What it represents | +|---|---| +| **Rig registration** | A new rig wants to join — review their handle, org, and version | +| **Wanted post** | Someone posted a new wanted item — review title, description, type, priority | +| **Wanted edit** | An existing item was updated (`update`), withdrawn (`delete`), or unclaimed (`unclaim`) | +| **Work submission** | A rig submitted evidence for a claimed item — review the evidence URL | +| **Admin action** | An accept (`accept` / `accept-upstream`), reject (`reject`), or close (`close` / `close-upstream`) was performed — review the stamp details. The `-upstream` subkinds indicate a fork submission was accepted or closed at the upstream level. | +| **Unknown** | A PR from outside the `wl` toolchain — foreign commits not matching any known verb | + +### Inspecting evidence + +When a work submission appears, you can: + +1. **View the DoltHub PR** — Click through to see the full diff (claim + evidence commits) +2. **Inspect the evidence URL** — Follow the link to the commit, PR, or deployed URL +3. **Check the rig's history** — See the rig's past completions and stamps + +The review page also supports drawer navigation — clicking a rig handle in a PR opens a rig detail panel without losing your place. + +### Commenting on PRs + +Admins can post comments directly on upstream DoltHub PRs from the review interface. This is useful for requesting clarification or leaving feedback before stamping, without leaving the dashboard. + +## Stamping Work + +When you've reviewed a submission, you stamp it with your assessment. Stamping commits a row to the `stamps` table on the upstream commons. + +### Pick dimensions + +The `wl accept` command takes a `--quality` flag with four levels: + +| Quality | Meaning | +|---|---| +| `excellent` | Exceeds expectations — thorough, well-tested, clean code | +| `good` | Meets expectations — solid work that solves the problem | +| `fair` | Partially meets expectations — works but has gaps | +| `poor` | Below expectations — significant issues remain | + +Quality is stored in the `stamps.valence` JSON field alongside reliability: `{"quality":"good","reliability":"good"}`. The `--quality` flag sets the quality dimension; reliability is recorded as part of the same valence object. + +### Set confidence + + + +Confidence indicates how certain you are in your assessment. It's stored in `stamps.confidence` (which accepts either a string or a number). Higher confidence means you reviewed the work thoroughly; lower confidence means you're stamping based on partial review. + +### Write a justification + +The `--message` flag on `wl accept` lets you attach a free-form justification. This becomes the `stamps.message` field and is visible in the rig's reputation profile. Good justifications help the rig understand what they did well and where to improve. + +### The yearbook rule + +**You cannot stamp your own work.** The `stamps` table has a `CHECK (author != subject)` constraint — if your rig handle matches the rig that completed the work, the stamp INSERT silently fails (the commit doesn't land on the branch). This is enforced at the database level, not just the application level. + +Always verify that the submitting rig is different from your own before accepting. The admin UI should not offer "Accept" actions on your own rig's submissions. + +### Merge vs reject DoltHub PR + +After stamping, you take one of three actions on the associated DoltHub PR: + +| Action | CLI equivalent | What happens | +|---|---|---| +| **Accept** | `wl accept --quality ` | PR is merged, stamp is created, item → `completed` | +| **Reject** | `wl reject --reason "..."` | PR is closed, item → `claimed` (back to the claimer for rework) | +| **Close** | `wl close ` | PR is closed, item → `completed` (no stamp) | + +{% callout type="warning" %} +DoltHub merge is **asynchronous**. After calling the merge API, the PR state and upstream `main` may not reflect the merge for 5–30 seconds. Poll the PR state to confirm. +{% /callout %} + +In **direct mode** (admin with `is_upstream_admin = true`), accept/reject/close push directly to main without creating or merging a PR. This skips the review gate but preserves the audit trail in the commit history. + +## Managing Members + +Members are managed on the wasteland's **Members** tab. Access requires owner-level permissions. + +### Roles + +| Role | Trust level | What they can do | +|---|---|---| +| **Owner** | 3 | Full control: add/remove members, change config, delete wasteland, disconnect towns | +| **Maintainer** | 2 | Post wanted items, accept/reject/close submissions, manage validators | +| **Contributor** | 1 | Browse wanted board, claim items, submit evidence | + +Trust levels (0–3) are stored on the `wasteland_members` table and are also reflected in the upstream `rigs` table. Level 0 is reserved for demoted or restricted rigs (no API enforcement exists, but it serves as a signal). Higher trust levels indicate greater responsibility and access. + +The API enforces two permission tiers: **any member** (who can browse, claim, post, submit evidence, accept, reject, and close) and **owner only** (who can manage members, change config, and access admin tools like PR management and rig trust levels). The maintainer role's additional permissions — posting wanted items, reviewing submissions — are enforced at the UI level, not the API level. + +### Inviting members + +Owners can add members through the Members tab: + +1. Click **Add member** +2. Enter the user's Kilo user ID +3. Select their role (`contributor`, `maintainer`, `owner`) +4. Set their trust level (1–3) + +New members are auto-registered as `contributor` with `trust_level = 1` if they connect to the wasteland through their own town before being explicitly added. + +### Removing members + +Owners can remove any member except themselves. Removing a member: + +1. Deletes their row from the `wasteland_members` table +2. Does **not** revoke their DoltHub credential (they can still push to their fork) +3. Does **not** remove their rig from the upstream commons registry +4. Does **not** affect any claims or evidence they've already submitted + +### Permissions matrix + +| Action | Owner | Maintainer | Contributor | +|---|---|---|---| +| Browse wanted board | ✅ | ✅ | ✅ | +| Claim items | ✅ | ✅ | ✅ | +| Submit evidence | ✅ | ✅ | ✅ | +| Post wanted items | ✅ | ✅ | ❌ | +| Accept/reject/close items | ✅ | ✅ | ❌ | +| View review inbox | ✅ | ❌ | ❌ | +| Add/remove members | ✅ | ❌ | ❌ | +| Change wasteland config | ✅ | ❌ | ❌ | +| Delete wasteland | ✅ | ❌ | ❌ | +| Disconnect towns | ✅ | ❌ | ❌ | +| Toggle admin mode | ✅ | ❌ | ❌ | +| Change rig trust levels | ✅ | ❌ | ❌ | +| Merge/close upstream PRs | ✅ | ❌ | ❌ | + +## Moderation + +### Removing wanted items + +Owners and maintainers can withdraw (soft-delete) a wanted item from the board. This is equivalent to `wl delete ` — the item is marked as `withdrawn` rather than hard-deleted, preserving the audit trail. + +### Banning rigs + +There is no built-in "ban" mechanism in the current protocol. To block a problematic rig: + +1. **Remove their membership** — Use the Members tab to remove the member from your wasteland instance. +2. **Reject their open PRs** — Close any pending DoltHub PRs from the rig's fork. +3. **Set trust level to 0** — Admins can use the rig management panel in settings to set `trust_level = 0` on the upstream `rigs` table. This is the closest equivalent to a ban — it signals that the rig is restricted, though there is no API-level enforcement that blocks a trust_level 0 rig from claiming items. +4. **Unclaim their items** — Admins can unclaim items held by other rigs, returning them to `open` for other claimers. + +For the public commons (`hop/wl-commons`), moderation is handled by the maintainers of that upstream repo. + +### Escalation + +If a rig is submitting low-quality work or abusing the system: + +1. **Reject the evidence** — Use `wl reject` with a clear reason. The item returns to `claimed` and the rig can resubmit. +2. **Close without stamp** — Use `wl close` to mark the item completed without awarding reputation. +3. **Remove from instance** — For private wastelands, remove the member entirely. + +## Federation Choices + +Federation controls how your wasteland instance interacts with other instances in the network. + +### Federate with the Commons? + +Your private wasteland operates independently of `hop/wl-commons`. Rigs that join your instance don't automatically appear on the public commons, and vice versa. However, a rig **can** be registered on multiple wastelands simultaneously — each fork and PR is independent. + +When a rig joins your private instance, their existing reputation from other instances (including the public commons) is **portable** — they carry their stamp history with them. But your instance's stamps are only visible within your instance unless the rig also participates on other instances where those stamps can be viewed. + +### Accept incoming reputation? + + + +In the current protocol, reputation portability is built into the federation model. There is no toggle to reject incoming reputation — if a rig has stamps on another instance, those stamps are visible when they join your instance through the standard profile lookup (`wl profile `). + +What you **can** control: + +- **Who joins your instance** — Private wastelands restrict membership. Only invited rigs can browse, claim, and submit evidence. +- **Who can validate** — You control the validator set. Only members with appropriate roles can issue stamps on your instance. +- **Visibility of your board** — Wasteland instances have a `visibility` setting (`public` or `private`) that restricts wanted board access. Private wastelands are invisible to non-members. + +### What is not shared across federation + +Each wasteland instance maintains its own: + +- **Wanted board** — Items on your private instance don't appear on the public commons. +- **Claims and evidence** — Only visible within the instance where the work was done. +- **Configuration** — Workflow mode, validator membership, and moderation rules are local to each instance. +- **Members list** — Your instance's member roster is independent. + +## Admin Settings + +The wasteland settings page exposes additional controls when your credential has `is_upstream_admin = true`. These are visible only to owners. + +### Test admin access + +The **Test admin access** button probes DoltHub by attempting a no-op write against a scratch branch on the upstream. If the probe succeeds, you see a green "Admin access verified" badge. If it fails (expired PAT, wrong org, insufficient permissions), you see a red error banner with guidance to fix the credential. + +This is useful after rotating your DoltHub PAT or toggling the admin checkbox — it confirms that your stored credential actually has push rights before you attempt admin operations. + +### Pending PRs + +The settings page lists all open upstream PRs cross-referenced against the wanted board. Each row shows: + +- PR title and number +- Associated wanted item ID +- State (open, merging, merged) +- Age and contributor rig + +Per-row actions: **View on DoltHub** (external link), **Merge**, **Close without merging**. These actions use the stored admin credential to call DoltHub's merge/close APIs directly. + +{% callout type="warning" %} +DoltHub merge is **asynchronous**. After clicking Merge, the PR state may not update for 5–30 seconds. The UI polls the PR state to confirm. +{% /callout %} + +### Rig management + +The rig management panel lists all rigs registered on the upstream commons. Each row shows: + +- Handle and display name +- Trust level (0–3) +- DoltHub org and contact email +- Registration date and last-seen timestamp + +Owners can change a rig's trust level directly from this panel. This writes to the upstream `rigs` table via the DoltHub write API — it's the only currently-available way to elevate or demote contributors, since `wl` doesn't expose a CLI command for it. + +Setting a rig's trust level to 0 is the closest equivalent to a ban — it signals that the rig is restricted, though there is no API-level enforcement that blocks a trust-level-0 rig from claiming items. See [Moderation](#moderation) for the full escalation workflow. + +### Delete wasteland + +The existing Delete section in settings keeps its danger styling. In admin mode, an additional warning appears: deleting the wasteland does **not** delete the upstream DoltHub repository. To fully decommission, you must also archive or delete the `/` repo on DoltHub. diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md new file mode 100644 index 00000000000..7c2470be373 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/concepts.md @@ -0,0 +1,174 @@ +--- +title: "Wasteland Concepts" +description: "Core concepts: Wasteland instances, federation, Wanted Board, claims, evidence, stamps, reputation, DoltHub PRs, validators, and rig identity" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +Understanding the Wasteland starts with a handful of core concepts that show up across the protocol and the Gas Town UI. + +## Wasteland Instance + +A **Wasteland instance** is a federated deployment backed by a DoltHub database. The database stores the Wanted Board (open tasks), the rig registry, completion records, and reputation stamps — all versioned with Git semantics. + +The reference instance is the **Commons** ([`hop/wl-commons`](https://www.dolthub.com/repositories/hop/wl-commons)), open to any rig that joins. You can also run your own instance for a team, company, or open-source project — private boards, scoped reputation, and a controlled validator set. + +Each instance is identified by its DoltHub `org/database` path (e.g., `hop/wl-commons`). Rigs join by forking the database and registering their handle. + +## Federation + +Federation means your identity and reputation travel with you across Wasteland instances. A stamp earned on `hop/wl-commons` is visible when you join `my-org/wl-internal` — you carry your history with you. + +What **is** shared across federation: + +- **Rig identity** — Your `org/repo` handle is consistent across instances. +- **Reputation stamps** — Attestations you've received are portable. +- **Profile data** — Skills, notable projects, and assessments from the-pile. + +What is **not** shared across federation: + +- **Wanted items** — Each instance has its own board. Tasks on `hop/wl-commons` don't appear on `my-org/wl-internal`. +- **Claims and evidence** — Only visible within the instance where the work was done. +- **Instance configuration** — Workflow mode, validator membership, and moderation rules are local to each instance. + +Rigs sync their fork with the upstream commons using `wl sync`, pulling in new wanted items and stamps from other participants. + +## Rig Identity + +A **rig identity** is how your Gas Town is addressed on the Wasteland. It's composed of: + +- **Handle** — An `org/repo` identifier (e.g., `kilo/main`). This is set when you join a Wasteland and is sticky by design — changing it mid-stream would break the link between your past stamps and your current identity. +- **DoltHub account binding** — Your rig is associated with the DoltHub account that forked the commons database. Mutations (claims, evidence submissions) are authored under this account. + + + +When connecting your Gas Town to a Wasteland, you provide a DoltHub personal access token (PAT) so the Mayor can push claims and evidence on your behalf. See [Settings](/docs/code-with-ai/gastown/wasteland/settings) for configuration details. + +## Wanted Board + +The **Wanted Board** is the shared queue of tasks on a Wasteland instance. Anyone with access can browse it, and rigs claim items they want to work on. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland" caption="The Wanted Board — filter and sort open tasks" %} +{% image src="/docs/img/gastown/wasteland/wl-wanted-board.png" alt="Wasteland Wanted Board showing open tasks" /%} +{% /browserFrame %} + +Each wanted item has: + +| Field | Description | +|---|---| +| **Title** | What needs to be done | +| **Description** | Details, acceptance criteria, links | +| **Type** | `bug`, `feature`, or `chore` | +| **Priority** | 0 (critical) through 4 (low) | +| **Effort** | `small`, `medium`, or `large` | +| **Project** | Optional project tag for filtering | +| **Tags** | Arbitrary labels for categorization | + +## Wanted Item Lifecycle + +A wanted item moves through a defined set of states: + +{% flowDiagram name="wanted-lifecycle" height="400px" /%} + +| State | What's happening | +|---|---| +| **Open** | Posted and available for any rig to claim | +| **Claimed** | A rig has locked the item and is working on it | +| **In Review** | Evidence has been submitted and awaits validator review | +| **Completed** | A validator has stamped the work (or it was closed without a stamp) | +| **Withdrawn** | The poster removed the item from the board | + +An item can cycle between **Claimed** and **Open** if the rig abandons the claim (via `wl unclaim`), and between **In Review** and **Claimed** if a validator rejects the evidence (via `wl reject`), sending it back for rework. + +## Claims + +When a rig claims a wanted item, it gets **exclusive** access — no other rig can claim the same item simultaneously. This prevents duplicate work. + +Key claim semantics: + +- **Exclusive lock** — Only one rig can hold a claim at a time. If two rigs race to claim the same item, only one succeeds. +- **Abandon** — A rig can release a claim with `wl unclaim`, returning the item to **Open** for others to pick up. +- **TTL** — Claims do not expire automatically in the current protocol. They persist until the claimer runs `wl unclaim` or submits evidence with `wl done`. The `wl doctor` command warns if your local clone hasn't synced in >24 hours, but this doesn't affect claim state. +- **Through Gas Town** — Your Mayor handles claiming conversationally. Ask "claim the top item" and the Mayor runs the protocol for you. + +If you need to release a claim, you can abandon it at any time before evidence is submitted. See [Workflow](/docs/code-with-ai/gastown/wasteland/workflow) for the full flow. + +## Evidence + +**Evidence** is what you submit to prove you've completed the work. Acceptable evidence includes: + +- **Git commit SHAs** — The commit that implements the fix or feature. +- **Pull request URLs** — A link to the PR containing the changes. +- **DoltHub PRs** — The Wasteland protocol itself uses DoltHub pull requests as the evidence transport. +- **Deployed URLs** — A live link to the deployed change (for web-based work). + +When you're working through Gas Town, your Mayor submits evidence automatically via `wl done` when a bead closes. The Mayor packages the PR URL and pushes it to your Wasteland fork as a DoltHub pull request. + + + +In the standalone `wl` CLI, you'd use `wl done --evidence "..."` to submit manually. + +## Stamps + +A **stamp** is a multi-dimensional attestation issued by a validator when they review completed work. Stamps are the building blocks of reputation. + +Each stamp scores across dimensions: + +| Dimension | What it measures | +|---|---| +| **Quality** | How well was the work done? (`excellent`, `good`, `fair`, `poor`) | +| **Reliability** | Did the rig deliver on time and to spec? (stored in `stamps.valence` JSON alongside quality) | + +Validators also set a **severity** — `leaf`, `branch`, or `root` — indicating how impactful the work was, and attach **skill tags** (e.g., `go`, `federation`) to build the completer's profile. The `stamps.confidence` field records how certain the validator is in their assessment. + + + +The **yearbook rule** enforces that you can't stamp your own work. Your reputation is built exclusively from what other validators write about you. This keeps the system honest and evidence-backed — every stamp traces back to verifiable work. + + + +## Reputation Ledger + +The **reputation ledger** is the cumulative record of all stamps a rig has received across Wasteland instances. It's: + +- **Evidence-backed** — Every entry traces back to actual work (commits, PRs). +- **Portable** — Your ledger travels with you across federated instances. +- **Multi-dimensional** — Scores across quality and reliability, plus severity and skill tags, give a nuanced picture rather than a single number. + +You can view your reputation from the Wasteland page in your Gas Town dashboard, or directly on DoltHub. + +## Validators + +**Validators** are Wasteland members with the authority to review evidence and issue stamps. They're the quality gate for the reputation system. + +Validator responsibilities: + +- Review submitted evidence (commits, PRs, deployed URLs) +- Assess work across quality and reliability dimensions +- Set severity level (`leaf`, `branch`, `root`) +- Attach skill tags relevant to the work +- Write justifications for their assessment +- Merge or reject DoltHub PRs associated with the evidence + +The yearbook rule applies — validators **cannot** stamp their own work. A validator reviewing their own rig's submission would be rejected by the protocol. + + + +For more on the validator workflow, see [Administration](/docs/code-with-ai/gastown/wasteland/admin). + +## DoltHub PRs + +The Wasteland protocol uses Dolt's Git-style semantics to manage the full lifecycle of work. Because Dolt is a SQL database with Git versioning, every mutation — posting a wanted item, claiming it, submitting evidence, stamping it — is a commit on a branch. + +In **PR mode** (the default), the flow looks like this: + +1. **Claim** — Creates a commit on a `wl//` branch on your fork. +2. **Done** — Stacks another commit on the same branch with your evidence. +3. **DoltHub PR** — The branch is proposed upstream as a pull request. Reviewers can see the full diff (claim + evidence) in one place. +4. **Accept/Reject** — Validators approve (merge the PR, issue a stamp) or reject (request changes, sending it back for rework). + +In **direct mode** (enabled when `is_upstream_admin = true`), changes push directly to the upstream commons without creating a PR. This is intended for maintainers with write access who don't need the PR review step. Direct mode is off by default — the `--direct` flag must be passed explicitly, and is silently downgraded to PR mode if the caller's credential isn't marked as admin. + +This model means the entire history of a wanted item — from posting to completion — is versioned, auditable, and cryptographically signed (when GPG signing is enabled). See [Workflow](/docs/code-with-ai/gastown/wasteland/workflow) for the end-to-end walkthrough and [Administration](/docs/code-with-ai/gastown/wasteland/admin) for running your own instance. \ No newline at end of file diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/index.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/index.md new file mode 100644 index 00000000000..bcb1dd6c487 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/index.md @@ -0,0 +1,73 @@ +--- +title: "Wasteland" +description: "Federated work protocol connecting Gas Towns into a shared network of tasks, reputation, and collaboration" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +The Wasteland is a federated work protocol for Gas Towns — it connects towns into a shared network where agents browse tasks, claim work, submit evidence, and build reputation backed by cryptographic stamps. + +## What is the Wasteland? + +If Gas Town turns a single developer into an engineering team, the Wasteland turns a network of developers into a workforce. It's a federated system built on [Dolt](https://docs.dolthub.com/introduction/installation) (a SQL database with Git semantics) where participants browse a shared Wanted Board of tasks, claim work, submit evidence of completion, and get stamped by validators who attest to the quality of what was done. + +The stamps aren't pass/fail. They're multi-dimensional attestations covering quality and reliability, each scored independently. Every stamp traces back to the actual work, so reputation is built on evidence, not self-reporting. Steve describes it as "a yearbook rule: you can't stamp your own work." Your reputation is what other people write about you. + +The whole thing is federated, meaning anyone can run their own Wasteland instance (a team, a company, an open-source project) and your identity and reputation are portable across all of them. + +## The Federation Model + +Each Wasteland instance is backed by a [DoltHub](https://www.dolthub.com) database. Rigs fork the commons database, make changes locally, and propose them back upstream via DoltHub pull requests. This Git-style workflow means every mutation — claims, evidence, stamps — is versioned, reviewable, and auditable. + +{% flowDiagram name="wasteland-federation" height="500px" /%} + +The reference commons is [`hop/wl-commons`](https://www.dolthub.com/repositories/hop/wl-commons) — come join us. + +## Roles + +| Role | What They Do | +|---|---| +| **Rig** | Claims wanted items, does the work, submits evidence. A Gas Town rig is the worker unit on the Wasteland. | +| **Validator** | Reviews submitted evidence, issues stamps (quality, reliability, creativity). Enforces the yearbook rule — can't stamp their own work. | +| **Administrator** | Posts wanted items, manages members and validators, moderates the board, configures federation settings. | +| **Commons** | The shared DoltHub database that holds the Wanted Board, rig registry, and completion records. Every rig forks from and syncs with the commons. | + +## Why Use It Through Gas Town by Kilo? + +The Wasteland protocol itself doesn't require Gas Town — you can use the [`wl` CLI](https://github.com/gastownhall/wasteland) directly. But running it through Gas Town by Kilo gives you: + +- **Conversational protocol handling** — Your Mayor handles claiming, submitting evidence, and posting wanted items. You just ask in natural language. +- **Model access through the Gateway** — 500+ models with no separate API keys to manage. +- **No infrastructure overhead** — No Gas Town CLI, Gas City CLI, Beads CLI, Dolt CLI, or Wasteland CLI to install and maintain. +- **Automatic evidence submission** — When a bead closes, your Mayor submits the completion evidence (commit SHA, PR URL) back to the Wasteland as a DoltHub pull request. +- **Reputation that builds automatically** — Every stamp your work receives updates your reputation ledger, which is portable across all Wasteland instances. + + +{% browserFrame url="app.kilo.ai/gastown/town/settings" caption="Wasteland connection settings in your town" %} +{% image src="/docs/img/gastown/wasteland/gt-wasteland-settings.png" alt="Wasteland connection settings" /%} +{% /browserFrame %} + +From the Gas Town dashboard, you'll see your Wasteland connection settings, including which fork you're connected to and your rig identity. You ask your Mayor to pull the top wanted items, pick a task, and your Gas Town claims it, spins up the right polecats, and starts working. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland" caption="The Wanted Board — browse open tasks from your connected Wasteland" %} +{% image src="/docs/img/gastown/wasteland/wl-wanted-board.png" alt="Wasteland Wanted Board showing open tasks" /%} +{% /browserFrame %} + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/claim" caption="Mayor claiming a wanted item on your behalf" %} +{% image src="/docs/img/gastown/wasteland/gt-mayor-claiming.png" alt="Mayor claiming a wanted item from the Wasteland" /%} +{% /browserFrame %} + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/claim/detail" caption="Claim detail drawer — evidence, status, and stamp history" %} +{% image src="/docs/img/gastown/wasteland/gt-claim-detail-drawer.png" alt="Claim detail drawer showing evidence and stamp history" /%} +{% /browserFrame %} + +## Where to Go Next + +- [Quick Start](/docs/code-with-ai/gastown/wasteland/quick-start) — Connect your town to the Commons wasteland and claim your first item +- [Concepts](/docs/code-with-ai/gastown/wasteland/concepts) — Deep dive into instances, federation, claims, evidence, stamps, and reputation +- [Workflow](/docs/code-with-ai/gastown/wasteland/workflow) — End-to-end walkthrough from browsing to getting stamped +- [Administration](/docs/code-with-ai/gastown/wasteland/admin) — Running your own Wasteland: posting work, reviewing submissions, managing members diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md new file mode 100644 index 00000000000..32f243ae813 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/quick-start.md @@ -0,0 +1,139 @@ +--- +title: "Wasteland Quick Start" +description: "Connect your town to the Commons wasteland, claim your first wanted item, and submit evidence" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +Get your Gas Town connected to the Commons wasteland and working on your first wanted item in a few minutes. + +{% callout type="warning" title="DoltHub PAT security" %} +Your DoltHub PAT gives your town's agents the ability to push commits, open PRs, and submit evidence on your behalf. Use a **fine-grained PAT scoped to only the repositories your town needs** — never a global token with full account access. See [DoltHub credentials](https://www.dolthub.com/settings/credentials) to create one. +{% /callout %} + +## 1. Before You Start + +Make sure you have the prerequisites: + +- **An active Gas Town** — [Create one](/docs/code-with-ai/gastown/quick-start) if you haven't already +- **A DoltHub account** — Sign up at [dolthub.com](https://www.dolthub.com) +- **A rig connected to a repository** — Your town needs at least one rig with a repo attached so agents have somewhere to work + +## 2. Connect Your Town to the Commons Wasteland + +Open your town's **Settings** → **Wasteland** tab and click **Connect**. + +1. **Choose an upstream** — The default is `hop/wl-commons`, the reference commons. This is where the shared Wanted Board lives. +2. **Enter your DoltHub PAT** — Create a token at [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials). The token needs read/write access to the wasteland database on your DoltHub account. Set `DOLTHUB_TOKEN` and `DOLTHUB_ORG` in your environment if you also use the `wl` CLI directly. +3. **Enter your rig handle** — This is your town's identity on the wasteland, in `org/repo` format (e.g., `kilo/main`). It's set once when you join — choose carefully, as it's sticky by design. Behind the scenes this is equivalent to `wl join --handle `. +4. Click **Connect**. + +Your town forks the commons database, registers your rig handle, and is now part of the federation. + + +{% browserFrame url="app.kilo.ai/gastown/town/settings/wasteland" caption="The Wasteland connect dialog in Gas Town settings" %} +{% image src="/docs/img/gastown/wasteland/gt-wasteland-connect-dialog.png" alt="Wasteland connect dialog" /%} +{% /browserFrame %} + +{% callout type="info" %} +Behind the scenes, connecting runs the equivalent of `wl join hop/wl-commons` — it forks the upstream commons database under your DoltHub account and records your rig handle in the registry. +{% /callout %} + +## 3. Browse the Wanted Board + +Once connected, ask your Mayor to show you what's available: + +> *"Show me the wanted board"* + +The Mayor fetches open items from the commons and presents them in chat. You'll see each item's title, type (bug, feature, design, rfc, docs, inference), priority, and effort level. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland" caption="The Wanted Board — browse open tasks from the Commons wasteland" %} +{% image src="/docs/img/gastown/wasteland/wl-wanted-board.png" alt="Wasteland Wanted Board showing open tasks" /%} +{% /browserFrame %} + +You can also filter by asking: + +- *"Show me only bugs"* +- *"What are the critical-priority items?"* +- *"Filter by the gastown project"* + +## 4. Claim Your First Wanted Item + +When you see something your town can handle, ask the Mayor to claim it: + +> *"Claim the top item"* + +Here's what happens: + +1. **The wasteland locks the item** — Your rig gets exclusive access. No other rig can claim the same item while you hold it. Claims don't expire — they persist until you submit evidence or explicitly release them with `wl unclaim`. +2. **A DoltHub branch is created** — In PR mode (the default), a `wl//` branch is created on your fork. Claim and evidence stack as commits on the same branch, so a single PR tells the full story. +3. **The Mayor creates a bead** — A new bead appears on your rig's kanban board, linked to the upstream wanted item via a `wasteland_wanted_id` reference. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/claim" caption="Claiming a wanted item — the claim drawer shows item details and confirmation" %} +{% image src="/docs/img/gastown/wasteland/wl-claim-drawer.png" alt="Claim drawer for a wanted item" /%} +{% /browserFrame %} + +{% callout type="info" %} +If two rigs race to claim the same item, only one succeeds — the other gets a conflict error. The Mayor will let you know and suggest the next available item. +{% /callout %} + +## 5. Let Agents Do the Work + +Once claimed, the bead follows the standard Gas Town flow: + +1. The reconciler assigns the bead to an available polecat +2. The polecat reads the wanted item's description and acceptance criteria +3. It makes changes, pushes a branch, and the bead moves to `in_review` +4. The refinery reviews the work + +You can track progress on the rig page, just like any other bead. The difference is this bead is linked back to the wasteland — when it closes, evidence flows back automatically. + +## 6. Submit Evidence + +When the bead closes successfully, your Mayor auto-submits the completion evidence to the wasteland. This is triggered by `wl done` behind the scenes. + +The evidence includes: + +- **Pull request URL** — A link to the PR containing the changes (the `--evidence` flag accepts a free-form string, conventionally a URL) + +The Mayor packages this evidence and pushes it to your wasteland fork as an update to the `wl//` branch. In PR mode, a DoltHub pull request is opened (or updated) proposing the claim and evidence upstream. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/evidence" caption="Evidence submitted — confirmation that your work has been proposed upstream" %} +{% image src="/docs/img/gastown/wasteland/wl-evidence-submitted.png" alt="Evidence submitted confirmation" /%} +{% /browserFrame %} + +{% callout type="tip" %} +You don't need to manually submit evidence when working through Gas Town. The Mayor handles it automatically when the bead closes. If you're using the standalone `wl` CLI, you'd run `wl done --evidence "..."` manually. +{% /callout %} + +## 7. Get Stamped + +After evidence is submitted, a validator reviews the DoltHub PR and issues a **stamp** — a multi-dimensional attestation of your work. + +Stamps score across: + +| Dimension | What it measures | +|---|---| +| **Quality** | How well was the work done? (`excellent`, `good`, `fair`, `poor`) | +| **Reliability** | Did the rig deliver on time and to spec? | + +Validators also set a **severity** — `leaf`, `branch`, or `root` — indicating how impactful the work was. Skill tags (e.g., `go`, `federation`) are attached to build the completer's profile. + + + +The yearbook rule applies: **you can't stamp your own work**. Your reputation is built exclusively from what others write about you. + +Reputation updates asynchronously — you don't need to wait around. Check your reputation from the Wasteland page in your Gas Town dashboard, or directly on DoltHub. You can also use `wl profile ` from the CLI to look up any rig's profile. + +## 8. What's Next + +You've completed your first wasteland cycle. Here's where to go from here: + +- **Post your own wanted items** — See [Administration](/docs/code-with-ai/gastown/wasteland/admin) for posting work to the board +- **Run your own wasteland** — A private instance for your team with scoped reputation and controlled validators +- **Learn about stamps and reputation** — See [Concepts](/docs/code-with-ai/gastown/wasteland/concepts) for the deep dive +- **Troubleshoot issues** — See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland) if something went wrong diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md new file mode 100644 index 00000000000..851700a4a13 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/settings.md @@ -0,0 +1,155 @@ +--- +title: "Wasteland Settings" +description: "Wasteland connection settings in the Gas Town dashboard, DoltHub token setup, and rig identity" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +Reference for every Wasteland-related setting in your Gas Town dashboard, from upstream selection to DoltHub PAT management. + +Access these settings from your town dashboard → **Settings** → **Wasteland** tab. + + +{% browserFrame url="app.kilo.ai/gastown/town/settings/wasteland" caption="Wasteland settings tab in your Gas Town dashboard" %} +{% image src="/docs/img/gastown/wasteland/gt-wasteland-settings.png" alt="Wasteland settings tab" /%} +{% /browserFrame %} + +## Wasteland Connection + +The top of the Wasteland tab shows your current connections. Each row represents a wasteland instance your town has joined. + +| Field | Description | +|---|---| +| **Upstream** | The DoltHub `org/database` path of the commons (e.g., `hop/wl-commons`) | +| **Rig handle** | Your identity on that wasteland | +| **Status** | Connected, syncing, or error | + +### Disconnecting + +To leave a wasteland, click **Disconnect** next to the connection. This: + +1. Removes the connection from your town's settings +2. Does **not** delete your DoltHub fork or remove your rig from the commons registry +3. Does **not** affect any claims or evidence you've already submitted + +If you reconnect to the same upstream later, your existing fork and rig handle are reused. + +{% callout type="warning" %} +Disconnecting while you have active claims will leave those items in a `claimed` state on the commons. Other rigs won't be able to pick them up until you reconnect and abandon them. Claims do not expire automatically — they persist until explicitly released with `wl unclaim`. +{% /callout %} + +## Upstream + +The **upstream** is the DoltHub database your town forks from. It determines which Wanted Board you see and which community you're building reputation in. + +| Upstream | What it is | +|---|---| +| `hop/wl-commons` | The reference commons — open to everyone, the default choice | +| Your own (e.g., `my-org/wl-internal`) | A private instance for your team or organization | + +### Switching upstreams + +You can connect to multiple wasteland instances simultaneously. Each connection has its own fork, rig handle, and Wanted Board. To switch between them, select the connection in the Wasteland tab. + +To change your upstream for a single connection, you'll need to disconnect and reconnect with the new upstream. Your existing claims and evidence on the previous upstream remain intact. + +## Rig Handle + +Your **rig handle** is your town's identity on the wasteland. It's an `org/repo`-style identifier (e.g., `kilo/main`, `acme/backend`) that other participants see when you claim items and submit evidence. + +### Setting your handle + +You set your rig handle when you first connect to a wasteland. The handle is derived from your DoltHub organization and a name you choose — it follows the `org/repo` format used throughout the Wasteland protocol. + +### Changing your handle + +Rig handles are **sticky by design**. Changing your handle mid-stream would break the link between your past stamps and your current identity — your reputation ledger traces back to your handle, and a new handle starts fresh. + +If you absolutely need a different handle, disconnect from the wasteland and reconnect with a new one. Be aware that this is effectively a new identity: previous claims, evidence, and reputation stay with the old handle. + +{% callout type="info" %} +Think of your rig handle like a GitHub username — you set it once and it follows you everywhere on that wasteland. Choose something stable and recognizable. +{% /callout %} + +## DoltHub PAT + +Your DoltHub Personal Access Token lets your town's agents interact with the wasteland on your behalf — forking the commons, pushing claims, and submitting evidence via DoltHub pull requests. + +### Creating a PAT + +1. Go to [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials) +2. Create a new token +3. Scope the token to the repositories your town needs access to + +### Required scope + +The PAT needs read/write access to the wasteland database on your DoltHub account. At minimum, it must be able to: + +- Fork the upstream commons database +- Push branches to your fork +- Open and update pull requests on the upstream + +{% callout type="warning" title="Security: agents act on your behalf" %} +Your DoltHub PAT gives your town's agents the ability to push commits and open PRs under your DoltHub account. **Use a fine-grained PAT scoped to only the repositories your town needs.** Never use a global token with full account access. Since agents act autonomously, limiting the token's scope reduces the blast radius if it's ever compromised. +{% /callout %} + +### Rotating your PAT + +To rotate an expired or compromised token: + +1. Generate a new PAT at [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials) +2. Go to **Settings** → **Wasteland** tab +3. Update the PAT field with the new token +4. Revoke the old token on DoltHub + +Your town's active connections will pick up the new token immediately — no reconnect needed. + +### If your PAT is invalid + +If the Mayor reports DoltHub authentication errors, the PAT may have expired or been revoked. Check: + +- The token hasn't been revoked on [dolthub.com/settings/credentials](https://www.dolthub.com/settings/credentials) +- The token's scope still covers the wasteland database +- The token hasn't hit a rate limit + +See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland) for detailed diagnosis steps. + +## Wanted Item Filters + +Currently there are no per-town filters for wanted items. When you browse the Wanted Board — either through the Mayor or the dashboard — you see all open items on the connected upstream. + +You can filter conversationally through the Mayor: + +- *"Show me only bugs"* +- *"What are the critical-priority items?"* +- *"Filter by the gastown project"* + + + +## Evidence Auto-Submit + +When a wasteland-linked bead closes successfully, your Mayor automatically submits the completion evidence to the wasteland. This is always-on behavior — there is no toggle to disable it. + +The auto-submit flow: + +1. The bead closes (passes refinery review, merges successfully) +2. The Mayor collects the commit SHA and PR URL +3. It runs the equivalent of `wl done --evidence ""` on your behalf +4. Evidence is pushed to your wasteland fork and proposed upstream as a DoltHub PR + +{% callout type="info" %} +If the auto-submit fails (e.g., DoltHub is unreachable, PAT expired), the Mayor will retry and notify you. The evidence isn't lost — it can be resubmitted once the issue is resolved. +{% /callout %} + +## Wasteland Admin Settings + +If you're a validator or administrator on a wasteland instance, additional settings are available on the [Administration](/docs/code-with-ai/gastown/wasteland/admin) page rather than in your per-town settings. + +Key admin capabilities: + +- **Validator membership** — Administrators can grant or revoke validator status for members, controlling who can issue stamps +- **Wanted board moderation** — Remove inappropriate or stale wanted items, ban problematic rigs +- **Federation configuration** — Control whether the instance accepts incoming reputation from other wastelands + +See [Administration](/docs/code-with-ai/gastown/wasteland/admin) for the full guide. diff --git a/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md new file mode 100644 index 00000000000..1f367d00d92 --- /dev/null +++ b/packages/kilo-docs/pages/code-with-ai/gastown/wasteland/workflow.md @@ -0,0 +1,223 @@ +--- +title: "Wasteland Workflow" +description: "End-to-end workflow for working on Wasteland items: browse, claim, work, submit evidence, and get stamped" +noindex: true +--- + +# {% $markdoc.frontmatter.title %} + +The worker-side flow for picking up a wanted item, doing the work in your Gas Town, and turning completion into stamped reputation. + +{% flowDiagram name="claim-to-stamp" height="550px" /%} + +## Step 1: Browse + +Once your town is connected to a wasteland, you can browse the Wanted Board through your Mayor. Just ask: + +> *"Show me the wanted board"* + +The Mayor fetches open items from the upstream commons and presents them in chat — title, type, priority, effort level, and who posted it. + +{% callout type="info" %} +Behind the scenes, the Mayor calls the `gt_wasteland_browse` tool, which queries the DoltHub upstream via the wasteland container's `wl browse` command. The results are cached briefly (30s) and invalidated on every mutation. +{% /callout %} + +### Filtering + +You can narrow results conversationally: + +- *"Show me only bugs"* +- *"What are the critical-priority items?"* +- *"Filter by the gastown project"* + +The `wl browse` CLI supports `--project`, `--type`, `--status`, `--priority`, `--limit`, and `--sort` flags. The Mayor translates your request into the appropriate filters. + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland" caption="The Wanted Board — browse and filter open tasks through the Mayor" %} +{% image src="/docs/img/gastown/wasteland/wl-wanted-board.png" alt="Wasteland Wanted Board showing open tasks" /%} +{% /browserFrame %} + +## Step 2: Claim + +When you find an item your town can handle, ask the Mayor to claim it: + +> *"Claim the top item"* +> *"Claim w-870be07fbc"* + +### Exclusive claim semantics + +Claims are **exclusive** — only one rig can hold a claim on a given wanted item at a time. This prevents duplicate work across the federation. + +When you claim an item: + +1. **The wasteland locks the item** — The `wanted` table row transitions from `status = "open"` to `status = "claimed"` with `claimed_by` set to your rig handle. No other rig can claim the same item while you hold it. +2. **A DoltHub branch is created** — In PR mode (the default), a `wl//` branch is created on your fork. The claim commits a status update to this branch. +3. **A DoltHub PR is opened** — The branch is proposed upstream as a pull request. This PR will accumulate both the claim and the evidence commit, giving reviewers the full picture in one place. + +### Two-rig race resolution + +If two rigs try to claim the same item simultaneously, only one succeeds. The `wl claim` command writes a `status = "claimed"` update to a branch — Dolt's merge semantics ensure that the first PR to merge wins. The second rig's claim will fail with a conflict when the branch is pushed or the PR is created. + +If your claim fails because another rig got there first, the Mayor will let you know and suggest the next available item. + +### Claim TTL + +Claims do **not** expire automatically in the current protocol. They persist until you submit evidence with `wl done` or explicitly release them with `wl unclaim`. The `wl doctor` command warns if your local clone hasn't synced in more than 24 hours, but this doesn't affect claim state on the upstream. + +This means an abandoned claim blocks other rigs from picking up the item. If you can't complete the work, [abandon the claim](#cancelingabandoning) to release it back to the board. + +### Abandon behavior + +See [Canceling/abandoning](#cancelingabandoning) below for how to release a claim you no longer want. + +## Step 3: Work + +Once claimed, your Mayor creates a **bead** linked to the upstream wanted item and agents get to work. + +### Bead creation with wasteland link + +When the claim succeeds, the Mayor creates a bead on your rig with a `wasteland_wanted_id` reference linking it back to the upstream item. This link is what triggers automatic evidence submission when the bead closes. + +The bead follows the standard Gas Town lifecycle: + +1. The reconciler assigns the bead to an available polecat +2. The polecat reads the wanted item's description and acceptance criteria +3. It makes code changes, pushes a branch, and the bead moves to `in_review` +4. The refinery reviews the work + + +{% browserFrame url="app.kilo.ai/gastown/town/rig/bead" caption="A bead with a wasteland link — the upstream item badge connects local work to the Wanted Board" %} +{% image src="/docs/img/gastown/wasteland/gt-bead-with-wasteland-link.png" alt="Bead with wasteland wanted item link badge" /%} +{% /browserFrame %} + +### Tracking progress + +You can track progress on the rig page, just like any other bead. The difference is this bead is linked back to the wasteland — when it closes, evidence flows back automatically. + +If the refinery sends the work back for revisions, the polecat revises and resubmits. The bead stays in the Gas Town pipeline until it passes review. + +## Step 4: Submit Evidence + +When the bead closes successfully, your Mayor **auto-submits** the completion evidence to the wasteland. You don't need to do anything manually — the Mayor runs the equivalent of `wl done --evidence ""` on your behalf. + +### Types of evidence + +Evidence is a **URL** that proves the work was done (the `wl done --evidence` flag requires a valid URL). Acceptable types: + +| Type | Example | +|---|---| +| **Pull request URL** | `https://github.com/org/repo/pull/123` | +| **Git commit SHA** | `https://github.com/org/repo/commit/abc123` | +| **Deployed URL** | `https://staging.example.com/new-feature` | +| **Artifact URL** | `https://registry.example.com/package@1.2.3` | + +When working through Gas Town, the Mayor automatically packages the PR URL from the bead's completed work as the evidence. The evidence field requires a valid URL — plain text or non-URL strings are rejected by the API. + +### How evidence flows + +1. The bead closes (passes refinery review, merges successfully) +2. The Mayor collects the commit SHA and PR URL +3. It calls `wl done --evidence ""` via the `gt_wasteland_done` tool +4. Evidence is pushed to your wasteland fork as a commit on the `wl//` branch +5. In PR mode, the existing DoltHub PR is updated with the evidence commit +6. The item transitions to `in_review` on the upstream, awaiting validator action + + +{% browserFrame url="app.kilo.ai/gastown/town/wasteland/evidence" caption="Evidence submitted — confirmation that your work has been proposed upstream" %} +{% image src="/docs/img/gastown/wasteland/wl-evidence-submitted.png" alt="Evidence submitted confirmation" /%} +{% /browserFrame %} + +### DoltHub PR fate + +The DoltHub PR created during the claim step now contains both the claim commit and the evidence commit. Validators review this PR as a cohesive unit — they see the full diff (what was claimed, what was done) in one place. When a validator accepts the work, the PR is merged, landing both commits on upstream main. + +If evidence submission fails (e.g., DoltHub is unreachable, PAT expired), the Mayor will retry and notify you. The evidence isn't lost — it can be resubmitted once the issue is resolved. + +{% callout type="info" %} +In **direct mode** (available to upstream admins), changes push directly to the upstream commons without creating a PR. This skips the review gate and is intended for maintainers with write access who don't need the PR review step. Direct mode is off by default. +{% /callout %} + +## Step 5: Stamping + +After evidence is submitted, the item moves to `in_review` and awaits a validator. Stamping is **asynchronous** — you don't need to wait around. + +### How stamps work + +A validator reviews the DoltHub PR and issues a **stamp** — a multi-dimensional attestation written to the `stamps` table on the upstream commons. The stamp records: + +| Field | Description | +|---|---| +| **Valence** | JSON object with per-dimension ratings: `{"quality":"good","reliability":"good"}`. Quality is set via `wl accept --quality` (`excellent`, `good`, `fair`, `poor`). | +| **Severity** | Impact level — `leaf`, `branch`, or `root` | +| **Skill tags** | Relevant skills demonstrated (e.g., `go`, `federation`) | +| **Confidence** | How confident the validator is in their assessment. Stored as a string or number in the schema. | +| **Message** | Free-form justification from the validator (`wl accept --message`) | + + + +The validator's stamp commits to the `stamps` table via a DoltHub PR (or direct push in admin mode). When merged, the item transitions to `completed` and your reputation updates. + +### The yearbook rule + +**You can't stamp your own work.** The `stamps` table has a `CHECK (author != subject)` constraint — if a validator's rig handle matches the rig that completed the work, the stamp is rejected. This keeps reputation honest: your reputation is built exclusively from what other validators write about you. + +### Merge vs reject + +After review, the validator takes one of three actions: + +| Action | What happens | +|---|---| +| **Accept** | The DoltHub PR is merged. A stamp row is created. The item moves to `completed`. Your reputation updates. | +| **Reject** | Evidence is cleared and the item goes back to `claimed`. You can resubmit with new evidence. No stamp is issued. | +| **Close** | The item moves to `completed` without a stamp. No reputation change. Used when work is no longer relevant. | + +## Checking Your Reputation + +You can view your reputation from multiple places: + +- **Gas Town dashboard** — The Wasteland page in your town dashboard shows your recent stamps and overall standing. +- **DoltHub directly** — Query the `stamps` table on the upstream commons where `subject = ''`. +- **`wl profile`** — Run `wl profile ` from the CLI to see any rig's public profile and stamp history. + +### Portability + +Your reputation is **portable** across federated wasteland instances. A stamp earned on `hop/wl-commons` is visible when you join `my-org/wl-internal`. Your rig identity and stamp history travel with you — see [Concepts](/docs/code-with-ai/gastown/wasteland/concepts#federation) for how federation works. + +## Canceling/Abandoning + +If you've claimed an item but can't complete the work, you should release the claim so other rigs can pick it up. + +### Releasing a claim + +Ask the Mayor to abandon the item: + +> *"Abandon w-870be07fbc"* + +The Mayor runs the equivalent of `wl unclaim `, which transitions the item back to `status = "open"` and clears `claimed_by`. Other rigs can then claim it. (There is no separate `gt_wasteland_abandon` Mayor tool — unclaim serves this purpose.) + +### Natural expiry + +Claims do **not** expire automatically. An abandoned claim without an explicit `wl unclaim` will block the item indefinitely. Always release claims you can't complete. + +{% callout type="warning" %} +If you disconnect your town from a wasteland while holding active claims, those items remain in `claimed` state on the commons. Other rigs can't pick them up until you reconnect and abandon them. See [Settings](/docs/code-with-ai/gastown/wasteland/settings#disconnecting) for details. +{% /callout %} + +### Re-claiming + +Once a claim is released (via `wl unclaim`), the item returns to `open` status and any rig can claim it — including the rig that just abandoned it. There's no cooldown or penalty for re-claiming. + +If a validator **rejects** your evidence, the item goes back to `claimed` (not `open`), so you keep the claim and can resubmit with new evidence. You don't need to re-claim. + +## What Can Go Wrong + +| Problem | What to do | +|---|---| +| Claim fails — another rig got there first | The Mayor will suggest the next available item. Try claiming a different one. | +| Evidence auto-submit fails | Check your DoltHub PAT is valid. The Mayor will retry. See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | +| Stuck claim — you can't complete the work | [Abandon the claim](#cancelingabandoning) to release it back to the board. | +| Item stays in `in_review` forever | Validators may be backlogged. You can check the PR status on DoltHub directly. | +| `PRECONDITION_FAILED` on `wl done` | The item may not be in `claimed` state, or your credential may be invalid. See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | +| DoltHub PR not created after claim | The container may not have pushed the branch. Check container status. See the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | +| Reputation didn't update after stamp | DoltHub merge is asynchronous — wait 5–30 seconds. If still missing, see the [Wasteland overview](/docs/code-with-ai/gastown/wasteland). | +| Can't claim — "rig not found" | Your rig registration PR may not be merged yet. Ask a validator to merge it. | diff --git a/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md b/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md index 76abc8decb2..ec23078ad6e 100644 --- a/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md +++ b/packages/kilo-docs/pages/code-with-ai/platforms/mobile.md @@ -5,20 +5,44 @@ description: "Using Kilo Code on iOS and Android" # Mobile Apps -Kilo Code is coming to mobile! Soon you'll be able to use Kilo Code's powerful AI coding capabilities directly from your iOS or Android device. +Use Kilo Code from your phone to keep coding sessions moving while you are away from your desk. The mobile app connects to Cloud Agents, KiloClaw, and remote sessions from your local CLI or editor extensions. -{% callout type="info" title="Coming Soon" %} -Mobile apps for iOS and Android are currently in development. Sign up to be notified when they launch! +{% callout type="info" title="Android app available now" %} +Install Kilo Code for Android from [Google Play](https://kilocode.onelink.me/ZzZZ/r10ni4zy). {% /callout %} -## iOS App +## What you can do -The Kilo Code iOS app will bring AI-powered coding assistance to your iPhone and iPad. +The mobile app lets you: -[Learn more about the iOS app →](https://kilo.ai/features/ios-app) +- View and manage Kilo Code sessions, including remote CLI and extension sessions running on your local machine. +- Chat with KiloClaw from your phone. +- Spawn Cloud Agents and code directly from the app. +- Monitor and view all non-remote sessions in one place. +- Create, onboard, and manage KiloClaw instances. + +{% imageGallery columns="3" width="220px" %} +{% image src="/docs/img/mobile-apps/home.webp" alt="Kilo Code mobile home screen showing KiloClaw and active agent sessions" caption="Start coding tasks, open KiloClaw, and resume active sessions from the mobile home screen." /%} + +{% image src="/docs/img/mobile-apps/new-session.webp" alt="Kilo Code mobile new session screen with coding mode selector" caption="Create a new Cloud Agent session and choose the right mode for the task." /%} + +{% image src="/docs/img/mobile-apps/session-chat.webp" alt="Kilo Code mobile session chat with an active coding task" caption="Review progress and continue coding conversations from the mobile app." /%} +{% /imageGallery %} + +{% imageGallery columns="2" width="220px" %} +{% image src="/docs/img/mobile-apps/kiloclaw-chat.webp" alt="KiloClaw chat in the Kilo Code mobile app" caption="Chat with KiloClaw on mobile." /%} + +{% image src="/docs/img/mobile-apps/session-filters.webp" alt="Kilo Code mobile session filter panel for Cloud Extension CLI Slack and other platforms" caption="Filter sessions by platform and project, including Cloud, Extension, CLI, Slack, and other sessions." /%} +{% /imageGallery %} ## Android App -The Kilo Code Android app will let you code with AI assistance on your Android phone or tablet. +The Android app is available now on Google Play. -[Learn more about the Android app →](https://kilo.ai/features/android-app) +[Install the Android app →](https://kilocode.onelink.me/ZzZZ/r10ni4zy) + +## iOS App + +The iOS app is in review with the App Store team and will be available soon. You can already sign up for the iOS waitlist to be notified when it launches. + +[Join the iOS app waitlist →](https://kilo.ai/features/ios-app) diff --git a/packages/kilo-docs/previous-docs-redirects.js b/packages/kilo-docs/previous-docs-redirects.js index 44fe2ff0fdb..1d872b63f5d 100644 --- a/packages/kilo-docs/previous-docs-redirects.js +++ b/packages/kilo-docs/previous-docs-redirects.js @@ -865,4 +865,10 @@ module.exports = [ basePath: false, permanent: true, }, + { + source: "/docs/code-with-ai/gastown/wasteland/troubleshooting", + destination: "/docs/code-with-ai/gastown/wasteland", + basePath: false, + permanent: true, + }, ] diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-bead-with-wasteland-link.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-bead-with-wasteland-link.png new file mode 100644 index 00000000000..f3f25d6fb05 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-bead-with-wasteland-link.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-claim-detail-drawer.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-claim-detail-drawer.png new file mode 100644 index 00000000000..d74355c16bb Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-claim-detail-drawer.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-mayor-claiming.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-mayor-claiming.png new file mode 100644 index 00000000000..0f4f4b90fb0 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-mayor-claiming.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-connect-dialog.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-connect-dialog.png new file mode 100644 index 00000000000..1d2a1008d8f Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-connect-dialog.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-settings.png b/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-settings.png new file mode 100644 index 00000000000..76b44beef80 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/gt-wasteland-settings.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-admin-review-inbox.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-admin-review-inbox.png new file mode 100644 index 00000000000..12306817e5a Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-admin-review-inbox.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-claim-drawer.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-claim-drawer.png new file mode 100644 index 00000000000..394655e9515 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-claim-drawer.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-evidence-submitted.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-evidence-submitted.png new file mode 100644 index 00000000000..0d25c696244 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-evidence-submitted.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-post-form.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-post-form.png new file mode 100644 index 00000000000..2112b4c3279 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-post-form.png differ diff --git a/packages/kilo-docs/public/img/gastown/wasteland/wl-wanted-board.png b/packages/kilo-docs/public/img/gastown/wasteland/wl-wanted-board.png new file mode 100644 index 00000000000..60218619367 Binary files /dev/null and b/packages/kilo-docs/public/img/gastown/wasteland/wl-wanted-board.png differ diff --git a/packages/kilo-docs/public/img/mobile-apps/home.webp b/packages/kilo-docs/public/img/mobile-apps/home.webp new file mode 100644 index 00000000000..8df94c62ca5 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/home.webp differ diff --git a/packages/kilo-docs/public/img/mobile-apps/kiloclaw-chat.webp b/packages/kilo-docs/public/img/mobile-apps/kiloclaw-chat.webp new file mode 100644 index 00000000000..1df99e23e20 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/kiloclaw-chat.webp differ diff --git a/packages/kilo-docs/public/img/mobile-apps/new-session.webp b/packages/kilo-docs/public/img/mobile-apps/new-session.webp new file mode 100644 index 00000000000..e8ca7dda341 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/new-session.webp differ diff --git a/packages/kilo-docs/public/img/mobile-apps/session-chat.webp b/packages/kilo-docs/public/img/mobile-apps/session-chat.webp new file mode 100644 index 00000000000..26a9b82adf6 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/session-chat.webp differ diff --git a/packages/kilo-docs/public/img/mobile-apps/session-filters.webp b/packages/kilo-docs/public/img/mobile-apps/session-filters.webp new file mode 100644 index 00000000000..a0f684b9cd6 Binary files /dev/null and b/packages/kilo-docs/public/img/mobile-apps/session-filters.webp differ diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png index ff195b4d918..edc40570888 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/chat/message-list-tool-to-queued-user-spacing-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3f37193389ad7937c7b2103ad2fe3dd63123a0d4f60a0fbfd65b31d3982c092 -size 12105 +oid sha256:dbaab3a1ff10eac8f710b0ed8ddc99d96188c06a09c227e634ca712bcee12130 +size 11543 diff --git a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png index 4d612a7ad03..ddcba78b314 100644 --- a/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png +++ b/packages/kilo-docs/public/img/screenshot-tests/kilo-vscode/visual-regression/components-shell/shell-execution-chromium-linux.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74a42ec77a8ac8d5a1c555b46e3d550c9acf3198f45cc87da6582c0ff924d5f9 -size 17677 +oid sha256:7cabfff215cef485ec913b9a082aaac844275e506e88ba0fab112d95ff6de329 +size 18200 diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 7dd1f9304fa..d7a778caa65 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "أوقف لقطات Kilo لهذا المشروع. ستفقد ميزة التراجع/الإعادة لتعديلات Kilo، لكن git سيستمر في تتبع كل شيء.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "فتح في عارض الفروقات", + "ui.messagePart.shell.command": "الأمر", + "ui.messagePart.shell.output": "المخرجات", + "ui.messagePart.openInEditor": "فتح في المحرر", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "كان هذا مفيدًا", diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index e79ef39bf30..bbf53cd2ff3 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Desligue os snapshots do Kilo para este projeto. Você perde desfazer/refazer das mudanças feitas pelo Kilo, mas o git continua rastreando tudo.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Abrir no Visualizador de Diferenças", + "ui.messagePart.shell.command": "Comando", + "ui.messagePart.shell.output": "Saída", + "ui.messagePart.openInEditor": "Abrir no Editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Isso foi útil", diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index e23e964bbd5..2e4983260dc 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -96,7 +96,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Isključi Kilo snapshotove za ovaj projekat. Izgubićete poništi/vrati za izmjene koje napravi Kilo, ali git i dalje prati sve.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Otvori u pregledniku razlika", + "ui.messagePart.shell.command": "Naredba", + "ui.messagePart.shell.output": "Izlaz", + "ui.messagePart.openInEditor": "Otvori u editoru", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Ovo je bilo korisno", diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index 2a086970b40..e930733acbe 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Slå Kilos snapshots fra for dette projekt. Du mister fortryd/gentag for Kilo-ændringer, men git sporer stadig alt.", - "ui.messagePart.openInDiffViewer": "Åbn i diff-viser", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Åbn i Diff-visning", + "ui.messagePart.shell.command": "Kommando", + "ui.messagePart.shell.output": "Output", + "ui.messagePart.openInEditor": "Åbn i editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Dette var nyttigt", diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index 437f5fc649f..ce73601069b 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Kilo-Snapshots für dieses Projekt ausschalten. Rückgängig/Wiederherstellen für Kilo-Änderungen ist nicht mehr möglich, aber git verfolgt weiterhin alles.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Im Diff-Viewer öffnen", + "ui.messagePart.shell.command": "Befehl", + "ui.messagePart.shell.output": "Ausgabe", + "ui.messagePart.openInEditor": "Im Editor öffnen", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Das war hilfreich", diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index bd80d92857d..7f908032995 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -99,6 +99,10 @@ export const dict = { // Edit-tool header: hover-revealed action opening the diff in a full tab. "ui.messagePart.openInDiffViewer": "Open in Diff Viewer", + // Shell-tool section labels and actions. + "ui.messagePart.shell.command": "Command", + "ui.messagePart.shell.output": "Output", + "ui.messagePart.openInEditor": "Open in Editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "This was helpful", diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index 8b05522cb9f..07af68a1a0f 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Apaga las instantáneas de Kilo para este proyecto. Perderás deshacer/rehacer de los cambios de Kilo, pero git seguirá rastreando todo.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Abrir en el visor de diferencias", + "ui.messagePart.shell.command": "Comando", + "ui.messagePart.shell.output": "Salida", + "ui.messagePart.openInEditor": "Abrir en el editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Esto fue útil", diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index b64ace8f1f9..2faf61d345b 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Désactivez les instantanés Kilo pour ce projet. Vous perdez l'annulation/restauration des modifications faites par Kilo, mais git continue de tout suivre.", - "ui.messagePart.openInDiffViewer": "Ouvrir dans la visionneuse de différences", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Ouvrir dans le visualiseur de différences", + "ui.messagePart.shell.command": "Commande", + "ui.messagePart.shell.output": "Sortie", + "ui.messagePart.openInEditor": "Ouvrir dans l'éditeur", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "C'était utile", diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index 9d5214c7a48..f30eae15557 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -89,7 +89,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "このプロジェクトでは Kilo のスナップショットを無効にします。Kilo による変更の取り消し/やり直しはできなくなりますが、git は引き続きすべてを追跡します。", - "ui.messagePart.openInDiffViewer": "差分ビューアで開く", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "差分ビューアーで開く", + "ui.messagePart.shell.command": "コマンド", + "ui.messagePart.shell.output": "出力", + "ui.messagePart.openInEditor": "エディタで開く", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "役に立ちました", diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 6308cf9fc68..8a9692001fe 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -89,7 +89,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "이 프로젝트의 Kilo 스냅샷을 끕니다. Kilo 변경에 대한 실행 취소/다시 실행은 사용할 수 없지만 git은 여전히 모든 것을 추적합니다.", - "ui.messagePart.openInDiffViewer": "Diff 뷰어에서 열기", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "차이점 뷰어에서 열기", + "ui.messagePart.shell.command": "명령어", + "ui.messagePart.shell.output": "출력", + "ui.messagePart.openInEditor": "편집기에서 열기", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "도움이 됐어요", diff --git a/packages/kilo-i18n/src/nl.ts b/packages/kilo-i18n/src/nl.ts index 4fb55eddca5..142c0958286 100644 --- a/packages/kilo-i18n/src/nl.ts +++ b/packages/kilo-i18n/src/nl.ts @@ -93,7 +93,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Zet Kilo-snapshots uit voor dit project. Je verliest ongedaan maken/opnieuw doen van Kilo-wijzigingen, maar git blijft alles volgen.", - "ui.messagePart.openInDiffViewer": "Openen in diff-viewer", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Openen in Diff-weergave", + "ui.messagePart.shell.command": "Opdracht", + "ui.messagePart.shell.output": "Uitvoer", + "ui.messagePart.openInEditor": "Openen in editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Dit was nuttig", diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index 915801390af..e7f223b923b 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Slå av Kilos snapshots for dette prosjektet. Du mister angre/gjør om for Kilo-endringer, men git fortsetter å spore alt.", - "ui.messagePart.openInDiffViewer": "Åpne i diff-viser", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Åpne i diff-visning", + "ui.messagePart.shell.command": "Kommando", + "ui.messagePart.shell.output": "Utdata", + "ui.messagePart.openInEditor": "Åpne i editor", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Dette var nyttig", diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index 67b83e46c29..8cfd61e3c2b 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Wyłącz snapshoty Kilo dla tego projektu. Stracisz cofanie/przywracanie zmian Kilo, ale git nadal śledzi wszystko.", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "Otwórz w podglądzie różnic", + "ui.messagePart.shell.command": "Polecenie", + "ui.messagePart.shell.output": "Wyjście", + "ui.messagePart.openInEditor": "Otwórz w edytorze", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "To było pomocne", diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index d9cff803db5..2a923436c22 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Выключите снимки Kilo для этого проекта. Вы потеряете отмену/повтор изменений Kilo, но git по-прежнему отслеживает всё.", - "ui.messagePart.openInDiffViewer": "Открыть в просмотрщике различий", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Открыть в просмотре различий", + "ui.messagePart.shell.command": "Команда", + "ui.messagePart.shell.output": "Вывод", + "ui.messagePart.openInEditor": "Открыть в редакторе", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Это было полезно", diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index be90eddc10a..ae0c5a4d88c 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "ปิดสแน็ปช็อตของ Kilo สำหรับโปรเจกต์นี้ คุณจะสูญเสียการยกเลิก/ทำซ้ำสำหรับการเปลี่ยนแปลงของ Kilo แต่ git ยังคงติดตามทุกอย่าง", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "เปิดในตัวดูความแตกต่าง", + "ui.messagePart.shell.command": "คำสั่ง", + "ui.messagePart.shell.output": "ผลลัพธ์", + "ui.messagePart.openInEditor": "เปิดในตัวแก้ไข", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "สิ่งนี้มีประโยชน์", diff --git a/packages/kilo-i18n/src/tr.ts b/packages/kilo-i18n/src/tr.ts index 0ebf2a73f24..6297ed3295a 100644 --- a/packages/kilo-i18n/src/tr.ts +++ b/packages/kilo-i18n/src/tr.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Bu proje için Kilo anlık görüntülerini kapat. Kilo değişiklikleri için geri alma/yeniden yapmayı kaybedersiniz, ancak git her şeyi izlemeye devam eder.", - "ui.messagePart.openInDiffViewer": "Fark görüntüleyicide aç", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Fark Görüntüleyicide Aç", + "ui.messagePart.shell.command": "Komut", + "ui.messagePart.shell.output": "Çıktı", + "ui.messagePart.openInEditor": "Düzenleyicide Aç", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Bu yardımcı oldu", diff --git a/packages/kilo-i18n/src/uk.ts b/packages/kilo-i18n/src/uk.ts index 3e325a33f7d..afc11ae14e1 100644 --- a/packages/kilo-i18n/src/uk.ts +++ b/packages/kilo-i18n/src/uk.ts @@ -91,7 +91,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "Вимкніть знімки Kilo для цього проєкту. Ви втратите скасування/повторення для змін Kilo, але git продовжить відстежувати все.", - "ui.messagePart.openInDiffViewer": "Відкрити у переглядачі відмінностей", + // Edit-tool header and shell-tool section labels + "ui.messagePart.openInDiffViewer": "Відкрити в переглядачі відмінностей", + "ui.messagePart.shell.command": "Команда", + "ui.messagePart.shell.output": "Вивід", + "ui.messagePart.openInEditor": "Відкрити в редакторі", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "Це було корисно", diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index 29a57c5290d..37954ff59d2 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -86,7 +86,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "关闭本项目的 Kilo 快照。你将失去对 Kilo 更改的撤销/重做,但 git 仍会追踪所有内容。", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "在差异查看器中打开", + "ui.messagePart.shell.command": "命令", + "ui.messagePart.shell.output": "输出", + "ui.messagePart.openInEditor": "在编辑器中打开", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "这有帮助", diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index 08582812567..5561173b89d 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -86,7 +86,11 @@ export const dict = { "snapshot.slowRepo.answer.disable.description": "關閉本專案的 Kilo 快照。你將失去對 Kilo 變更的撤銷/重做,但 git 仍會追蹤所有內容。", + // Edit-tool header and shell-tool section labels "ui.messagePart.openInDiffViewer": "在差異檢視器中開啟", + "ui.messagePart.shell.command": "命令", + "ui.messagePart.shell.output": "輸出", + "ui.messagePart.openInEditor": "在編輯器中開啟", // Message feedback (thumbs up/down per assistant response) "ui.message.feedback.helpful": "這有幫助", diff --git a/packages/kilo-jetbrains/backend/build.gradle.kts b/packages/kilo-jetbrains/backend/build.gradle.kts index b4cae1beac5..5924d04a5f9 100644 --- a/packages/kilo-jetbrains/backend/build.gradle.kts +++ b/packages/kilo-jetbrains/backend/build.gradle.kts @@ -43,6 +43,7 @@ openApiGenerate { "anyOf<>" to "kotlin.Any", "number" to "kotlin.Double", "decimal" to "kotlin.Double", + "integer" to "kotlin.Long", )) openapiNormalizer.set(mapOf( "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING" to "true", diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt index b5e014623e8..dd649bfd1ec 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendSessionManager.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.serialization.json.JsonPrimitive +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request @@ -156,6 +157,36 @@ class KiloBackendSessionManager( directories.remove(id) } + /** + * Rename a session by sending `PATCH /session/{id}?directory={dir}` with `{"title":"..."}`. + * + * Uses raw HTTP because the generated Kotlin client is build-time only and + * this repo already uses raw HTTP for session create and cloud operations. + */ + fun rename(id: String, dir: String, title: String): SessionDto { + val h = http ?: throw IllegalStateException("Session manager not started") + val url = base ?: throw IllegalStateException("Session manager not started") + val json = """{"title":"${escape(title)}"}""" + val patch = url.toHttpUrl().newBuilder() + .addPathSegment("session") + .addPathSegment(id) + .addQueryParameter("directory", dir) + .build() + val request = Request.Builder() + .url(patch) + .method("PATCH", json.toRequestBody("application/json".toMediaType())) + .build() + + h.newCall(request).execute().use { response -> + val raw = response.body?.string() + if (!response.isSuccessful) { + log.warn("Session rename failed: HTTP ${response.code}, body=$raw") + throw RuntimeException("Session rename failed: HTTP ${response.code} — $raw") + } + return KiloCliDataParser.parseSession(raw!!) + } + } + fun cloudSessions(dir: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto { val h = http ?: throw IllegalStateException("Session manager not started") val url = base ?: throw IllegalStateException("Session manager not started") @@ -238,9 +269,9 @@ class KiloBackendSessionManager( ), summary = s.summary?.let { SessionSummaryDto( - additions = it.additions.toInt(), - deletions = it.deletions.toInt(), - files = it.files.toInt(), + additions = it.additions.safeInt(), + deletions = it.deletions.safeInt(), + files = it.files.safeInt(), ) }, ) @@ -259,9 +290,9 @@ class KiloBackendSessionManager( ), summary = s.summary?.let { SessionSummaryDto( - additions = it.additions.toInt(), - deletions = it.deletions.toInt(), - files = it.files.toInt(), + additions = it.additions.safeInt(), + deletions = it.deletions.safeInt(), + files = it.files.safeInt(), ) }, ) @@ -269,8 +300,8 @@ class KiloBackendSessionManager( private fun statusDto(s: SessionStatus) = SessionStatusDto( type = s.type.value, message = s.message.ifBlank { null }, - attempt = s.attempt.toInt(), - next = s.next.toLong(), + attempt = s.attempt.safeInt(), + next = s.next, requestID = s.requestID.ifBlank { null }, ) @@ -288,4 +319,6 @@ class KiloBackendSessionManager( } } } + + private fun Long.safeInt() = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt() } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt index 3371a389004..9e097feae5c 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloCliDataParser.kt @@ -200,8 +200,8 @@ object KiloCliDataParser { val file = d.str("file") ?: return@mapNotNull null DiffFileDto( file = file, - additions = d.long("additions")?.toInt() ?: 0, - deletions = d.long("deletions")?.toInt() ?: 0, + additions = d.long("additions")?.safeInt() ?: 0, + deletions = d.long("deletions")?.safeInt() ?: 0, patch = d.str("patch"), ) } ?: emptyList() @@ -537,9 +537,9 @@ object KiloCliDataParser { ), summary = summary?.let { SessionSummaryDto( - additions = it.long("additions")?.toInt() ?: 0, - deletions = it.long("deletions")?.toInt() ?: 0, - files = it.long("files")?.toInt() ?: 0, + additions = it.long("additions")?.safeInt() ?: 0, + deletions = it.long("deletions")?.safeInt() ?: 0, + files = it.long("files")?.safeInt() ?: 0, ) }, ) @@ -564,7 +564,7 @@ object KiloCliDataParser { return SessionStatusDto( type = type, message = st.str("message"), - attempt = st.long("attempt")?.toInt(), + attempt = st.long("attempt")?.safeInt(), next = st.long("next"), requestID = st.str("requestID"), ) @@ -679,6 +679,8 @@ private fun JsonObject.num(key: String): Double? = private fun JsonObject.long(key: String): Long? = this[key]?.jsonPrimitive?.longOrNull +private fun Long.safeInt() = coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt() + private fun JsonObject?.map(key: String): Map { val obj = this?.get(key)?.jsonObject ?: return emptyMap() return obj.entries.mapNotNull { (name, value) -> diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt index 302e3bb9f3b..0192304ef4a 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloSessionRpcApiImpl.kt @@ -71,6 +71,11 @@ class KiloSessionRpcApiImpl : KiloSessionRpcApi { workspaces.get(dir).deleteSession(id) } + override suspend fun rename(id: String, directory: String, title: String): ai.kilocode.rpc.dto.SessionDto { + val dir = sessions.getDirectory(id, directory) + return sessions.rename(id, dir, title) + } + override suspend fun cloudSessions(directory: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto = sessions.cloudSessions(directory, cursor, limit, gitUrl) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt index 0f653f89f11..8419d47340d 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/app/KiloBackendSessionManagerTest.kt @@ -20,6 +20,7 @@ import kotlin.test.AfterTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -472,6 +473,134 @@ class KiloBackendSessionManagerTest { assertNotNull(result) } + // ------ Session rename ------ + + @Test + fun `rename patches session title and returns updated session`() = runBlocking { + mock.sessionRenameResponse = """{ + "id": "ses_1", + "slug": "s1", + "projectID": "prj_test", + "directory": "/test", + "title": "New Name", + "version": "1.0.0", + "time": {"created": 1000, "updated": 2000} + }""" + val app = setup() + ready(app) + + val session = app.sessions.rename("ses_1", "/test", "New Name") + + assertEquals("ses_1", session.id) + assertEquals("New Name", session.title) + val path = mock.lastSessionRenamePath ?: error("missing rename request") + assertTrue(path.startsWith("/session/ses_1?"), "Expected /session/ses_1?... got $path") + assertTrue(path.contains("directory=%2Ftest"), "Expected directory=/test in $path") + assertEquals("PATCH", mock.lastSessionRenameMethod) + assertEquals("""{"title":"New Name"}""", mock.lastSessionRenameBody) + } + + @Test + fun `rename response preserves directory parentId summary and timestamps`() = runBlocking { + mock.sessionRenameResponse = """{ + "id": "ses_1", + "slug": "s1", + "projectID": "prj_test", + "directory": "/worktree/path", + "title": "Renamed", + "version": "2.0.0", + "time": {"created": 1000, "updated": 9999}, + "parentID": "ses_parent", + "summary": {"additions": 5, "deletions": 3, "files": 2} + }""" + val app = setup() + ready(app) + + val session = app.sessions.rename("ses_1", "/test", "Renamed") + + assertEquals("/worktree/path", session.directory) + assertEquals("ses_parent", session.parentID) + assertEquals(1000.0, session.time.created) + assertEquals(9999.0, session.time.updated) + assertNotNull(session.summary) + assertEquals(5, session.summary!!.additions) + assertEquals(3, session.summary!!.deletions) + assertEquals(2, session.summary!!.files) + } + + @Test + fun `rename url-encodes session id and directory for special characters`() = runBlocking { + // Session IDs/directories may contain spaces, slashes, plus signs, and ampersands + val app = setup() + ready(app) + + app.sessions.rename("ses_a/b c", "/my dir/project", "New Name") + + val path = mock.lastSessionRenamePath ?: error("missing rename request") + assertTrue(path.startsWith("/session/ses_a%2Fb%20c?"), "Expected encoded session path in $path") + assertTrue(path.contains("directory=%2Fmy%20dir%2Fproject"), "Expected encoded directory in $path") + } + + @Test + fun `rename url-encodes ampersand plus and query separators`() = runBlocking { + val app = setup() + ready(app) + + app.sessions.rename("ses_a+b&c?d", "/path?a=1&b=2", "Title") + + val path: String = mock.lastSessionRenamePath ?: error("missing rename request") + val bare = path.substringBefore("?") + val query = path.substringAfter("?", "") + assertTrue(path.contains("/session/ses_a+b&c%3Fd?"), "Unexpected encoded session id: $path") + assertFalse(query.contains("/path?a=1&b=2"), "Directory must be encoded as one query value: $query") + assertTrue(query.contains("directory=%2Fpath%3Fa%3D1%26b%3D2"), "Unexpected encoded directory: $query") + } + + @Test + fun `rename encodes title in json body`() = runBlocking { + val app = setup() + ready(app) + + app.sessions.rename("ses_1", "/test", "Has \"quotes\" and \\ backslash") + + assertEquals("""{"title":"Has \"quotes\" and \\ backslash"}""", mock.lastSessionRenameBody) + } + + @Test + fun `rename encodes title control characters in json body`() = runBlocking { + val app = setup() + ready(app) + + app.sessions.rename("ses_1", "/test", "Line\nTab\tReturn\rBell\u0007") + + assertEquals("""{"title":"Line\nTab\tReturn\rBell\u0007"}""", mock.lastSessionRenameBody) + } + + @Test + fun `rename surfaces server failure`() = runBlocking { + mock.sessionRenameStatus = 500 + mock.sessionRenameResponse = """{"error":"boom"}""" + val app = setup() + ready(app) + + val err = assertFailsWith { + app.sessions.rename("ses_1", "/test", "New Name") + } + + assertTrue(err.message.orEmpty().contains("HTTP 500")) + assertTrue(err.message.orEmpty().contains("boom")) + } + + @Test + fun `rename throws before start`() = runBlocking { + val app = setup() + // Don't connect — manager is not started + + assertFailsWith { + app.sessions.rename("ses_1", "/test", "Title") + } + } + // ------ Session with summary ------ @Test @@ -496,4 +625,32 @@ class KiloBackendSessionManagerTest { assertEquals(7, session.summary!!.deletions) assertEquals(3, session.summary!!.files) } + + @Test + fun `status and summary long values clamp to shared dto int range`() = runBlocking { + mock.sessions = """[{ + "id": "ses_big", + "slug": "big", + "projectID": "prj", + "directory": "/d", + "title": "Big", + "version": "1", + "time": {"created": 1, "updated": 1}, + "summary": {"additions": 2147483648, "deletions": 9223372036854775807, "files": 3} + }]""" + mock.sessionStatuses = """{ + "ses_big": {"type":"retry","attempt":2147483648,"message":"retrying","next":9223372036854775807,"requestID":"req"} + }""" + val app = setup() + ready(app) + + val result = app.sessions.list("/d") + val session = result.sessions[0] + val status = result.statuses["ses_big"] ?: error("missing status") + + assertEquals(Int.MAX_VALUE, session.summary?.additions) + assertEquals(Int.MAX_VALUE, session.summary?.deletions) + assertEquals(Int.MAX_VALUE, status.attempt) + assertEquals(Long.MAX_VALUE, status.next) + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt index 7ddd445175f..b19ed6072dd 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/GeneratedApiModelSerializationTest.kt @@ -39,7 +39,7 @@ class GeneratedApiModelSerializationTest { @Test fun `empty provider list`() { - val src = """{"all":[],"default":{},"connected":[]}""" + val src = """{"all":[],"default":{},"connected":[],"failed":[]}""" val obj = json.decodeFromString(src) assertTrue(obj.all.isEmpty()) assertTrue(obj.default.isEmpty()) @@ -81,7 +81,8 @@ class GeneratedApiModelSerializationTest { } }], "default": {"code": "anthropic/claude-4"}, - "connected": ["anthropic"] + "connected": ["anthropic"], + "failed": [] }""" val obj = json.decodeFromString(src) assertEquals(1, obj.all.size) @@ -132,7 +133,8 @@ class GeneratedApiModelSerializationTest { } }], "default": {}, - "connected": [] + "connected": [], + "failed": [] }""" val obj = json.decodeFromString(src) val model = obj.all[0].models["free-model"]!! @@ -149,12 +151,22 @@ class GeneratedApiModelSerializationTest { "all": [], "default": {}, "connected": [], + "failed": [], "future_field": "value" }""" val obj = json.decodeFromString(src) assertTrue(obj.all.isEmpty()) } + @Test + fun `provider list defaults omitted collection fields`() { + val obj = json.decodeFromString("{}") + assertTrue(obj.all.isEmpty()) + assertTrue(obj.default.isEmpty()) + assertTrue(obj.connected.isEmpty()) + assertTrue(obj.failed.isEmpty()) + } + // ------ Agent ------ @Test @@ -287,4 +299,70 @@ class GeneratedApiModelSerializationTest { val list = json.decodeFromString>("[]") assertTrue(list.isEmpty()) } + + // ------ ProviderList200Response.failed non-empty ------ + + @Test + fun `provider list failed field handles non-empty list`() { + val src = """{ + "all": [], + "default": {}, + "connected": [], + "failed": ["openai", "anthropic"] + }""" + val obj = json.decodeFromString(src) + assertEquals(listOf("openai", "anthropic"), obj.failed) + } + + @Test + fun `provider list failed field handles single failed provider`() { + val src = """{"all":[],"default":{},"connected":[],"failed":["openai"]}""" + val obj = json.decodeFromString(src) + assertEquals(listOf("openai"), obj.failed) + } + + // ------ Model limits with realistic large values ------ + + @Test + fun `model limit context and output deserialize with large values`() { + val src = """{ + "all": [{ + "id": "p", + "name": "Provider", + "source": "api", + "env": [], + "options": {}, + "models": { + "big-model": { + "id": "big-model", + "providerID": "p", + "name": "Big Model", + "api": {"id": "p", "url": "", "npm": ""}, + "capabilities": { + "temperature": true, + "reasoning": false, + "attachment": false, + "toolcall": true, + "input": {"text": true, "audio": false, "image": false, "video": false, "pdf": false}, + "output": {"text": true, "audio": false, "image": false, "video": false, "pdf": false}, + "interleaved": {"field":"reasoning_content"} + }, + "cost": {"input": 0, "output": 0, "cache": {"read": 0, "write": 0}}, + "limit": {"context": 1000000, "output": 32000}, + "status": "active", + "options": {}, + "headers": {}, + "release_date": "2025-01-01" + } + } + }], + "default": {}, + "connected": [], + "failed": [] + }""" + val obj = json.decodeFromString(src) + val model = obj.all[0].models["big-model"]!! + assertEquals(1000000.0, model.limit.context) + assertEquals(32000.0, model.limit.output) + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt index 6e6550256d6..6b1f3c980fe 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloCliDataParserTest.kt @@ -836,6 +836,21 @@ class KiloCliDataParserTest { assertEquals(5000L, result.status.next) } + @Test + fun `parseChatEvent - session status clamps large attempt`() { + val data = globalEvent(""" + "type": "session.status", + "properties": { + "sessionID": "ses_1", + "status": {"type": "retry", "message": "Retrying...", "attempt": 2147483648, "next": 9223372036854775807} + } + """) + + val result = KiloCliDataParser.parseChatEvent("session.status", data) as ChatEventDto.SessionStatusChanged + assertEquals(Int.MAX_VALUE, result.status.attempt) + assertEquals(Long.MAX_VALUE, result.status.next) + } + @Test fun `parseChatEvent - session status offline with requestID`() { val data = globalEvent(""" @@ -970,6 +985,21 @@ class KiloCliDataParserTest { assertEquals(3, result.diff[0].additions) } + @Test + fun `parseChatEvent - session diff clamps large counts`() { + val data = globalEvent(""" + "type": "session.diff", + "properties": { + "sessionID": "ses_1", + "diff": [{"file": "src/A.kt", "additions": 2147483648, "deletions": 9223372036854775807, "patch": "@@ ..."}] + } + """) + + val result = KiloCliDataParser.parseChatEvent("session.diff", data) as ChatEventDto.SessionDiffChanged + assertEquals(Int.MAX_VALUE, result.diff[0].additions) + assertEquals(Int.MAX_VALUE, result.diff[0].deletions) + } + // ================================================================ // parseChatEvent — part with callID // ================================================================ diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt index 093b62e9c6a..35ba6f52e34 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/ProjectModelSerializationTest.kt @@ -23,7 +23,7 @@ class ProjectModelSerializationTest { @Test fun `empty provider list`() { - val src = """{"all":[],"default":{},"connected":[]}""" + val src = """{"all":[],"default":{},"connected":[],"failed":[]}""" val obj = json.decodeFromString(src) assertTrue(obj.all.isEmpty()) assertTrue(obj.default.isEmpty()) @@ -64,7 +64,8 @@ class ProjectModelSerializationTest { } }], "default": {"code": "anthropic/claude-4"}, - "connected": ["anthropic"] + "connected": ["anthropic"], + "failed": [] }""" val obj = json.decodeFromString(src) assertEquals(1, obj.all.size) @@ -115,7 +116,8 @@ class ProjectModelSerializationTest { } }], "default": {}, - "connected": [] + "connected": [], + "failed": [] }""" val obj = json.decodeFromString(src) val model = obj.all[0].models["free-model"]!! @@ -132,12 +134,20 @@ class ProjectModelSerializationTest { "all": [], "default": {}, "connected": [], + "failed": [], "future_field": "value" }""" val obj = json.decodeFromString(src) assertTrue(obj.all.isEmpty()) } + @Test + fun `provider list defaults omitted failed field`() { + val src = """{"all":[],"default":{},"connected":[]}""" + val obj = json.decodeFromString(src) + assertTrue(obj.failed.isEmpty()) + } + // ------ Agent ------ @Test @@ -270,4 +280,18 @@ class ProjectModelSerializationTest { val list = json.decodeFromString>("[]") assertTrue(list.isEmpty()) } + + // ------ ProviderList200Response.failed non-empty ------ + + @Test + fun `provider list failed field handles non-empty list`() { + val src = """{ + "all": [], + "default": {}, + "connected": [], + "failed": ["openai", "gemini"] + }""" + val obj = json.decodeFromString(src) + assertEquals(listOf("openai", "gemini"), obj.failed) + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/SessionModelSerializationTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/SessionModelSerializationTest.kt index 398a1e4c4aa..e837d2693b9 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/SessionModelSerializationTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/SessionModelSerializationTest.kt @@ -26,7 +26,7 @@ class SessionModelSerializationTest { "directory": "/test/project", "title": "My Session", "version": "1.0.0", - "time": {"created": 1000.0, "updated": 2000.0} + "time": {"created": 1000, "updated": 2000} }""" val obj = json.decodeFromString(src) assertEquals("ses_abc", obj.id) @@ -34,8 +34,8 @@ class SessionModelSerializationTest { assertEquals("prj_123", obj.projectID) assertEquals("/test/project", obj.directory) assertEquals("My Session", obj.title) - assertEquals(1000.0, obj.time.created) - assertEquals(2000.0, obj.time.updated) + assertEquals(1000L, obj.time.created) + assertEquals(2000L, obj.time.updated) assertNull(obj.parentID) assertNull(obj.summary) } @@ -49,14 +49,14 @@ class SessionModelSerializationTest { "directory": "/d", "title": "T", "version": "1.0.0", - "time": {"created": 1.0, "updated": 2.0}, + "time": {"created": 1, "updated": 2}, "summary": {"additions": 10, "deletions": 5, "files": 3} }""" val obj = json.decodeFromString(src) assertNotNull(obj.summary) - assertEquals(10.0, obj.summary!!.additions) - assertEquals(5.0, obj.summary!!.deletions) - assertEquals(3.0, obj.summary!!.files) + assertEquals(10, obj.summary!!.additions) + assertEquals(5, obj.summary!!.deletions) + assertEquals(3, obj.summary!!.files) } @Test @@ -68,7 +68,7 @@ class SessionModelSerializationTest { "directory": "/d", "title": "Fork", "version": "1.0.0", - "time": {"created": 1.0, "updated": 2.0, "archived": 3000.0}, + "time": {"created": 1, "updated": 2, "archived": 3000.0}, "parentID": "ses_parent" }""" val obj = json.decodeFromString(src) @@ -132,9 +132,9 @@ class SessionModelSerializationTest { val src = """{"type":"retry","attempt":2,"message":"Rate limited","next":1500,"requestID":"req_1"}""" val obj = json.decodeFromString(src) assertEquals(SessionStatus.Type.RETRY, obj.type) - assertEquals(2.0, obj.attempt) + assertEquals(2L, obj.attempt) assertEquals("Rate limited", obj.message) - assertEquals(1500.0, obj.next) + assertEquals(1500L, obj.next) } @Test @@ -162,4 +162,69 @@ class SessionModelSerializationTest { val map = json.decodeFromString>("{}") assertTrue(map.isEmpty()) } + + // ------ Large integer (Long) handling ------ + + @Test + fun `Session time created and updated deserialize as Long for values larger than Int MAX_VALUE`() { + val big = Int.MAX_VALUE.toLong() + 12345L + val src = """{ + "id": "ses_big", + "slug": "big", + "projectID": "prj", + "directory": "/d", + "title": "Big Times", + "version": "1", + "time": {"created": $big, "updated": ${big + 1L}} + }""" + val obj = json.decodeFromString(src) + assertEquals(big, obj.time.created) + assertEquals(big + 1L, obj.time.updated) + } + + @Test + fun `Session time millisecond epoch timestamps deserialize correctly`() { + // Realistic millisecond timestamp: 2026-01-01T00:00:00Z = 1767139200000 + val ms = 1767139200000L + val src = """{ + "id": "ses_ms", + "slug": "ms", + "projectID": "prj", + "directory": "/d", + "title": "MS Times", + "version": "1", + "time": {"created": $ms, "updated": $ms} + }""" + val obj = json.decodeFromString(src) + assertEquals(ms, obj.time.created) + assertEquals(ms, obj.time.updated) + assertTrue(obj.time.created > Int.MAX_VALUE) + } + + @Test + fun `Session time defaults to 0L when fields are omitted`() { + // The generated model has default = 0L for created and updated + val obj = ai.kilocode.jetbrains.api.model.SessionTime() + assertEquals(0L, obj.created) + assertEquals(0L, obj.updated) + } + + @Test + fun `SessionStatus attempt and next deserialize as Long for large values`() { + val bigAttempt = Int.MAX_VALUE.toLong() + 5L + val bigNext = Int.MAX_VALUE.toLong() + 99999L + val src = """{"type":"retry","attempt":$bigAttempt,"message":"Retrying","next":$bigNext,"requestID":"req_1"}""" + val obj = json.decodeFromString(src) + assertEquals(SessionStatus.Type.RETRY, obj.type) + assertEquals(bigAttempt, obj.attempt) + assertEquals(bigNext, obj.next) + } + + @Test + fun `SessionStatus defaults attempt and next to 0L when omitted`() { + val src = """{"type":"idle","message":"","requestID":""}""" + val obj = json.decodeFromString(src) + assertEquals(0L, obj.attempt) + assertEquals(0L, obj.next) + } } diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt index 5b234053a70..e98c52c0429 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/testing/MockCliServer.kt @@ -41,7 +41,7 @@ class MockCliServer : AutoCloseable { @Volatile var notificationsStatus = 200 // Project-scoped REST responses - @Volatile var providers = """{"all":[],"default":{},"connected":[]}""" + @Volatile var providers = """{"all":[],"default":{},"connected":[],"failed":[]}""" @Volatile var agents = "[]" @Volatile var commands = "[]" @Volatile var skills = "[]" @@ -72,6 +72,11 @@ class MockCliServer : AutoCloseable { @Volatile var summarizeStatus = 200 @Volatile var lastSummarizePath: String? = null @Volatile var lastSummarizeBody: String? = null + @Volatile var sessionRenameStatus = 200 + @Volatile var sessionRenameResponse = """{"id":"ses_test","slug":"test","projectID":"prj_test","directory":"/test","title":"Renamed","version":"1.0.0","time":{"created":1000,"updated":2000}}""" + @Volatile var lastSessionRenamePath: String? = null + @Volatile var lastSessionRenameBody: String? = null + @Volatile var lastSessionRenameMethod: String? = null /** Configurable delay for all endpoint responses (ms). 0 = no delay. */ @Volatile var responseDelay: Long = 0 @@ -239,10 +244,16 @@ class MockCliServer : AutoCloseable { bare == "/session/status" -> respond(output, sessionStatusesStatus, sessionStatuses) bare == "/session" && method == "GET" -> respond(output, sessionsStatus, sessions) bare == "/session" && method == "POST" -> respond(output, sessionCreateStatus, sessionCreate) - bare.matches(Regex("/session/ses_[^/]+")) && method == "GET" -> + bare.matches(Regex("/session/ses_.+")) && !bare.contains("/summarize") && method == "GET" -> respond(output, sessionGetStatus, sessionCreate) - bare.matches(Regex("/session/ses_[^/]+")) && method == "DELETE" -> + bare.matches(Regex("/session/ses_.+")) && !bare.contains("/summarize") && method == "DELETE" -> respond(output, sessionDeleteStatus, "true") + bare.matches(Regex("/session/ses_.+")) && !bare.contains("/summarize") && method == "PATCH" -> { + lastSessionRenamePath = path + lastSessionRenameBody = body + lastSessionRenameMethod = method + respond(output, sessionRenameStatus, sessionRenameResponse) + } bare.matches(Regex("/session/ses_[^/]+/summarize")) && method == "POST" -> { lastSummarizePath = path lastSummarizeBody = body diff --git a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/FixGeneratedApiTask.kt b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/FixGeneratedApiTask.kt index 7a39f7c2161..099d0212123 100644 --- a/packages/kilo-jetbrains/build-tasks/src/main/kotlin/FixGeneratedApiTask.kt +++ b/packages/kilo-jetbrains/build-tasks/src/main/kotlin/FixGeneratedApiTask.kt @@ -218,7 +218,7 @@ abstract class FixGeneratedApiTask : DefaultTask() { } } - // Fix 7: Default values for non-nullable primitives in model data classes. + // Fix 7: Default values for non-nullable primitives and collections in model data classes. // The CLI API may omit fields that the OpenAPI spec marks as required // (e.g. `attachment`, `reasoning` on dynamically added models). // Add Kotlin defaults so kotlinx.serialization doesn't throw @@ -230,6 +230,9 @@ abstract class FixGeneratedApiTask : DefaultTask() { Regex("""(val \w+:\s*kotlin\.Boolean)(,|\n)""") to { m: MatchResult -> "${m.groupValues[1]} = false${m.groupValues[2]}" }, + Regex("""(val \w+:\s*kotlin\.Long)(,|\n)""") to { m: MatchResult -> + "${m.groupValues[1]} = 0L${m.groupValues[2]}" + }, Regex("""(val \w+:\s*kotlin\.Int)(,|\n)""") to { m: MatchResult -> "${m.groupValues[1]} = 0${m.groupValues[2]}" }, @@ -239,6 +242,12 @@ abstract class FixGeneratedApiTask : DefaultTask() { Regex("""(val \w+:\s*kotlin\.String)(,|\n)""") to { m: MatchResult -> "${m.groupValues[1]} = \"\"${m.groupValues[2]}" }, + Regex("""(val \w+:\s*kotlin\.collections\.List<[^>]+>)(,|\n)""") to { m: MatchResult -> + "${m.groupValues[1]} = emptyList()${m.groupValues[2]}" + }, + Regex("""(val \w+:\s*kotlin\.collections\.Map<[^>]+>)(,|\n)""") to { m: MatchResult -> + "${m.groupValues[1]} = emptyMap()${m.groupValues[2]}" + }, ) for ((pattern, transform) in primitiveDefaults) { if (pattern.containsMatchIn(text)) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/DeleteSessionAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/DeleteSessionAction.kt new file mode 100644 index 00000000000..3db258f8cce --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/DeleteSessionAction.kt @@ -0,0 +1,47 @@ +package ai.kilocode.client.actions + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.history.HistoryDataKeys +import ai.kilocode.client.session.SessionManager +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages + +class DeleteSessionAction : AnAction() { + /** Overridable in tests to avoid showing a real modal dialog. */ + internal var confirm: (project: Project?, msg: String) -> Boolean = { project, msg -> + Messages.showYesNoDialog( + project, + msg, + KiloBundle.message("history.delete.confirm.title"), + Messages.getWarningIcon(), + ) == Messages.YES + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + + override fun update(e: AnActionEvent) { + val selection = e.getData(HistoryDataKeys.SELECTION) + val manager = e.getData(SessionManager.KEY) + e.presentation.isEnabledAndVisible = manager != null && + selection != null && + selection.selectedLocal.isNotEmpty() + } + + override fun actionPerformed(e: AnActionEvent) { + val selection = e.getData(HistoryDataKeys.SELECTION) ?: return + val controller = e.getData(HistoryDataKeys.CONTROLLER) ?: return + val items = selection.selectedLocal.filter { !controller.deleting(it) } + if (items.isEmpty()) return + + val msg = if (items.size == 1) + KiloBundle.message("history.delete.confirm.message", ai.kilocode.client.session.history.title(items[0])) + else + KiloBundle.message("history.delete.confirm.message.multiple", items.size) + + if (!confirm(e.project, msg)) return + items.forEach { controller.delete(it) } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenSessionAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenSessionAction.kt new file mode 100644 index 00000000000..c6ef6f3dc10 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/OpenSessionAction.kt @@ -0,0 +1,26 @@ +package ai.kilocode.client.actions + +import ai.kilocode.client.session.history.HistoryDataKeys +import ai.kilocode.client.session.SessionManager +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent + +class OpenSessionAction : AnAction() { + override fun getActionUpdateThread() = ActionUpdateThread.EDT + + override fun update(e: AnActionEvent) { + val selection = e.getData(HistoryDataKeys.SELECTION) + val manager = e.getData(SessionManager.KEY) + e.presentation.isEnabledAndVisible = manager != null && + selection != null && + (selection.selectedLocal.size == 1 || selection.cloudItems.size == 1) + } + + override fun actionPerformed(e: AnActionEvent) { + val selection = e.getData(HistoryDataKeys.SELECTION) ?: return + val controller = e.getData(HistoryDataKeys.CONTROLLER) ?: return + selection.selectedLocal.singleOrNull()?.let { controller.open(it); return } + selection.cloudItems.singleOrNull()?.let { controller.open(it) } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt new file mode 100644 index 00000000000..aa40c15fb41 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt @@ -0,0 +1,47 @@ +package ai.kilocode.client.actions + +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.history.HistoryDataKeys +import ai.kilocode.client.session.history.title +import ai.kilocode.client.session.SessionManager +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages + +class RenameSessionAction : AnAction() { + /** Overridable in tests to avoid showing a real modal dialog. */ + internal var input: (project: Project?, current: String) -> String? = { project, current -> + Messages.showInputDialog( + project, + KiloBundle.message("history.rename.prompt"), + KiloBundle.message("history.rename.title"), + null, + current, + null, + ) + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + + override fun update(e: AnActionEvent) { + val selection = e.getData(HistoryDataKeys.SELECTION) + val manager = e.getData(SessionManager.KEY) + e.presentation.isEnabledAndVisible = manager != null && + selection != null && + selection.selectedLocal.size == 1 + } + + override fun actionPerformed(e: AnActionEvent) { + val selection = e.getData(HistoryDataKeys.SELECTION) ?: return + val controller = e.getData(HistoryDataKeys.CONTROLLER) ?: return + val item = selection.selectedLocal.singleOrNull() ?: return + + val current = title(item) + val newTitle = input(e.project, current)?.trim() ?: return + + if (newTitle.isBlank() || newTitle == current) return + controller.rename(item, newTitle) + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt index a011ad885e6..4f2fdcf05f9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/app/KiloSessionService.kt @@ -124,6 +124,12 @@ class KiloSessionService internal constructor( list(dir) } + suspend fun renameSession(id: String, dir: String, newTitle: String): ai.kilocode.rpc.dto.SessionDto { + val session = call { rename(id, dir, newTitle) } + _sessions.value = _sessions.value.map { if (it.id == id) session else it } + return session + } + suspend fun cloudSessions(dir: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto = call { cloudSessions(dir, cursor, limit, gitUrl) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index 2fa5d531c56..118c2e87d24 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -113,7 +113,7 @@ class SessionSidePanelManager( deleted = this::removeSession, ) Disposer.register(this) { cs.cancel() } - return HistoryPanel(this, controller, nav = this::back).component + return HistoryPanel(this, controller, nav = this::back, manager = this).component } private fun back() { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/GitRemoteUrl.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/GitRemoteUrl.kt new file mode 100644 index 00000000000..7093a5008b3 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/GitRemoteUrl.kt @@ -0,0 +1,34 @@ +package ai.kilocode.client.session.history + +import java.io.File +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +/** + * Resolves the origin remote URL for a given directory by running + * `git remote get-url origin` as a subprocess. + * + * Returns null when the directory is not a git repo, has no origin remote, + * or the command fails for any reason. + * + * Overridable in tests via [resolve] parameter. + */ +internal fun resolveGitRemoteUrl(dir: String): String? = runCatching { + val proc = ProcessBuilder("git", "remote", "get-url", "origin") + .directory(File(dir)) + .redirectErrorStream(true) + .start() + + val text = CompletableFuture.supplyAsync { + proc.inputStream.bufferedReader().use { it.readText() } + } + val done = proc.waitFor(5, TimeUnit.SECONDS) + if (!done) { + proc.destroyForcibly() + return@runCatching null + } + + val out = text.get(1, TimeUnit.SECONDS).trim() + val code = proc.exitValue() + if (code == 0 && out.isNotEmpty()) out else null +}.getOrNull() diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index 81993348e78..d4b6165e4d5 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -8,7 +8,11 @@ import ai.kilocode.rpc.dto.CloudSessionDto import ai.kilocode.rpc.dto.SessionDto import com.intellij.openapi.application.ApplicationManager import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext class HistoryController( private val sessions: KiloSessionService, @@ -16,21 +20,43 @@ class HistoryController( private val cs: CoroutineScope, open: (SessionRef) -> Unit = {}, private val deleted: (String) -> Unit = {}, + private val gitUrlProvider: () -> String? = { resolveGitRemoteUrl(workspace.directory) }, ) { companion object { - const val CLOUD_LIMIT = 150 + const val CLOUD_LIMIT = 50 } val local = HistoryModel() val cloud = CloudHistoryModel() + /** Resolved once on first cloud load; null means no remote found. Written from IO, read on EDT. */ + @Volatile + var gitUrl: String? = null + private set + + @Volatile + private var resolved = false + private val lock = Mutex() + + /** Whether to filter cloud history by the current repository. */ + var repoOnly: Boolean = false + private set + + /** Notified on EDT when [repoOnly] changes (e.g. to update checkbox state). */ + var onRepoOnlyChanged: ((Boolean) -> Unit)? = null + + private fun updateRepoOnly(value: Boolean) { + if (repoOnly == value) return + repoOnly = value + edt { onRepoOnlyChanged?.invoke(value) } + } + private val deleting = mutableSetOf() private val opener = open - private var git: String? = null - fun reload(gitUrl: String? = null) { + fun reload() { reloadLocal() - reloadCloud(gitUrl) + reloadCloud() } fun reloadLocal() { @@ -46,8 +72,7 @@ class HistoryController( } } - fun reloadCloud(gitUrl: String? = null) { - git = gitUrl + fun reloadCloud() { loadCloud(reset = true) } @@ -56,14 +81,20 @@ class HistoryController( loadCloud(reset = false) } + fun applyRepoOnly(value: Boolean) { + updateRepoOnly(value) + edt { reloadCloud() } + } + fun delete(item: LocalHistoryItem) { edt { if (item.id in deleting) return@edt deleting.add(item.id) local.refresh() + val dir = item.directory ?: workspace.directory cs.launch { try { - sessions.deleteSession(item.id, item.directory ?: workspace.directory) + sessions.deleteSession(item.id, dir) edt { deleting.remove(item.id) local.remove(item.id) @@ -79,6 +110,18 @@ class HistoryController( } } + fun rename(item: LocalHistoryItem, title: String) { + val dir = item.directory ?: workspace.directory + cs.launch { + try { + val updated = sessions.renameSession(item.id, dir, title) + edt { local.update(LocalHistoryItem(updated)) } + } catch (e: Exception) { + edt { local.fail(e.message ?: KiloBundle.message("history.error.local.rename")) } + } + } + } + fun deleting(item: LocalHistoryItem): Boolean = item.id in deleting fun open(item: LocalHistoryItem) { @@ -91,11 +134,12 @@ class HistoryController( private fun loadCloud(reset: Boolean) { val cursor = cloud.cursor.takeUnless { reset } - val gitUrl = git edt { cloud.start(reset) } cs.launch { + val url = resolveUrlIfNeeded() + val filter = if (repoOnly) url else null try { - val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, gitUrl) + val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, filter) val items = result.sessions.map(::cloudItem) edt { if (reset) cloud.replace(items, result.nextCursor) @@ -106,6 +150,28 @@ class HistoryController( } } } + + /** + * Resolves [gitUrl] on first cloud load. Subsequent calls return the cached value. + * Also enables [repoOnly] by default when a URL is found the first time. + * + * Must be called from a coroutine. Writes [gitUrl] directly (volatile) and then + * propagates state updates to EDT via [edt]. + */ + private suspend fun resolveUrlIfNeeded(): String? { + if (resolved) return gitUrl + return lock.withLock { + if (resolved) return@withLock gitUrl + val url = withContext(Dispatchers.IO) { + gitUrlProvider() + } + // Write gitUrl directly (volatile) so it is visible before EDT callbacks fire. + gitUrl = url + resolved = true + if (url != null) updateRepoOnly(true) + url + } + } } private fun edt(block: () -> Unit) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryDataKeys.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryDataKeys.kt new file mode 100644 index 00000000000..3c560038abc --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryDataKeys.kt @@ -0,0 +1,16 @@ +package ai.kilocode.client.session.history + +import com.intellij.openapi.actionSystem.DataKey + +data class HistorySelection( + val source: HistorySource, + val localItems: List, + val cloudItems: List, +) { + val selectedLocal: List get() = if (source == HistorySource.LOCAL) localItems else emptyList() +} + +object HistoryDataKeys { + val SELECTION: DataKey = DataKey.create("ai.kilocode.client.session.history.HistorySelection") + val CONTROLLER: DataKey = DataKey.create("ai.kilocode.client.session.history.HistoryController") +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt index a7fb4e2c961..4aae232943a 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListRenderer.kt @@ -3,7 +3,6 @@ package ai.kilocode.client.session.history import ai.kilocode.client.session.ui.PickerRow import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons -import com.intellij.ui.ExperimentalUI import com.intellij.ui.GroupHeaderSeparator import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes @@ -20,7 +19,7 @@ import javax.swing.JPanel import javax.swing.ListCellRenderer import javax.swing.SwingConstants -private const val DELETE_CLICK_AREA_WIDTH = 32 +private const val DELETE_AREA_WIDTH = 32 internal open class HistoryRenderer( private val model: HistoryModel, @@ -31,14 +30,14 @@ internal open class HistoryRenderer( private val empty: Icon = EmptyIcon.create(icon) fun isDeleteClick(list: JList<*>, bounds: Rectangle, point: Point): Boolean { - val width = JBUI.scale(DELETE_CLICK_AREA_WIDTH) - val inset = deleteInset(list) - if (list.componentOrientation.isLeftToRight) { - val right = bounds.x + bounds.width - inset - return point.x in (right - width)..right + val width = JBUI.scale(DELETE_AREA_WIDTH) + return if (list.componentOrientation.isLeftToRight) { + val right = bounds.x + bounds.width + point.x in (right - width)..right + } else { + val left = bounds.x + point.x in left..(left + width) } - val left = bounds.x + inset - return point.x in left..(left + width) } fun section(items: List, index: Int): String? { @@ -48,13 +47,6 @@ internal open class HistoryRenderer( if (current == previous) return null return HistoryTime.title(current) } - - private fun deleteInset(list: JList<*>): Int { - if (!ExperimentalUI.isNewUI()) return 0 - val inner = JBUI.CurrentTheme.Popup.Selection.innerInsets() - val edge = JBUI.CurrentTheme.Popup.Selection.LEFT_RIGHT_INSET.get() - return edge + if (list.componentOrientation.isLeftToRight) inner.right else inner.left - } } private val sep = GroupHeaderSeparator(JBUI.CurrentTheme.Popup.separatorLabelInsets()) @@ -75,7 +67,7 @@ internal open class HistoryRenderer( } private val row = JPanel(BorderLayout()).apply { add(main, BorderLayout.CENTER) - add(del, BorderLayout.EAST) + if (deletable) add(del, BorderLayout.EAST) } private val wrap = PickerRow() @@ -114,13 +106,11 @@ internal open class HistoryRenderer( ) time.text = value?.let(HistoryTime::relative).orEmpty() time.foreground = weak - del.icon = if (deletable && selected) icon else empty + if (deletable) del.icon = if (selected) icon else empty top.invalidate() return this } - - fun deleteVisible(): Boolean = del.icon === icon } internal class LocalHistoryRenderer(model: HistoryModel) : HistoryRenderer(model, deletable = true) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListUi.kt index c5c9b2a6be4..df01ec2b823 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryListUi.kt @@ -6,10 +6,9 @@ import java.awt.event.MouseEvent internal fun title(item: HistoryItem): String = item.title.takeIf { it.isNotBlank() } ?: KiloBundle.message("history.untitled") -internal fun clicked(list: JBList, e: MouseEvent): T? { +internal fun itemAt(list: JBList, e: MouseEvent): T? { val row = list.locationToIndex(e.point) val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return null if (!box.contains(e.point)) return null - list.selectedIndex = row return list.model.getElementAt(row) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt index a9d0af9e774..67b6c105e93 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryModel.kt @@ -48,6 +48,12 @@ open class HistoryModel : AbstractListModel() { filter() } + fun update(item: T) { + if (all.none { it.id == item.id }) return + all = HistoryTime.sorted(all.map { if (it.id == item.id) item else it }) + filter() + } + fun fail(message: String) { loading = false loaded = true @@ -60,8 +66,6 @@ open class HistoryModel : AbstractListModel() { filter() } - fun hasFilter(): Boolean = query.isNotEmpty() - fun refresh() { val end = size.coerceAtLeast(1) - 1 fireContentsChanged(this, 0, end) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt index 4c4460acd28..b3ad33485d8 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryPanel.kt @@ -1,20 +1,28 @@ package ai.kilocode.client.session.history import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionManager import ai.kilocode.client.session.ui.LoadingPanel import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.ui.UiStyle import com.intellij.icons.AllIcons import com.intellij.ide.ui.LafManagerListener -import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.Disposable -import com.intellij.openapi.ui.Messages +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.actionSystem.impl.ActionButton import com.intellij.openapi.util.Disposer import com.intellij.ui.DocumentAdapter -import com.intellij.ui.ListUtil +import com.intellij.ui.PopupHandler import com.intellij.ui.SearchTextField import com.intellij.ui.ScrollingUtil +import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBList import com.intellij.ui.components.JBScrollPane import com.intellij.ui.tabs.JBTabs @@ -49,14 +57,19 @@ import javax.swing.event.ListDataListener class HistoryPanel( parent: Disposable, private val controller: HistoryController, - private val gitUrl: () -> String? = { null }, private val nav: () -> Unit = {}, -) : BorderLayoutPanel(), Disposable { + private val manager: SessionManager? = null, +) : BorderLayoutPanel(), Disposable, DataProvider { private val localSearch = search(controller.local) private val cloudSearch = search(controller.cloud) private val localList = localList() private val cloudList = cloudList() private val more = LoadMoreButton() + private val repoOnly = JBCheckBox(KiloBundle.message("history.cloud.repo.only"), true).apply { + isVisible = false + border = JBUI.Borders.emptyLeft(UiStyle.Gap.lg()) + addActionListener { controller.applyRepoOnly(isSelected) } + } private val localPanel = panel(localSearch, localList) private val cloudPanel = panel(cloudSearch, cloudList, more) private val cards = CardLayout() @@ -89,6 +102,9 @@ class HistoryPanel( bind(localList, controller.local) bind(cloudList, controller.cloud) bindTheme() + controller.onRepoOnlyChanged = { value -> + repoOnly.isSelected = value + } addHierarchyListener { e -> if (e.changeFlags and HierarchyEvent.SHOWING_CHANGED.toLong() == 0L) return@addHierarchyListener if (isShowing && stale) { @@ -111,7 +127,7 @@ class HistoryPanel( fun refresh() { stale = false updateTheme() - controller.reload(gitUrl()) + controller.reload() } private fun bindTheme() { @@ -162,19 +178,30 @@ class HistoryPanel( ) } - private fun back() = BorderLayoutPanel().apply { - add(JButton(KiloBundle.message("history.back"), AllIcons.Actions.Back).apply { - putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) - isFocusable = false + private fun back(): BorderLayoutPanel { + val label = KiloBundle.message("history.back") + val action = object : AnAction(label, null, AllIcons.Actions.Back) { + override fun actionPerformed(e: AnActionEvent) = nav() + } + val presentation = Presentation(label).apply { icon = AllIcons.Actions.Back } + val btn = ActionButton(action, presentation, ActionPlaces.TOOLBAR, JBUI.size(16)).apply { cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) - addActionListener { nav() } - }, BorderLayout.WEST) - border = JBUI.Borders.emptyRight(UiStyle.Gap.lg()) + } + return BorderLayoutPanel().apply { + add(btn, BorderLayout.WEST) + border = JBUI.Borders.emptyRight(UiStyle.Gap.lg()) + } } private fun panel(search: SearchTextField, list: JList, footer: JComponent? = null): JComponent { return BorderLayoutPanel().apply { - add(search, BorderLayout.NORTH) + val north = BorderLayoutPanel().apply { + add(search, BorderLayout.CENTER) + if (list === cloudList) { + add(repoOnly, BorderLayout.SOUTH) + } + } + add(north, BorderLayout.NORTH) add(JBScrollPane(list).apply { border = JBUI.Borders.empty() viewportBorder = JBUI.Borders.empty() @@ -188,21 +215,22 @@ class HistoryPanel( } private fun localList() = JBList(controller.local).apply { - selectionMode = ListSelectionModel.SINGLE_SELECTION - isFocusable = false + selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION + isFocusable = true cellRenderer = LocalHistoryRenderer(controller.local) cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) emptyText.text = KiloBundle.message("history.empty") addMouseListener(object : MouseAdapter() { - override fun mouseReleased(e: MouseEvent) { - if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) return - val item = clicked(this@apply, e) ?: return - if (deleteClick(this@apply, e)) { + override fun mouseClicked(e: MouseEvent) { + val row = locationToIndex(e.point) + val box = row.takeIf { it >= 0 }?.let { getCellBounds(it, it) } ?: return + if (!box.contains(e.point)) return + if (e.clickCount == 1 && HistoryRenderer.isDeleteClick(this@apply, box, e.point)) { + val item = model.getElementAt(row) confirm(item) - e.consume() - return + } else if (e.clickCount == 2) { + selectedValue?.let(::activate) } - activate(item) } }) registerKeyboardAction( @@ -210,20 +238,19 @@ class HistoryPanel( KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_FOCUSED, ) - ListUtil.installAutoSelectOnMouseMove(this) + installContextMenu(this) ScrollingUtil.installActions(this) } private fun cloudList() = JBList(controller.cloud).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION - isFocusable = false + isFocusable = true cellRenderer = CloudHistoryRenderer(controller.cloud) cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) emptyText.text = KiloBundle.message("history.empty") addMouseListener(object : MouseAdapter() { - override fun mouseReleased(e: MouseEvent) { - if (!UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED, true)) return - clicked(this@apply, e)?.let(::activate) + override fun mouseClicked(e: MouseEvent) { + if (e.clickCount == 2) selectedValue?.let(::activate) } }) registerKeyboardAction( @@ -231,7 +258,7 @@ class HistoryPanel( KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), JComponent.WHEN_FOCUSED, ) - ListUtil.installAutoSelectOnMouseMove(this) + installContextMenu(this) ScrollingUtil.installActions(this) } @@ -253,6 +280,7 @@ class HistoryPanel( syncList(cloudList, controller.cloud) more.isEnabled = controller.cloud.cursor != null && !controller.cloud.loading more.isVisible = controller.cloud.cursor != null || controller.cloud.loading + repoOnly.isVisible = controller.gitUrl != null cards.show(body, if (loading()) CARD_LOAD else CARD_TABS) revalidate() repaint() @@ -272,13 +300,6 @@ class HistoryPanel( } } - private fun deleteClick(list: JBList, e: MouseEvent): Boolean { - val row = list.locationToIndex(e.point) - val box = row.takeIf { it >= 0 }?.let { list.getCellBounds(it, it) } ?: return false - if (!box.contains(e.point)) return false - return HistoryRenderer.isDeleteClick(list, box, e.point) - } - private fun activate(item: HistoryItem) { when (item) { is LocalHistoryItem -> controller.open(item) @@ -286,18 +307,54 @@ class HistoryPanel( } } + override fun getData(dataId: String): Any? { + if (SessionManager.KEY.`is`(dataId)) return manager + if (HistoryDataKeys.CONTROLLER.`is`(dataId)) return controller + if (HistoryDataKeys.SELECTION.`is`(dataId)) { + val source = selectedSource() + val local = if (source == HistorySource.LOCAL) localList.selectedValuesList.filterIsInstance() else emptyList() + val cloud = if (source == HistorySource.CLOUD) cloudList.selectedValuesList.filterIsInstance() else emptyList() + return HistorySelection(source, local, cloud) + } + return null + } + + private fun installContextMenu(list: JBList) { + val group = ActionManager.getInstance().getAction("Kilo.History.ContextMenu") + if (group is ActionGroup) { + PopupHandler.installPopupMenu(list, group, ActionPlaces.POPUP) + } + } + private fun confirm(item: LocalHistoryItem) { if (controller.deleting(item)) return - val result = Messages.showYesNoDialog( + val result = com.intellij.openapi.ui.Messages.showYesNoDialog( this, KiloBundle.message("history.delete.confirm.message", title(item)), KiloBundle.message("history.delete.confirm.title"), - Messages.getWarningIcon(), + com.intellij.openapi.ui.Messages.getWarningIcon(), ) - if (result != Messages.YES) return + if (result != com.intellij.openapi.ui.Messages.YES) return controller.delete(item) } + internal fun confirmDelete(items: List) { + val active = items.filter { !controller.deleting(it) } + if (active.isEmpty()) return + val msg = if (active.size == 1) + KiloBundle.message("history.delete.confirm.message", title(active[0])) + else + KiloBundle.message("history.delete.confirm.message.multiple", active.size) + val result = com.intellij.openapi.ui.Messages.showYesNoDialog( + this, + msg, + KiloBundle.message("history.delete.confirm.title"), + com.intellij.openapi.ui.Messages.getWarningIcon(), + ) + if (result != com.intellij.openapi.ui.Messages.YES) return + active.forEach { controller.delete(it) } + } + internal fun itemCount() = activeModel().size internal fun selectedSource() = if (tabs.selectedInfo === cloudInfo) HistorySource.CLOUD else HistorySource.LOCAL @@ -306,29 +363,38 @@ class HistoryPanel( activeList().selectedIndex = index } + internal fun selectIndices(vararg indices: Int) { + activeList().selectedIndices = indices + } + internal fun selectedIndex() = activeList().selectedIndex internal fun listFocusable() = activeList().isFocusable + internal fun listSelectionMode() = activeList().selectionMode + + internal fun loadMoreFocusable() = more.isFocusable + internal fun listCursor() = activeList().cursor.type internal fun backText(): String? { val view = activeInfo().foreSideComponent ?: return null - return UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.text + return UIUtil.uiTraverser(view).filter(ActionButton::class.java).firstOrNull()?.presentation?.text } internal fun backCursor(): Int? { val view = activeInfo().foreSideComponent ?: return null - return UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.cursor?.type + return UIUtil.uiTraverser(view).filter(ActionButton::class.java).firstOrNull()?.cursor?.type } internal fun clickBack() { val view = activeInfo().foreSideComponent ?: return - UIUtil.uiTraverser(view).filter(JButton::class.java).firstOrNull()?.doClick() + UIUtil.uiTraverser(view).filter(ActionButton::class.java).firstOrNull()?.click() } internal fun clickDelete() { - localList.selectedValue?.let(controller::delete) + val items = localList.selectedValuesList.filterIsInstance() + items.forEach { controller.delete(it) } } internal fun clickCloud() { @@ -354,16 +420,12 @@ class HistoryPanel( return items.indices.mapNotNull { HistoryRenderer.section(items, it) } } - internal fun deleteVisible(index: Int, selected: Boolean = true): Boolean { - val item = controller.local.getElementAt(index) - val view = localList.cellRenderer.getListCellRendererComponent(localList, item, index, selected, false) - return view is HistoryRenderer<*> && view.deleteVisible() - } + internal fun repoOnlyVisible() = repoOnly.isVisible - internal fun cloudDeleteVisible(index: Int, selected: Boolean = true): Boolean { - val item = controller.cloud.getElementAt(index) - val view = cloudList.cellRenderer.getListCellRendererComponent(cloudList, item, index, selected, false) - return view is HistoryRenderer<*> && view.deleteVisible() + internal fun repoOnlySelected() = repoOnly.isSelected + + internal fun clickRepoOnly() { + repoOnly.doClick() } private fun activeList(): JBList = if (tabs.selectedInfo === cloudInfo) cloudList else localList @@ -385,15 +447,14 @@ class HistoryPanel( } override fun dispose() { - // no-op + controller.onRepoOnlyChanged = null } private class LoadMoreButton : JButton(KiloBundle.message("history.cloud.load.more")) { private var over = false init { - isFocusable = false - setRequestFocusEnabled(false) + isFocusable = true isContentAreaFilled = false isBorderPainted = false isOpaque = false diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt index 1b3f9516ecf..a498e09365d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryTime.kt @@ -35,8 +35,8 @@ internal object HistoryTime { val today = Instant.ofEpochMilli(now).atZone(zone).toLocalDate() if (date == today) return HistorySection.TODAY if (date == today.minusDays(1)) return HistorySection.YESTERDAY - if (date.isAfter(today.minusDays(7)) && date.isBefore(today)) return HistorySection.WEEK - if (date.isAfter(today.minusDays(30)) && date.isBefore(today)) return HistorySection.MONTH + if (!date.isBefore(today.minusDays(7)) && date.isBefore(today)) return HistorySection.WEEK + if (!date.isBefore(today.minusDays(30)) && date.isBefore(today)) return HistorySection.MONTH return HistorySection.OLDER } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt index d2811072a28..3d079596616 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/EmptySessionPanel.kt @@ -4,7 +4,7 @@ import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionRef import ai.kilocode.client.session.history.HistoryTime import ai.kilocode.client.session.history.LocalHistoryItem -import ai.kilocode.client.session.history.clicked +import ai.kilocode.client.session.history.itemAt import ai.kilocode.client.session.history.title import ai.kilocode.client.session.ui.style.SessionEditorStyle import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget @@ -71,7 +71,7 @@ class EmptySessionPanel( emptyText.clear() addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { - val item = clicked(this@apply, e) ?: return + val item = itemAt(this@apply, e) ?: return controller.openSession(SessionRef.Local(item.session)) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt index e559d1b5d23..615f52df59e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/ui/header/SessionHeaderPanel.kt @@ -108,7 +108,7 @@ class SessionHeaderPanel( add(output) add(Box.createHorizontalStrut(UiStyle.Gap.sm())) add(cacheRead) - add(Box.createHorizontalStrut(UiStyle.Gap.small())) + add(Box.createHorizontalStrut(UiStyle.Gap.sm())) add(cacheWrite) } private val todoRow = JPanel(FlowLayout(FlowLayout.LEFT, UiStyle.Gap.md(), 0)).apply { diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml index 5eabe4727d6..dedf3ee3045 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml +++ b/packages/kilo-jetbrains/frontend/src/main/resources/kilo.jetbrains.frontend.xml @@ -58,5 +58,25 @@ + + + + + + + + + + + + + + + diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties index 5b3f9b7aaaf..4277f72e785 100644 --- a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle.properties @@ -14,21 +14,21 @@ session.tab.new=New Session session.tab.untitled=Untitled Session session.permission.title=Permission request -session.permission.meta=Tool: {0} \u2022 Patterns: {1} +session.permission.meta=Tool: {0} • Patterns: {1} session.permission.allow=Allow session.permission.deny=Deny session.question.dismiss=Dismiss -session.status.considering=Considering next steps\u2026 -session.status.thinking=Thinking\u2026 -session.status.writing=Writing response\u2026 -session.status.delegating=Delegating work\u2026 -session.status.planning=Planning\u2026 -session.status.gathering=Gathering context\u2026 -session.status.searching.codebase=Searching codebase\u2026 -session.status.searching.web=Searching web\u2026 -session.status.editing=Making edits\u2026 -session.status.commands=Running commands\u2026 +session.status.considering=Considering next steps… +session.status.thinking=Thinking… +session.status.writing=Writing response… +session.status.delegating=Delegating work… +session.status.planning=Planning… +session.status.gathering=Gathering context… +session.status.searching.codebase=Searching codebase… +session.status.searching.web=Searching web… +session.status.editing=Making edits… +session.status.commands=Running commands… session.part.reasoning=Reasoning session.part.compaction=context compacted @@ -92,6 +92,9 @@ history.untitled=Untitled history.delete.text=Delete history.delete.confirm.title=Delete session? history.delete.confirm.message=Delete "{0}" from local history? +history.delete.confirm.message.multiple=Delete {0} sessions from local history? +history.rename.title=Rename Session +history.rename.prompt=New session name: history.cloud.load.more=Load more history.cloud.repo.only=Only this repository history.group.today=Today @@ -108,6 +111,7 @@ history.time.years={0}y ago history.error.local=Failed to load local history history.error.cloud=Failed to load cloud history history.error.local.delete=Failed to delete session +history.error.local.rename=Failed to rename session history.error.cloud.delete=Cloud sessions cannot be deleted yet action.Kilo.Settings.text=Settings @@ -126,3 +130,10 @@ action.Kilo.Restart.text=Restart Kilo action.Kilo.Restart.description=Kill and restart the CLI process action.Kilo.Reinstall.text=Reinstall Kilo action.Kilo.Reinstall.description=Re-extract the CLI binary and restart +action.Kilo.Session.Open.text=Open +action.Kilo.Session.Open.description=Open the selected session +action.Kilo.Session.Rename.text=Rename +action.Kilo.Session.Rename.description=Rename the selected session +action.Kilo.Session.Delete.text=Delete +action.Kilo.Session.Delete.description=Delete the selected session(s) +action.Kilo.History.ContextMenu.text=History Actions diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties new file mode 100644 index 00000000000..9cb9ee73187 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ar.properties @@ -0,0 +1,139 @@ +session.connection.connecting=جاري التحميل… +session.connection.error.app=فشل الاتصال +session.connection.error.workspace=فشل تحميل مساحة العمل +session.connection.error.unknown=خطأ غير معروف +session.connection.retry=إعادة المحاولة +session.connection.warning.config=تحذيرات التكوين + +session.empty.welcome=Kilo Code هو مساعد برمجة بالذكاء الاصطناعي. اطلب منه بناء ميزات أو إصلاح أخطاء أو شرح قاعدة الكود. +session.empty.loading=جاري التحميل… +session.empty.recent=الحديثة +session.showHistory=عرض السجل +session.scroll.bottom=التمرير إلى الأسفل +session.tab.new=جلسة جديدة +session.tab.untitled=جلسة بدون عنوان + +session.permission.title=طلب إذن +session.permission.meta=الأداة: {0} • الأنماط: {1} +session.permission.allow=سماح +session.permission.deny=رفض +session.question.dismiss=رفض + +session.status.considering=جار التفكير في الخطوات التالية… +session.status.thinking=جار التفكير… +session.status.writing=جاري كتابة الرد… +session.status.delegating=جار تفويض العمل… +session.status.planning=جار التخطيط… +session.status.gathering=جار جمع السياق… +session.status.searching.codebase=جار البحث في قاعدة الكود… +session.status.searching.web=جار البحث على الويب… +session.status.editing=جاري التحرير… +session.status.commands=جاري تنفيذ الأوامر… + +session.part.reasoning=الاستدلال +session.part.compaction=السياق مضغوط +session.part.tool.copy=نسخ +session.part.tool.error=خطأ +session.part.tool.pending=معلق +session.part.tool.read=قراءة +session.part.tool.running=قيد التشغيل +session.part.tool.shell=Shell +session.part.tool.truncated=المخرجات مختصرة في المعاينة المسبقة. المخرجات الكاملة لا تزال في بيانات الجلسة. + +session.error.prompt=فشل الطلب +session.error.compact=فشل ضغط الجلسة +session.error.unknown=خطأ غير معروف + +session.header.tokens=الرموز +session.header.tokens.description=الرموز المستخدمة في آخر رد للمساعد: الإدخال، الإخراج، كتابات المخزن المؤقت وقراءاته. +session.header.input=إدخال {0} +session.header.output=إخراج {0} +session.header.cache.write=كتابة مخزن مؤقت {0} +session.header.cache.read=قراءة مخزن مؤقت {0} +session.header.compact=ضغط الجلسة +session.header.compact.description=تلخيص الجلسة لتقليل حجم السياق +session.header.expand=عرض مقاييس الجلسة +session.header.collapse=إخفاء مقاييس الجلسة +session.header.todos.progress={0}/{1} مهام مكتملة +session.header.todos.done=جميع {0} مهام مكتملة +session.header.context.tooltip.percent={0} رمز ({1}٪ من السياق) +session.header.context.tooltip.tokens={0} رمز +session.header.context.used={0} / {1} رمز مستخدم +session.header.context.reserved={0} محجوز للإخراج +session.header.context.available={0} متاح + +prompt.placeholder=اكتب رسالة… +prompt.placeholder.with.shortcuts=اكتب رسالة… ({0} للإرسال، {1} لسطر جديد) +prompt.placeholder.with.send=اكتب رسالة… ({0} للإرسال) +prompt.placeholder.with.newline=اكتب رسالة… ({0} لسطر جديد) +prompt.button.send=إرسال +prompt.button.stop=إيقاف +prompt.button.send.tooltip.stop=للإيقاف، اضغط {0} +mode.picker.tooltip=اختيار الوضع +mode.picker.deprecated=مهجور +model.picker.tooltip=اختيار النموذج +model.picker.recommended=موصى به +model.picker.favorites=المفضلة +model.picker.search=بحث عن نماذج +model.picker.no.matches=لا توجد نماذج مطابقة +model.picker.favorite.add=إضافة إلى المفضلة +model.picker.favorite.remove=إزالة من المفضلة +model.picker.free=مجاني +model.picker.reset=إعادة ضبط النموذج للافتراضي +reasoning.picker.tooltip=اختيار مستوى الاستدلال + +history.tab.local=محلي +history.tab.cloud=سحابي +history.back=رجوع +history.search.placeholder=بحث في الجلسات +history.empty=لا توجد جلسات +history.loading=جاري التحميل… +history.untitled=بدون عنوان +history.delete.text=حذف +history.delete.confirm.title=حذف الجلسة؟ +history.delete.confirm.message=حذف "{0}" من السجل المحلي؟ +history.delete.confirm.message.multiple=حذف {0} جلسات من السجل المحلي؟ +history.rename.title=إعادة تسمية الجلسة +history.rename.prompt=اسم الجلسة الجديد: +history.cloud.load.more=تحميل المزيد +history.cloud.repo.only=هذا المستودع فقط +history.group.today=اليوم +history.group.yesterday=أمس +history.group.week=هذا الأسبوع +history.group.month=هذا الشهر +history.group.older=أقدم +history.time.moments=للتو +history.time.minutes=منذ {0} دقيقة +history.time.hours=منذ {0} ساعة +history.time.days=منذ {0} يوم +history.time.months=منذ {0} شهر +history.time.years=منذ {0} سنة +history.error.local=فشل تحميل السجل المحلي +history.error.cloud=فشل تحميل سجل السحابة +history.error.local.delete=فشل حذف الجلسة +history.error.local.rename=فشل إعادة تسمية الجلسة +history.error.cloud.delete=لا يمكن حذف جلسات السحابة بعد + +action.Kilo.Settings.text=الإعدادات +action.Kilo.Settings.description=إعدادات Kilo Code +action.Kilo.NewSession.text=جلسة جديدة +action.Kilo.NewSession.description=بدء جلسة Kilo جديدة +action.Kilo.History.text=السجل +action.Kilo.History.description=عرض سجل الجلسات +action.Kilo.SendPrompt.text=إرسال الطلب +action.Kilo.SendPrompt.description=إرسال طلب Kilo الحالي +action.Kilo.StopSession.text=إيقاف الجلسة +action.Kilo.StopSession.description=إيقاف جلسة Kilo الحالية +action.Kilo.SettingsGroup.text=الإعدادات +action.Kilo.SettingsGroup.description=إعدادات Kilo Code +action.Kilo.Restart.text=إعادة تشغيل Kilo +action.Kilo.Restart.description=إنهاء عملية CLI وإعادة تشغيلها +action.Kilo.Reinstall.text=إعادة تثبيت Kilo +action.Kilo.Reinstall.description=استخراج CLI مجدداً وإعادة التشغيل +action.Kilo.Session.Open.text=فتح +action.Kilo.Session.Open.description=فتح الجلسة المحددة +action.Kilo.Session.Rename.text=إعادة تسمية +action.Kilo.Session.Rename.description=إعادة تسمية الجلسة المحددة +action.Kilo.Session.Delete.text=حذف +action.Kilo.Session.Delete.description=حذف الجلسة (الجلسات) المحددة +action.Kilo.History.ContextMenu.text=إجراءات السجل diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties new file mode 100644 index 00000000000..a310da70e8e --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_bs.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Učitavanje… +session.connection.error.app=Greška pri spajanju +session.connection.error.workspace=Greška pri učitavanju radnog prostora +session.connection.error.unknown=Nepoznata greška +session.connection.retry=Pokušaj ponovo +session.connection.warning.config=Upozorenja konfiguracije + +session.empty.welcome=Kilo Code je AI asistent za kodiranje. Zatražite od njega da gradi funkcije, ispravlja greške ili objašnjava vašu bazu koda. +session.empty.loading=Učitavanje… +session.empty.recent=NEDAVNO +session.showHistory=Prikaži historiju +session.scroll.bottom=Skrolaj na dno +session.tab.new=Nova sesija +session.tab.untitled=Sesija bez naslova + +session.permission.title=Zahtjev za dozvolu +session.permission.meta=Alat: {0} • Uzorci: {1} +session.permission.allow=Dozvoli +session.permission.deny=Odbij +session.question.dismiss=Zatvori + +session.status.considering=Razmatranje sljedećih koraka… +session.status.thinking=Razmišljanje… +session.status.writing=Pisanje odgovora… +session.status.delegating=Delegiranje posla… +session.status.planning=Planiranje… +session.status.gathering=Prikupljanje konteksta… +session.status.searching.codebase=Pretraživanje baze koda… +session.status.searching.web=Pretraživanje weba… +session.status.editing=Uređivanje… +session.status.commands=Pokretanje komandi… + +session.part.reasoning=Rezonovanje +session.part.compaction=kontekst komprimovan +session.part.tool.copy=Kopiraj +session.part.tool.error=Greška +session.part.tool.pending=Na čekanju +session.part.tool.read=Čita +session.part.tool.running=Pokrenuto +session.part.tool.shell=Shell +session.part.tool.truncated=Izlaz skraćen u pregledu. Potpuni izlaz ostaje u podacima sesije. + +session.error.prompt=Greška upita +session.error.compact=Komprimiranje sesije neuspješno +session.error.unknown=Nepoznata greška + +session.header.tokens=Tokeni +session.header.tokens.description=Tokeni korišteni u posljednjem odgovoru asistenta: ulaz, izlaz, pisanja u keš i čitanja iz keša. +session.header.input=ulaz {0} +session.header.output=izlaz {0} +session.header.cache.write=pisanje u keš {0} +session.header.cache.read=čitanje iz keša {0} +session.header.compact=Komprimiraj sesiju +session.header.compact.description=Sažmite sesiju kako biste smanjili veličinu konteksta +session.header.expand=Prikaži metrike sesije +session.header.collapse=Sakrij metrike sesije +session.header.todos.progress={0}/{1} zadataka završeno +session.header.todos.done=Svih {0} zadataka završeno +session.header.context.tooltip.percent={0} tokena ({1}% konteksta) +session.header.context.tooltip.tokens={0} tokena +session.header.context.used={0} / {1} tokena korišteno +session.header.context.reserved={0} rezervisano za izlaz +session.header.context.available={0} dostupno + +prompt.placeholder=Upišite poruku… +prompt.placeholder.with.shortcuts=Upišite poruku… ({0} za slanje, {1} za novi red) +prompt.placeholder.with.send=Upišite poruku… ({0} za slanje) +prompt.placeholder.with.newline=Upišite poruku… ({0} za novi red) +prompt.button.send=Pošalji +prompt.button.stop=Zaustavi +prompt.button.send.tooltip.stop=Da zaustavite, pritisnite {0} +mode.picker.tooltip=Odaberi način rada +mode.picker.deprecated=zastarjelo +model.picker.tooltip=Odaberi model +model.picker.recommended=Preporučeno +model.picker.favorites=Omiljeni +model.picker.search=Pretraži modele +model.picker.no.matches=Nema odgovarajućih modela +model.picker.favorite.add=Dodaj u omiljene +model.picker.favorite.remove=Ukloni iz omiljenih +model.picker.free=Besplatno +model.picker.reset=Vrati model na zadano +reasoning.picker.tooltip=Odaberi razinu rezoniranja + +history.tab.local=Lokalno +history.tab.cloud=Oblak +history.back=Nazad +history.search.placeholder=Pretraži sesije +history.empty=Nema sesija +history.loading=Učitavanje… +history.untitled=Bez naslova +history.delete.text=Obriši +history.delete.confirm.title=Obrisati sesiju? +history.delete.confirm.message=Obrisati "{0}" iz lokalne historije? +history.delete.confirm.message.multiple=Obrisati {0} sesija iz lokalne historije? +history.rename.title=Preimenuj sesiju +history.rename.prompt=Novi naziv sesije: +history.cloud.load.more=Učitaj više +history.cloud.repo.only=Samo ovaj repozitorij +history.group.today=Danas +history.group.yesterday=Jučer +history.group.week=Ove sedmice +history.group.month=Ovog mjeseca +history.group.older=Starije +history.time.moments=Upravo sada +history.time.minutes=Prije {0} min +history.time.hours=Prije {0} h +history.time.days=Prije {0} d +history.time.months=Prije {0} mj +history.time.years=Prije {0} g +history.error.local=Učitavanje lokalne historije neuspješno +history.error.cloud=Učitavanje historije iz oblaka neuspješno +history.error.local.delete=Brisanje sesije neuspješno +history.error.local.rename=Preimenovanje sesije neuspješno +history.error.cloud.delete=Sesije iz oblaka još uvijek nije moguće brisati + +action.Kilo.Settings.text=Postavke +action.Kilo.Settings.description=Postavke Kilo Code +action.Kilo.NewSession.text=Nova sesija +action.Kilo.NewSession.description=Pokrenite novu Kilo sesiju +action.Kilo.History.text=Historija +action.Kilo.History.description=Prikaži historiju sesija +action.Kilo.SendPrompt.text=Pošalji upit +action.Kilo.SendPrompt.description=Pošalji trenutni Kilo upit +action.Kilo.StopSession.text=Zaustavi sesiju +action.Kilo.StopSession.description=Zaustavi trenutnu Kilo sesiju +action.Kilo.SettingsGroup.text=Postavke +action.Kilo.SettingsGroup.description=Postavke Kilo Code +action.Kilo.Restart.text=Ponovo pokrenite Kilo +action.Kilo.Restart.description=Ugasite i ponovo pokrenite CLI proces +action.Kilo.Reinstall.text=Ponovo instalirajte Kilo +action.Kilo.Reinstall.description=Ponovo raspakirajte CLI binarnu datoteku i ponovo pokrenite +action.Kilo.Session.Open.text=Otvori +action.Kilo.Session.Open.description=Otvorite odabranu sesiju +action.Kilo.Session.Rename.text=Preimenuj +action.Kilo.Session.Rename.description=Preimenujte odabranu sesiju +action.Kilo.Session.Delete.text=Obriši +action.Kilo.Session.Delete.description=Obrišite odabrane sesije +action.Kilo.History.ContextMenu.text=Akcije historije diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties new file mode 100644 index 00000000000..706e2faf0b6 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_da.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Indlæser… +session.connection.error.app=Forbindelsesfejl +session.connection.error.workspace=Fejl ved indlæsning af arbejdsområde +session.connection.error.unknown=Ukendt fejl +session.connection.retry=Prøv igen +session.connection.warning.config=Konfigurationsadvarsler + +session.empty.welcome=Kilo Code er en AI-kodningsassistent. Bed den om at bygge funktioner, rette fejl eller forklare din kodebase. +session.empty.loading=Indlæser… +session.empty.recent=SENESTE +session.showHistory=Vis historik +session.scroll.bottom=Rul til bunden +session.tab.new=Ny session +session.tab.untitled=Unavngivet session + +session.permission.title=Tilladelsesanmodning +session.permission.meta=Værktøj: {0} • Mønstre: {1} +session.permission.allow=Tillad +session.permission.deny=Afvis +session.question.dismiss=Luk + +session.status.considering=Overvejer næste skridt… +session.status.thinking=Tænker… +session.status.writing=Skriver svar… +session.status.delegating=Delegerer arbejde… +session.status.planning=Planlagger… +session.status.gathering=Indsamler kontekst… +session.status.searching.codebase=Søger i kodebasen… +session.status.searching.web=Søger på nettet… +session.status.editing=Foretager redigeringer… +session.status.commands=Kører kommandoer… + +session.part.reasoning=Ræsonnering +session.part.compaction=kontekst komprimeret +session.part.tool.copy=Kopiér +session.part.tool.error=Fejl +session.part.tool.pending=Afventer +session.part.tool.read=Læs +session.part.tool.running=Kører +session.part.tool.shell=Shell +session.part.tool.truncated=Output afkortet i forhåndsvisning. Fuldt output forbliver i sessionsdata. + +session.error.prompt=Prompt mislykkedes +session.error.compact=Sessionskomprimering mislykkedes +session.error.unknown=Ukendt fejl + +session.header.tokens=Tokens +session.header.tokens.description=Tokens brugt af det seneste assistentsvar: input, output, cache-skrivninger og cache-læsninger. +session.header.input=ind {0} +session.header.output=ud {0} +session.header.cache.write=cache-skrivning {0} +session.header.cache.read=cache-læsning {0} +session.header.compact=Komprimer session +session.header.compact.description=Opsummer sessionen for at reducere kontekstsstørrelsen +session.header.expand=Vis sessionsmålinger +session.header.collapse=Skjul sessionsmålinger +session.header.todos.progress={0}/{1} opgaver færdige +session.header.todos.done=Alle {0} opgaver færdige +session.header.context.tooltip.percent={0} tokens ({1}% af konteksten) +session.header.context.tooltip.tokens={0} tokens +session.header.context.used={0} / {1} tokens brugt +session.header.context.reserved={0} reserveret til output +session.header.context.available={0} tilgængelige + +prompt.placeholder=Skriv en besked… +prompt.placeholder.with.shortcuts=Skriv en besked… ({0} for at sende, {1} for ny linje) +prompt.placeholder.with.send=Skriv en besked… ({0} for at sende) +prompt.placeholder.with.newline=Skriv en besked… ({0} for ny linje) +prompt.button.send=Send +prompt.button.stop=Stop +prompt.button.send.tooltip.stop=Tryk på {0} for at stoppe +mode.picker.tooltip=Vælg tilstand +mode.picker.deprecated=forældet +model.picker.tooltip=Vælg model +model.picker.recommended=Anbefalet +model.picker.favorites=Favoritter +model.picker.search=Søg efter modeller +model.picker.no.matches=Ingen matchende modeller +model.picker.favorite.add=Føj til favoritter +model.picker.favorite.remove=Fjern fra favoritter +model.picker.free=Gratis +model.picker.reset=Nulstil model til standard +reasoning.picker.tooltip=Vælg ræsonneringsindsats + +history.tab.local=Lokal +history.tab.cloud=Sky +history.back=Tilbage +history.search.placeholder=Søg i sessioner +history.empty=Ingen sessioner +history.loading=Indlæser… +history.untitled=Unavngivet +history.delete.text=Slet +history.delete.confirm.title=Slet session? +history.delete.confirm.message=Slet "{0}" fra lokal historik? +history.delete.confirm.message.multiple=Slet {0} sessioner fra lokal historik? +history.rename.title=Omdøb session +history.rename.prompt=Nyt sessionsnavn: +history.cloud.load.more=Indlæs mere +history.cloud.repo.only=Kun dette arkiv +history.group.today=I dag +history.group.yesterday=I går +history.group.week=Denne uge +history.group.month=Denne måned +history.group.older=Ældre +history.time.moments=Lige nu +history.time.minutes={0} min siden +history.time.hours={0} t siden +history.time.days={0} d siden +history.time.months={0} mdr siden +history.time.years={0} år siden +history.error.local=Kunne ikke indlæse lokal historik +history.error.cloud=Kunne ikke indlæse skyhistorik +history.error.local.delete=Kunne ikke slette session +history.error.local.rename=Kunne ikke omdøbe session +history.error.cloud.delete=Skysessioner kan endnu ikke slettes + +action.Kilo.Settings.text=Indstillinger +action.Kilo.Settings.description=Kilo Code-indstillinger +action.Kilo.NewSession.text=Ny session +action.Kilo.NewSession.description=Start en ny Kilo-session +action.Kilo.History.text=Historik +action.Kilo.History.description=Vis sessionshistorik +action.Kilo.SendPrompt.text=Send prompt +action.Kilo.SendPrompt.description=Send den aktuelle Kilo-prompt +action.Kilo.StopSession.text=Stop session +action.Kilo.StopSession.description=Stop den aktuelle Kilo-session +action.Kilo.SettingsGroup.text=Indstillinger +action.Kilo.SettingsGroup.description=Kilo Code-indstillinger +action.Kilo.Restart.text=Genstart Kilo +action.Kilo.Restart.description=Afslut og genstart CLI-processen +action.Kilo.Reinstall.text=Geninstaller Kilo +action.Kilo.Reinstall.description=Udpak CLI-binærfilen igen og genstart +action.Kilo.Session.Open.text=Åbn +action.Kilo.Session.Open.description=Åbn den valgte session +action.Kilo.Session.Rename.text=Omdøb +action.Kilo.Session.Rename.description=Omdøb den valgte session +action.Kilo.Session.Delete.text=Slet +action.Kilo.Session.Delete.description=Slet den/de valgte session(er) +action.Kilo.History.ContextMenu.text=Historikhandlinger diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties new file mode 100644 index 00000000000..f604c18c850 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_de.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Wird geladen… +session.connection.error.app=Verbindung fehlgeschlagen +session.connection.error.workspace=Workspace-Laden fehlgeschlagen +session.connection.error.unknown=Unbekannter Fehler +session.connection.retry=Erneut versuchen +session.connection.warning.config=Konfigurationswarnungen + +session.empty.welcome=Kilo Code ist ein KI-Coding-Assistent. Bitten Sie ihn, Funktionen zu erstellen, Fehler zu beheben oder Ihre Codebasis zu erklären. +session.empty.loading=Wird geladen… +session.empty.recent=ZULETZT +session.showHistory=Verlauf anzeigen +session.scroll.bottom=Zum Ende scrollen +session.tab.new=Neue Sitzung +session.tab.untitled=Unbenannte Sitzung + +session.permission.title=Berechtigungsanfrage +session.permission.meta=Werkzeug: {0} • Muster: {1} +session.permission.allow=Erlauben +session.permission.deny=Ablehnen +session.question.dismiss=Schließen + +session.status.considering=Nächste Schritte abwägen… +session.status.thinking=Nachdenken… +session.status.writing=Antwort schreiben… +session.status.delegating=Arbeit delegieren… +session.status.planning=Planen… +session.status.gathering=Kontext sammeln… +session.status.searching.codebase=Codebasis durchsuchen… +session.status.searching.web=Web durchsuchen… +session.status.editing=Änderungen vornehmen… +session.status.commands=Befehle ausführen… + +session.part.reasoning=Denken +session.part.compaction=Kontext komprimiert +session.part.tool.copy=Kopieren +session.part.tool.error=Fehler +session.part.tool.pending=Ausstehend +session.part.tool.read=Lesen +session.part.tool.running=Läuft +session.part.tool.shell=Shell +session.part.tool.truncated=Ausgabe in der Vorschau gekürzt. Vollständige Ausgabe verbleibt in den Sitzungsdaten. + +session.error.prompt=Anfrage fehlgeschlagen +session.error.compact=Sitzungskomprimierung fehlgeschlagen +session.error.unknown=Unbekannter Fehler + +session.header.tokens=Token +session.header.tokens.description=Von der letzten Assistentenantwort verwendete Token: Eingabe, Ausgabe, Cache-Schreibvorgänge und Cache-Lesevorgänge. +session.header.input=ein {0} +session.header.output=aus {0} +session.header.cache.write=Cache-Schreiben {0} +session.header.cache.read=Cache-Lesen {0} +session.header.compact=Sitzung komprimieren +session.header.compact.description=Sitzung zusammenfassen, um die Kontextgröße zu reduzieren +session.header.expand=Sitzungsmetriken anzeigen +session.header.collapse=Sitzungsmetriken ausblenden +session.header.todos.progress={0}/{1} Aufgaben abgeschlossen +session.header.todos.done=Alle {0} Aufgaben abgeschlossen +session.header.context.tooltip.percent={0} Token ({1}% des Kontexts) +session.header.context.tooltip.tokens={0} Token +session.header.context.used={0} / {1} Token verwendet +session.header.context.reserved={0} für Ausgabe reserviert +session.header.context.available={0} verfügbar + +prompt.placeholder=Nachricht eingeben… +prompt.placeholder.with.shortcuts=Nachricht eingeben… ({0} zum Senden, {1} für neue Zeile) +prompt.placeholder.with.send=Nachricht eingeben… ({0} zum Senden) +prompt.placeholder.with.newline=Nachricht eingeben… ({0} für neue Zeile) +prompt.button.send=Senden +prompt.button.stop=Stopp +prompt.button.send.tooltip.stop=Zum Stoppen drücken Sie {0} +mode.picker.tooltip=Modus auswählen +mode.picker.deprecated=veraltet +model.picker.tooltip=Modell auswählen +model.picker.recommended=Empfohlen +model.picker.favorites=Favoriten +model.picker.search=Modelle suchen +model.picker.no.matches=Keine passenden Modelle +model.picker.favorite.add=Zu Favoriten hinzufügen +model.picker.favorite.remove=Aus Favoriten entfernen +model.picker.free=Kostenlos +model.picker.reset=Modell auf Standard zurücksetzen +reasoning.picker.tooltip=Denkanstrengung auswählen + +history.tab.local=Lokal +history.tab.cloud=Cloud +history.back=Zurück +history.search.placeholder=Sitzungen suchen +history.empty=Keine Sitzungen +history.loading=Wird geladen… +history.untitled=Unbenannt +history.delete.text=Löschen +history.delete.confirm.title=Sitzung löschen? +history.delete.confirm.message="{0}" aus dem lokalen Verlauf löschen? +history.delete.confirm.message.multiple={0} Sitzungen aus dem lokalen Verlauf löschen? +history.rename.title=Sitzung umbenennen +history.rename.prompt=Neuer Sitzungsname: +history.cloud.load.more=Mehr laden +history.cloud.repo.only=Nur dieses Repository +history.group.today=Heute +history.group.yesterday=Gestern +history.group.week=Diese Woche +history.group.month=Dieser Monat +history.group.older=Älter +history.time.moments=Gerade eben +history.time.minutes={0} Min. zuvor +history.time.hours={0} Std. zuvor +history.time.days={0} Tage zuvor +history.time.months={0} Mon. zuvor +history.time.years={0} J. zuvor +history.error.local=Lokaler Verlauf konnte nicht geladen werden +history.error.cloud=Cloud-Verlauf konnte nicht geladen werden +history.error.local.delete=Sitzung konnte nicht gelöscht werden +history.error.local.rename=Sitzung konnte nicht umbenannt werden +history.error.cloud.delete=Cloud-Sitzungen können noch nicht gelöscht werden + +action.Kilo.Settings.text=Einstellungen +action.Kilo.Settings.description=Kilo Code Einstellungen +action.Kilo.NewSession.text=Neue Sitzung +action.Kilo.NewSession.description=Neue Kilo-Sitzung starten +action.Kilo.History.text=Verlauf +action.Kilo.History.description=Sitzungsverlauf anzeigen +action.Kilo.SendPrompt.text=Anfrage senden +action.Kilo.SendPrompt.description=Aktuelle Kilo-Anfrage senden +action.Kilo.StopSession.text=Sitzung stoppen +action.Kilo.StopSession.description=Aktuelle Kilo-Sitzung stoppen +action.Kilo.SettingsGroup.text=Einstellungen +action.Kilo.SettingsGroup.description=Kilo Code Einstellungen +action.Kilo.Restart.text=Kilo neu starten +action.Kilo.Restart.description=CLI-Prozess beenden und neu starten +action.Kilo.Reinstall.text=Kilo neu installieren +action.Kilo.Reinstall.description=CLI-Binärdatei neu extrahieren und neu starten +action.Kilo.Session.Open.text=Öffnen +action.Kilo.Session.Open.description=Ausgewählte Sitzung öffnen +action.Kilo.Session.Rename.text=Umbenennen +action.Kilo.Session.Rename.description=Ausgewählte Sitzung umbenennen +action.Kilo.Session.Delete.text=Löschen +action.Kilo.Session.Delete.description=Ausgewählte Sitzung(en) löschen +action.Kilo.History.ContextMenu.text=Verlaufsaktionen diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties new file mode 100644 index 00000000000..92fbad3508f --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_es.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Cargando… +session.connection.error.app=Error de conexión +session.connection.error.workspace=Error al cargar el espacio de trabajo +session.connection.error.unknown=Error desconocido +session.connection.retry=Reintentar +session.connection.warning.config=Advertencias de configuración + +session.empty.welcome=Kilo Code es un asistente de codificación con IA. Pida que construya funciones, corrija errores o explique su base de código. +session.empty.loading=Cargando… +session.empty.recent=RECIENTE +session.showHistory=Mostrar historial +session.scroll.bottom=Desplazarse al final +session.tab.new=Nueva sesión +session.tab.untitled=Sesión sin título + +session.permission.title=Solicitud de permiso +session.permission.meta=Herramienta: {0} • Patrones: {1} +session.permission.allow=Permitir +session.permission.deny=Denegar +session.question.dismiss=Descartar + +session.status.considering=Considerando los próximos pasos… +session.status.thinking=Pensando… +session.status.writing=Escribiendo respuesta… +session.status.delegating=Delegando trabajo… +session.status.planning=Planificando… +session.status.gathering=Recopilando contexto… +session.status.searching.codebase=Buscando en la base de código… +session.status.searching.web=Buscando en la web… +session.status.editing=Realizando ediciones… +session.status.commands=Ejecutando comandos… + +session.part.reasoning=Razonamiento +session.part.compaction=contexto compactado +session.part.tool.copy=Copiar +session.part.tool.error=Error +session.part.tool.pending=Pendiente +session.part.tool.read=Leer +session.part.tool.running=Ejecutando +session.part.tool.shell=Shell +session.part.tool.truncated=Salida truncada en la vista previa. La salida completa permanece en los datos de la sesión. + +session.error.prompt=Error en el mensaje +session.error.compact=Error al compactar la sesión +session.error.unknown=Error desconocido + +session.header.tokens=Tokens +session.header.tokens.description=Tokens utilizados por la última respuesta del asistente: entrada, salida, escrituras en caché y lecturas en caché. +session.header.input=entrada {0} +session.header.output=salida {0} +session.header.cache.write=escritura caché {0} +session.header.cache.read=lectura caché {0} +session.header.compact=Compactar sesión +session.header.compact.description=Resumir la sesión para reducir el tamaño del contexto +session.header.expand=Mostrar métricas de sesión +session.header.collapse=Ocultar métricas de sesión +session.header.todos.progress={0}/{1} tareas completadas +session.header.todos.done=Todas las {0} tareas completadas +session.header.context.tooltip.percent={0} tokens ({1}% del contexto) +session.header.context.tooltip.tokens={0} tokens +session.header.context.used={0} / {1} tokens utilizados +session.header.context.reserved={0} reservados para la salida +session.header.context.available={0} disponibles + +prompt.placeholder=Escribe un mensaje… +prompt.placeholder.with.shortcuts=Escribe un mensaje… ({0} para enviar, {1} para nueva línea) +prompt.placeholder.with.send=Escribe un mensaje… ({0} para enviar) +prompt.placeholder.with.newline=Escribe un mensaje… ({0} para nueva línea) +prompt.button.send=Enviar +prompt.button.stop=Detener +prompt.button.send.tooltip.stop=Para detener, presiona {0} +mode.picker.tooltip=Seleccionar modo +mode.picker.deprecated=obsoleto +model.picker.tooltip=Seleccionar modelo +model.picker.recommended=Recomendado +model.picker.favorites=Favoritos +model.picker.search=Buscar modelos +model.picker.no.matches=No hay modelos coincidentes +model.picker.favorite.add=Añadir a favoritos +model.picker.favorite.remove=Eliminar de favoritos +model.picker.free=Gratis +model.picker.reset=Restablecer modelo a predeterminado +reasoning.picker.tooltip=Seleccionar esfuerzo de razonamiento + +history.tab.local=Local +history.tab.cloud=Nube +history.back=Atrás +history.search.placeholder=Buscar sesiones +history.empty=Sin sesiones +history.loading=Cargando… +history.untitled=Sin título +history.delete.text=Eliminar +history.delete.confirm.title=¿Eliminar sesión? +history.delete.confirm.message=¿Eliminar "{0}" del historial local? +history.delete.confirm.message.multiple=¿Eliminar {0} sesiones del historial local? +history.rename.title=Renombrar sesión +history.rename.prompt=Nuevo nombre de sesión: +history.cloud.load.more=Cargar más +history.cloud.repo.only=Solo este repositorio +history.group.today=Hoy +history.group.yesterday=Ayer +history.group.week=Esta semana +history.group.month=Este mes +history.group.older=Más antiguo +history.time.moments=Hace un momento +history.time.minutes=Hace {0} min +history.time.hours=Hace {0} h +history.time.days=Hace {0} d +history.time.months=Hace {0} mes(es) +history.time.years=Hace {0} año(s) +history.error.local=Error al cargar el historial local +history.error.cloud=Error al cargar el historial en la nube +history.error.local.delete=Error al eliminar la sesión +history.error.local.rename=Error al renombrar la sesión +history.error.cloud.delete=Las sesiones en la nube no se pueden eliminar aún + +action.Kilo.Settings.text=Configuración +action.Kilo.Settings.description=Configuración de Kilo Code +action.Kilo.NewSession.text=Nueva sesión +action.Kilo.NewSession.description=Iniciar una nueva sesión de Kilo +action.Kilo.History.text=Historial +action.Kilo.History.description=Mostrar el historial de sesiones +action.Kilo.SendPrompt.text=Enviar mensaje +action.Kilo.SendPrompt.description=Enviar el mensaje actual de Kilo +action.Kilo.StopSession.text=Detener sesión +action.Kilo.StopSession.description=Detener la sesión actual de Kilo +action.Kilo.SettingsGroup.text=Configuración +action.Kilo.SettingsGroup.description=Configuración de Kilo Code +action.Kilo.Restart.text=Reiniciar Kilo +action.Kilo.Restart.description=Terminar y reiniciar el proceso CLI +action.Kilo.Reinstall.text=Reinstalar Kilo +action.Kilo.Reinstall.description=Reextraer el binario CLI y reiniciar +action.Kilo.Session.Open.text=Abrir +action.Kilo.Session.Open.description=Abrir la sesión seleccionada +action.Kilo.Session.Rename.text=Renombrar +action.Kilo.Session.Rename.description=Renombrar la sesión seleccionada +action.Kilo.Session.Delete.text=Eliminar +action.Kilo.Session.Delete.description=Eliminar la(s) sesión(es) seleccionada(s) +action.Kilo.History.ContextMenu.text=Acciones del historial diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties new file mode 100644 index 00000000000..bf51dd4e426 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_fr.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Chargement… +session.connection.error.app=Échec de la connexion +session.connection.error.workspace=Échec du chargement de l'espace de travail +session.connection.error.unknown=Erreur inconnue +session.connection.retry=Réessayer +session.connection.warning.config=Avertissements de configuration + +session.empty.welcome=Kilo Code est un assistant de codage IA. Demandez-lui de créer des fonctionnalités, corriger des bugs ou expliquer votre base de code. +session.empty.loading=Chargement… +session.empty.recent=RÉCENT +session.showHistory=Afficher l'historique +session.scroll.bottom=Faire défiler vers le bas +session.tab.new=Nouvelle session +session.tab.untitled=Session sans titre + +session.permission.title=Demande de permission +session.permission.meta=Outil : {0} • Modèles : {1} +session.permission.allow=Autoriser +session.permission.deny=Refuser +session.question.dismiss=Ignorer + +session.status.considering=Considération des prochaines étapes… +session.status.thinking=Réflexion… +session.status.writing=Rédaction de la réponse… +session.status.delegating=Délégation du travail… +session.status.planning=Planification… +session.status.gathering=Collecte du contexte… +session.status.searching.codebase=Recherche dans la base de code… +session.status.searching.web=Recherche sur le web… +session.status.editing=Modifications en cours… +session.status.commands=Exécution des commandes… + +session.part.reasoning=Raisonnement +session.part.compaction=contexte compacté +session.part.tool.copy=Copier +session.part.tool.error=Erreur +session.part.tool.pending=En attente +session.part.tool.read=Lire +session.part.tool.running=En cours +session.part.tool.shell=Shell +session.part.tool.truncated=Sortie tronquée dans l'aperçu. La sortie complète reste dans les données de session. + +session.error.prompt=Échec de l'invite +session.error.compact=Échec de la compaction de session +session.error.unknown=Erreur inconnue + +session.header.tokens=Tokens +session.header.tokens.description=Tokens utilisés par la dernière réponse de l'assistant : entrée, sortie, écritures en cache et lectures en cache. +session.header.input=entrée {0} +session.header.output=sortie {0} +session.header.cache.write=écriture cache {0} +session.header.cache.read=lecture cache {0} +session.header.compact=Compacter la session +session.header.compact.description=Résumer la session pour réduire la taille du contexte +session.header.expand=Afficher les métriques de session +session.header.collapse=Masquer les métriques de session +session.header.todos.progress={0}/{1} tâches terminées +session.header.todos.done=Toutes les {0} tâches terminées +session.header.context.tooltip.percent={0} tokens ({1} % du contexte) +session.header.context.tooltip.tokens={0} tokens +session.header.context.used={0} / {1} tokens utilisés +session.header.context.reserved={0} réservés pour la sortie +session.header.context.available={0} disponibles + +prompt.placeholder=Saisir un message… +prompt.placeholder.with.shortcuts=Saisir un message… ({0} pour envoyer, {1} pour nouvelle ligne) +prompt.placeholder.with.send=Saisir un message… ({0} pour envoyer) +prompt.placeholder.with.newline=Saisir un message… ({0} pour nouvelle ligne) +prompt.button.send=Envoyer +prompt.button.stop=Arrêter +prompt.button.send.tooltip.stop=Pour arrêter, appuyez sur {0} +mode.picker.tooltip=Sélectionner le mode +mode.picker.deprecated=obsolète +model.picker.tooltip=Sélectionner le modèle +model.picker.recommended=Recommandé +model.picker.favorites=Favoris +model.picker.search=Rechercher des modèles +model.picker.no.matches=Aucun modèle correspondant +model.picker.favorite.add=Ajouter aux favoris +model.picker.favorite.remove=Retirer des favoris +model.picker.free=Gratuit +model.picker.reset=Réinitialiser le modèle par défaut +reasoning.picker.tooltip=Sélectionner l'effort de raisonnement + +history.tab.local=Local +history.tab.cloud=Cloud +history.back=Retour +history.search.placeholder=Rechercher des sessions +history.empty=Aucune session +history.loading=Chargement… +history.untitled=Sans titre +history.delete.text=Supprimer +history.delete.confirm.title=Supprimer la session ? +history.delete.confirm.message=Supprimer « {0} » de l'historique local ? +history.delete.confirm.message.multiple=Supprimer {0} sessions de l'historique local ? +history.rename.title=Renommer la session +history.rename.prompt=Nouveau nom de session : +history.cloud.load.more=Charger plus +history.cloud.repo.only=Ce dépôt uniquement +history.group.today=Aujourd'hui +history.group.yesterday=Hier +history.group.week=Cette semaine +history.group.month=Ce mois +history.group.older=Plus ancien +history.time.moments=À l'instant +history.time.minutes=Il y a {0} min +history.time.hours=Il y a {0} h +history.time.days=Il y a {0} j +history.time.months=Il y a {0} mois +history.time.years=Il y a {0} an(s) +history.error.local=Échec du chargement de l'historique local +history.error.cloud=Échec du chargement de l'historique cloud +history.error.local.delete=Échec de la suppression de la session +history.error.local.rename=Échec du renommage de la session +history.error.cloud.delete=Les sessions cloud ne peuvent pas encore être supprimées + +action.Kilo.Settings.text=Paramètres +action.Kilo.Settings.description=Paramètres de Kilo Code +action.Kilo.NewSession.text=Nouvelle session +action.Kilo.NewSession.description=Démarrer une nouvelle session Kilo +action.Kilo.History.text=Historique +action.Kilo.History.description=Afficher l'historique des sessions +action.Kilo.SendPrompt.text=Envoyer l'invite +action.Kilo.SendPrompt.description=Envoyer l'invite Kilo actuelle +action.Kilo.StopSession.text=Arrêter la session +action.Kilo.StopSession.description=Arrêter la session Kilo actuelle +action.Kilo.SettingsGroup.text=Paramètres +action.Kilo.SettingsGroup.description=Paramètres de Kilo Code +action.Kilo.Restart.text=Redémarrer Kilo +action.Kilo.Restart.description=Tuer et redémarrer le processus CLI +action.Kilo.Reinstall.text=Réinstaller Kilo +action.Kilo.Reinstall.description=Réextraire le binaire CLI et redémarrer +action.Kilo.Session.Open.text=Ouvrir +action.Kilo.Session.Open.description=Ouvrir la session sélectionnée +action.Kilo.Session.Rename.text=Renommer +action.Kilo.Session.Rename.description=Renommer la session sélectionnée +action.Kilo.Session.Delete.text=Supprimer +action.Kilo.Session.Delete.description=Supprimer la ou les sessions sélectionnées +action.Kilo.History.ContextMenu.text=Actions d'historique diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties new file mode 100644 index 00000000000..c32cc633e9f --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ja.properties @@ -0,0 +1,139 @@ +session.connection.connecting=読み込み中… +session.connection.error.app=接続に失敗しました +session.connection.error.workspace=ワークスペースの読み込みに失敗しました +session.connection.error.unknown=不明なエラー +session.connection.retry=再試行 +session.connection.warning.config=設定の警告 + +session.empty.welcome=Kilo CodeはAIコーディングアシスタントです。機能の作成、バグの修正、またはコードベースの説明を依頼できます。 +session.empty.loading=読み込み中… +session.empty.recent=最近 +session.showHistory=履歴を表示 +session.scroll.bottom=一番下にスクロール +session.tab.new=新しいセッション +session.tab.untitled=名前なしのセッション + +session.permission.title=権限リクエスト +session.permission.meta=ツール:{0} • パターン:{1} +session.permission.allow=許可 +session.permission.deny=拒否 +session.question.dismiss=閉じる + +session.status.considering=次のステップを検討中… +session.status.thinking=考え中… +session.status.writing=回答を作成中… +session.status.delegating=作業を委任中… +session.status.planning=計画中… +session.status.gathering=コンテキストを収集中… +session.status.searching.codebase=コードベースを検索中… +session.status.searching.web=ウェブを検索中… +session.status.editing=編集中… +session.status.commands=コマンドを実行中… + +session.part.reasoning=推論 +session.part.compaction=コンテキスト圧縮済み +session.part.tool.copy=コピー +session.part.tool.error=エラー +session.part.tool.pending=保留中 +session.part.tool.read=読み取り +session.part.tool.running=実行中 +session.part.tool.shell=シェル +session.part.tool.truncated=プレビューでは出力が切り詰められています。完全な出力はセッションデータに残っています。 + +session.error.prompt=プロンプトが失敗しました +session.error.compact=セッション圧縮が失敗しました +session.error.unknown=不明なエラー + +session.header.tokens=トークン +session.header.tokens.description=最新のアシスタントの回答で使用されたトークン:入力、出力、キャッシュ書き込み、キャッシュ読み取り。 +session.header.input=入力 {0} +session.header.output=出力 {0} +session.header.cache.write=キャッシュ書き込み {0} +session.header.cache.read=キャッシュ読み取り {0} +session.header.compact=セッションを圧縮 +session.header.compact.description=セッションを要約してコンテキストサイズを削減 +session.header.expand=セッションメトリクスを表示 +session.header.collapse=セッションメトリクスを非表示 +session.header.todos.progress={0}/{1}個のタスク完了 +session.header.todos.done=全{0}個のタスク完了 +session.header.context.tooltip.percent={0}トークン(コンテキストの{1}%) +session.header.context.tooltip.tokens={0}トークン +session.header.context.used={0} / {1}トークン使用中 +session.header.context.reserved={0}は出力用に予約済み +session.header.context.available={0}利用可能 + +prompt.placeholder=メッセージを入力… +prompt.placeholder.with.shortcuts=メッセージを入力…({0}で送信、{1}で改行) +prompt.placeholder.with.send=メッセージを入力…({0}で送信) +prompt.placeholder.with.newline=メッセージを入力…({0}で改行) +prompt.button.send=送信 +prompt.button.stop=停止 +prompt.button.send.tooltip.stop=停止するには{0}を押してください +mode.picker.tooltip=モードを選択 +mode.picker.deprecated=非推奨 +model.picker.tooltip=モデルを選択 +model.picker.recommended=おすすめ +model.picker.favorites=お気に入り +model.picker.search=モデルを検索 +model.picker.no.matches=一致するモデルなし +model.picker.favorite.add=お気に入りに追加 +model.picker.favorite.remove=お気に入りから削除 +model.picker.free=無料 +model.picker.reset=モデルをデフォルトにリセット +reasoning.picker.tooltip=推論努力を選択 + +history.tab.local=ローカル +history.tab.cloud=クラウド +history.back=戻る +history.search.placeholder=セッションを検索 +history.empty=セッションなし +history.loading=読み込み中… +history.untitled=名前なし +history.delete.text=削除 +history.delete.confirm.title=セッションを削除しますか? +history.delete.confirm.message=「{0}」をローカル履歴から削除しますか? +history.delete.confirm.message.multiple={0}個のセッションをローカル履歴から削除しますか? +history.rename.title=セッションを変名 +history.rename.prompt=新しいセッション名: +history.cloud.load.more=さらに読み込む +history.cloud.repo.only=このリポジトリのみ +history.group.today=今日 +history.group.yesterday=昨日 +history.group.week=今週 +history.group.month=今月 +history.group.older=さらに古い +history.time.moments=たった今 +history.time.minutes={0}分前 +history.time.hours={0}時間前 +history.time.days={0}日前 +history.time.months={0}か月前 +history.time.years={0}年前 +history.error.local=ローカル履歴の読み込みに失敗しました +history.error.cloud=クラウド履歴の読み込みに失敗しました +history.error.local.delete=セッションの削除に失敗しました +history.error.local.rename=セッションの変名に失敗しました +history.error.cloud.delete=クラウドセッションはまだ削除できません + +action.Kilo.Settings.text=設定 +action.Kilo.Settings.description=Kilo Codeの設定 +action.Kilo.NewSession.text=新しいセッション +action.Kilo.NewSession.description=新しいKiloセッションを開始 +action.Kilo.History.text=履歴 +action.Kilo.History.description=セッション履歴を表示 +action.Kilo.SendPrompt.text=プロンプトを送信 +action.Kilo.SendPrompt.description=現在のKiloプロンプトを送信 +action.Kilo.StopSession.text=セッションを停止 +action.Kilo.StopSession.description=現在のKiloセッションを停止 +action.Kilo.SettingsGroup.text=設定 +action.Kilo.SettingsGroup.description=Kilo Codeの設定 +action.Kilo.Restart.text=Kiloを再起動 +action.Kilo.Restart.description=CLIプロセスを終了して再起動 +action.Kilo.Reinstall.text=Kiloを再インストール +action.Kilo.Reinstall.description=CLIバイナリを再展開して再起動 +action.Kilo.Session.Open.text=開く +action.Kilo.Session.Open.description=選択したセッションを開く +action.Kilo.Session.Rename.text=変名 +action.Kilo.Session.Rename.description=選択したセッションを変名 +action.Kilo.Session.Delete.text=削除 +action.Kilo.Session.Delete.description=選択したセッションを削除 +action.Kilo.History.ContextMenu.text=履歴アクション diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties new file mode 100644 index 00000000000..893abc95b96 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ko.properties @@ -0,0 +1,139 @@ +session.connection.connecting=로딩 중… +session.connection.error.app=연결 실패 +session.connection.error.workspace=워크스페이스 로딩 실패 +session.connection.error.unknown=알 수 없는 오류 +session.connection.retry=다시 시도 +session.connection.warning.config=구성 경고 + +session.empty.welcome=Kilo Code는 AI 코딩 어시스턴트입니다. 기능 구축, 버그 수정, 코드베이스 설명을 요청하세요. +session.empty.loading=로딩 중… +session.empty.recent=최근 +session.showHistory=기록 보기 +session.scroll.bottom=맨 아래로 스크롤 +session.tab.new=새 세션 +session.tab.untitled=제목 없는 세션 + +session.permission.title=권한 요청 +session.permission.meta=도구: {0} • 패턴: {1} +session.permission.allow=허용 +session.permission.deny=거부 +session.question.dismiss=닫기 + +session.status.considering=다음 단계 고려 중… +session.status.thinking=생각 중… +session.status.writing=응답 작성 중… +session.status.delegating=작업 위임 중… +session.status.planning=계획 중… +session.status.gathering=컨텍스트 수집 중… +session.status.searching.codebase=코드베이스 검색 중… +session.status.searching.web=웹 검색 중… +session.status.editing=편집 중… +session.status.commands=명령 실행 중… + +session.part.reasoning=추론 +session.part.compaction=컨텍스트 압축됨 +session.part.tool.copy=복사 +session.part.tool.error=오류 +session.part.tool.pending=대기 중 +session.part.tool.read=읽기 +session.part.tool.running=실행 중 +session.part.tool.shell=셸 +session.part.tool.truncated=미리보기에서 출력이 잘렸습니다. 전체 출력은 세션 데이터에 남아 있습니다. + +session.error.prompt=프롬프트 실패 +session.error.compact=세션 압축 실패 +session.error.unknown=알 수 없는 오류 + +session.header.tokens=토큰 +session.header.tokens.description=마지막 어시스턴트 응답에서 사용된 토큰: 입력, 출력, 캐시 쓰기, 캐시 읽기. +session.header.input=입력 {0} +session.header.output=출력 {0} +session.header.cache.write=캐시 쓰기 {0} +session.header.cache.read=캐시 읽기 {0} +session.header.compact=세션 압축 +session.header.compact.description=컨텍스트 크기를 줄이기 위해 세션 요약 +session.header.expand=세션 메트릭 표시 +session.header.collapse=세션 메트릭 숨기기 +session.header.todos.progress={0}/{1} 할 일 완료 +session.header.todos.done=모든 {0}개 할 일 완료 +session.header.context.tooltip.percent={0} 토큰 (컨텍스트의 {1}%) +session.header.context.tooltip.tokens={0} 토큰 +session.header.context.used={0} / {1} 토큰 사용 중 +session.header.context.reserved={0} 출력용 예약됨 +session.header.context.available={0} 사용 가능 + +prompt.placeholder=메시지를 입력하세요… +prompt.placeholder.with.shortcuts=메시지를 입력하세요… ({0} 전송, {1} 새 줄) +prompt.placeholder.with.send=메시지를 입력하세요… ({0} 전송) +prompt.placeholder.with.newline=메시지를 입력하세요… ({0} 새 줄) +prompt.button.send=전송 +prompt.button.stop=정지 +prompt.button.send.tooltip.stop=정지하려면 {0}을 누르세요 +mode.picker.tooltip=모드 선택 +mode.picker.deprecated=사용 중단 +model.picker.tooltip=모델 선택 +model.picker.recommended=추천 +model.picker.favorites=즐겨찾기 +model.picker.search=모델 검색 +model.picker.no.matches=일치하는 모델 없음 +model.picker.favorite.add=즐겨찾기에 추가 +model.picker.favorite.remove=즐겨찾기에서 제거 +model.picker.free=무료 +model.picker.reset=모델을 기본값으로 재설정 +reasoning.picker.tooltip=추론 노력 선택 + +history.tab.local=로컈 +history.tab.cloud=클라우드 +history.back=뒤로 +history.search.placeholder=세션 검색 +history.empty=세션 없음 +history.loading=로딩 중… +history.untitled=제목 없음 +history.delete.text=삭제 +history.delete.confirm.title=세션을 삭제하시겠습니까? +history.delete.confirm.message=로컈 기록에서 "{0}"을 삭제하시겠습니까? +history.delete.confirm.message.multiple=로컈 기록에서 {0}개의 세션을 삭제하시겠습니까? +history.rename.title=세션 이름 바꾸기 +history.rename.prompt=새 세션 이름: +history.cloud.load.more=더 불러오기 +history.cloud.repo.only=이 리포지터리만 +history.group.today=오늘 +history.group.yesterday=어제 +history.group.week=이번 주 +history.group.month=이번 달 +history.group.older=더 오래된 +history.time.moments=방금 +history.time.minutes={0}분 전 +history.time.hours={0}시간 전 +history.time.days={0}일 전 +history.time.months={0}개월 전 +history.time.years={0}년 전 +history.error.local=로컈 기록을 로드하지 못했습니다 +history.error.cloud=클라우드 기록을 로드하지 못했습니다 +history.error.local.delete=세션을 삭제하지 못했습니다 +history.error.local.rename=세션의 이름을 바꾸지 못했습니다 +history.error.cloud.delete=클라우드 세션은 아직 삭제할 수 없습니다 + +action.Kilo.Settings.text=설정 +action.Kilo.Settings.description=Kilo Code 설정 +action.Kilo.NewSession.text=새 세션 +action.Kilo.NewSession.description=새 Kilo 세션 시작 +action.Kilo.History.text=기록 +action.Kilo.History.description=세션 기록 표시 +action.Kilo.SendPrompt.text=프롬프트 전송 +action.Kilo.SendPrompt.description=현재 Kilo 프롬프트 전송 +action.Kilo.StopSession.text=세션 정지 +action.Kilo.StopSession.description=현재 Kilo 세션 정지 +action.Kilo.SettingsGroup.text=설정 +action.Kilo.SettingsGroup.description=Kilo Code 설정 +action.Kilo.Restart.text=Kilo 재시작 +action.Kilo.Restart.description=CLI 프로세스 종료 후 재시작 +action.Kilo.Reinstall.text=Kilo 재설치 +action.Kilo.Reinstall.description=CLI 바이너리 재추출 후 재시작 +action.Kilo.Session.Open.text=열기 +action.Kilo.Session.Open.description=선택한 세션 열기 +action.Kilo.Session.Rename.text=이름 바꾸기 +action.Kilo.Session.Rename.description=선택한 세션 이름 바꾸기 +action.Kilo.Session.Delete.text=삭제 +action.Kilo.Session.Delete.description=선택한 세션 삭제 +action.Kilo.History.ContextMenu.text=기록 작업 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties new file mode 100644 index 00000000000..e88d7960997 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_nl.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Laden… +session.connection.error.app=Verbinding mislukt +session.connection.error.workspace=Werkruimte laden mislukt +session.connection.error.unknown=Onbekende fout +session.connection.retry=Opnieuw proberen +session.connection.warning.config=Configuratiewaarschuwingen + +session.empty.welcome=Kilo Code is een AI-codeerassistent. Vraag het om functies te bouwen, bugs te repareren of uw codebase uit te leggen. +session.empty.loading=Laden… +session.empty.recent=RECENT +session.showHistory=Geschiedenis weergeven +session.scroll.bottom=Naar beneden scrollen +session.tab.new=Nieuwe sessie +session.tab.untitled=Naamloze sessie + +session.permission.title=Toestemmingsverzoek +session.permission.meta=Hulpmiddel: {0} • Patronen: {1} +session.permission.allow=Toestaan +session.permission.deny=Weigeren +session.question.dismiss=Sluiten + +session.status.considering=Volgende stappen overwegen… +session.status.thinking=Nadenken… +session.status.writing=Antwoord schrijven… +session.status.delegating=Werk delegeren… +session.status.planning=Plannen… +session.status.gathering=Context verzamelen… +session.status.searching.codebase=Codebase doorzoeken… +session.status.searching.web=Web doorzoeken… +session.status.editing=Bewerkingen uitvoeren… +session.status.commands=Opdrachten uitvoeren… + +session.part.reasoning=Redenering +session.part.compaction=context gecomprimeerd +session.part.tool.copy=Kopiëren +session.part.tool.error=Fout +session.part.tool.pending=In afwachting +session.part.tool.read=Lezen +session.part.tool.running=Actief +session.part.tool.shell=Shell +session.part.tool.truncated=Uitvoer ingekort in voorvertoning. Volledige uitvoer blijft beschikbaar in sessiegegevens. + +session.error.prompt=Prompt mislukt +session.error.compact=Sessiecomprimering mislukt +session.error.unknown=Onbekende fout + +session.header.tokens=Tokens +session.header.tokens.description=Tokens gebruikt door de laatste assistent-reactie: invoer, uitvoer, cache-schrijfacties en cache-leesacties. +session.header.input=in {0} +session.header.output=uit {0} +session.header.cache.write=cache schrijven {0} +session.header.cache.read=cache lezen {0} +session.header.compact=Sessie comprimeren +session.header.compact.description=Sessie samenvatten om de contextgrootte te verkleinen +session.header.expand=Sessiestatistieken weergeven +session.header.collapse=Sessiestatistieken verbergen +session.header.todos.progress={0}/{1} taken voltooid +session.header.todos.done=Alle {0} taken voltooid +session.header.context.tooltip.percent={0} tokens ({1}% van de context) +session.header.context.tooltip.tokens={0} tokens +session.header.context.used={0} / {1} tokens gebruikt +session.header.context.reserved={0} gereserveerd voor uitvoer +session.header.context.available={0} beschikbaar + +prompt.placeholder=Typ een bericht… +prompt.placeholder.with.shortcuts=Typ een bericht… ({0} om te verzenden, {1} voor nieuwe regel) +prompt.placeholder.with.send=Typ een bericht… ({0} om te verzenden) +prompt.placeholder.with.newline=Typ een bericht… ({0} voor nieuwe regel) +prompt.button.send=Verzenden +prompt.button.stop=Stoppen +prompt.button.send.tooltip.stop=Druk op {0} om te stoppen +mode.picker.tooltip=Modus selecteren +mode.picker.deprecated=verouderd +model.picker.tooltip=Model selecteren +model.picker.recommended=Aanbevolen +model.picker.favorites=Favorieten +model.picker.search=Modellen zoeken +model.picker.no.matches=Geen overeenkomende modellen +model.picker.favorite.add=Toevoegen aan favorieten +model.picker.favorite.remove=Verwijderen uit favorieten +model.picker.free=Gratis +model.picker.reset=Model terugzetten naar standaard +reasoning.picker.tooltip=Redeerniveau selecteren + +history.tab.local=Lokaal +history.tab.cloud=Cloud +history.back=Terug +history.search.placeholder=Sessies zoeken +history.empty=Geen sessies +history.loading=Laden… +history.untitled=Naamloos +history.delete.text=Verwijderen +history.delete.confirm.title=Sessie verwijderen? +history.delete.confirm.message="{0}" verwijderen uit lokale geschiedenis? +history.delete.confirm.message.multiple={0} sessies verwijderen uit lokale geschiedenis? +history.rename.title=Sessie hernoemen +history.rename.prompt=Nieuwe sessienaam: +history.cloud.load.more=Meer laden +history.cloud.repo.only=Alleen deze repository +history.group.today=Vandaag +history.group.yesterday=Gisteren +history.group.week=Deze week +history.group.month=Deze maand +history.group.older=Ouder +history.time.moments=Zojuist +history.time.minutes={0} min geleden +history.time.hours={0} u geleden +history.time.days={0} d geleden +history.time.months={0} mnd geleden +history.time.years={0} jr geleden +history.error.local=Laden van lokale geschiedenis mislukt +history.error.cloud=Laden van cloudgeschiedenis mislukt +history.error.local.delete=Verwijderen van sessie mislukt +history.error.local.rename=Hernoemen van sessie mislukt +history.error.cloud.delete=Cloudsessies kunnen nog niet worden verwijderd + +action.Kilo.Settings.text=Instellingen +action.Kilo.Settings.description=Kilo Code-instellingen +action.Kilo.NewSession.text=Nieuwe sessie +action.Kilo.NewSession.description=Een nieuwe Kilo-sessie starten +action.Kilo.History.text=Geschiedenis +action.Kilo.History.description=Sessiegeschiedenis weergeven +action.Kilo.SendPrompt.text=Prompt verzenden +action.Kilo.SendPrompt.description=Huidige Kilo-prompt verzenden +action.Kilo.StopSession.text=Sessie stoppen +action.Kilo.StopSession.description=Huidige Kilo-sessie stoppen +action.Kilo.SettingsGroup.text=Instellingen +action.Kilo.SettingsGroup.description=Kilo Code-instellingen +action.Kilo.Restart.text=Kilo herstarten +action.Kilo.Restart.description=CLI-proces beëindigen en herstarten +action.Kilo.Reinstall.text=Kilo herinstalleren +action.Kilo.Reinstall.description=CLI-binair opnieuw uitpakken en herstarten +action.Kilo.Session.Open.text=Openen +action.Kilo.Session.Open.description=Geselecteerde sessie openen +action.Kilo.Session.Rename.text=Hernoemen +action.Kilo.Session.Rename.description=Geselecteerde sessie hernoemen +action.Kilo.Session.Delete.text=Verwijderen +action.Kilo.Session.Delete.description=Geselecteerde sessie(s) verwijderen +action.Kilo.History.ContextMenu.text=Geschiedenisacties diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties new file mode 100644 index 00000000000..e7631a21d2c --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_no.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Laster… +session.connection.error.app=Tilkoblingsfeil +session.connection.error.workspace=Feil ved lasting av arbeidsområde +session.connection.error.unknown=Ukjent feil +session.connection.retry=Prøv igjen +session.connection.warning.config=Konfigurasjonsadvarsler + +session.empty.welcome=Kilo Code er en AI-kodingsassistent. Be den om å bygge funksjoner, fikse feil eller forklare kodebasen din. +session.empty.loading=Laster… +session.empty.recent=NYLIGE +session.showHistory=Vis historikk +session.scroll.bottom=Rull til bunnen +session.tab.new=Ny økt +session.tab.untitled=Uten tittel + +session.permission.title=Tillatelsesforespørsel +session.permission.meta=Verktøy: {0} • Mønstre: {1} +session.permission.allow=Tillat +session.permission.deny=Avslå +session.question.dismiss=Lukk + +session.status.considering=Vurderer neste steg… +session.status.thinking=Tenker… +session.status.writing=Skriver svar… +session.status.delegating=Delegerer arbeid… +session.status.planning=Planlegger… +session.status.gathering=Samler kontekst… +session.status.searching.codebase=Søker i kodebasen… +session.status.searching.web=Søker på nettet… +session.status.editing=Gjør redigeringer… +session.status.commands=Kjører kommandoer… + +session.part.reasoning=Resonnering +session.part.compaction=kontekst komprimert +session.part.tool.copy=Kopier +session.part.tool.error=Feil +session.part.tool.pending=Venter +session.part.tool.read=Les +session.part.tool.running=Kjører +session.part.tool.shell=Shell +session.part.tool.truncated=Utdata avkortet i forhåndsvisning. Fullstendig utdata finnes fortsatt i øktdata. + +session.error.prompt=Forespørsel mislyktes +session.error.compact=Komprimer økt mislyktes +session.error.unknown=Ukjent feil + +session.header.tokens=Tokens +session.header.tokens.description=Tokens brukt av siste assistentsvar: inndata, utdata, cache-skrivninger og cache-lesninger. +session.header.input=inn {0} +session.header.output=ut {0} +session.header.cache.write=cache-skriving {0} +session.header.cache.read=cache-lesing {0} +session.header.compact=Komprimer økt +session.header.compact.description=Oppsummer økten for å redusere kontekststørrelsen +session.header.expand=Vis øktberegninger +session.header.collapse=Skjul øktberegninger +session.header.todos.progress={0}/{1} oppgaver fullført +session.header.todos.done=Alle {0} oppgaver fullført +session.header.context.tooltip.percent={0} tokens ({1}% av konteksten) +session.header.context.tooltip.tokens={0} tokens +session.header.context.used={0} / {1} tokens brukt +session.header.context.reserved={0} reservert for utdata +session.header.context.available={0} tilgjengelig + +prompt.placeholder=Skriv en melding… +prompt.placeholder.with.shortcuts=Skriv en melding… ({0} for å sende, {1} for ny linje) +prompt.placeholder.with.send=Skriv en melding… ({0} for å sende) +prompt.placeholder.with.newline=Skriv en melding… ({0} for ny linje) +prompt.button.send=Send +prompt.button.stop=Stopp +prompt.button.send.tooltip.stop=Trykk {0} for å stoppe +mode.picker.tooltip=Velg modus +mode.picker.deprecated=utdatert +model.picker.tooltip=Velg modell +model.picker.recommended=Anbefalt +model.picker.favorites=Favoritter +model.picker.search=Søk etter modeller +model.picker.no.matches=Ingen samsvarende modeller +model.picker.favorite.add=Legg til favoritter +model.picker.favorite.remove=Fjern fra favoritter +model.picker.free=Gratis +model.picker.reset=Tilbakestill modell til standard +reasoning.picker.tooltip=Velg resonnementsnivå + +history.tab.local=Lokal +history.tab.cloud=Sky +history.back=Tilbake +history.search.placeholder=Søk i økter +history.empty=Ingen økter +history.loading=Laster… +history.untitled=Uten tittel +history.delete.text=Slett +history.delete.confirm.title=Slette økt? +history.delete.confirm.message=Slette "{0}" fra lokal historikk? +history.delete.confirm.message.multiple=Slette {0} økter fra lokal historikk? +history.rename.title=Gi økten nytt navn +history.rename.prompt=Nytt øktnavn: +history.cloud.load.more=Last inn mer +history.cloud.repo.only=Bare dette depotet +history.group.today=I dag +history.group.yesterday=I går +history.group.week=Denne uken +history.group.month=Denne måneden +history.group.older=Eldre +history.time.moments=Akkurat nå +history.time.minutes={0} min siden +history.time.hours={0} t siden +history.time.days={0} d siden +history.time.months={0} mnd siden +history.time.years={0} år siden +history.error.local=Kunne ikke laste lokal historikk +history.error.cloud=Kunne ikke laste sky-historikk +history.error.local.delete=Kunne ikke slette økt +history.error.local.rename=Kunne ikke gi økten nytt navn +history.error.cloud.delete=Sky-økter kan ikke slettes ennå + +action.Kilo.Settings.text=Innstillinger +action.Kilo.Settings.description=Kilo Code-innstillinger +action.Kilo.NewSession.text=Ny økt +action.Kilo.NewSession.description=Start en ny Kilo-økt +action.Kilo.History.text=Historikk +action.Kilo.History.description=Vis økthistorikk +action.Kilo.SendPrompt.text=Send forespørsel +action.Kilo.SendPrompt.description=Send gjeldende Kilo-forespørsel +action.Kilo.StopSession.text=Stopp økt +action.Kilo.StopSession.description=Stopp gjeldende Kilo-økt +action.Kilo.SettingsGroup.text=Innstillinger +action.Kilo.SettingsGroup.description=Kilo Code-innstillinger +action.Kilo.Restart.text=Start Kilo på nytt +action.Kilo.Restart.description=Avslutt og start CLI-prosessen på nytt +action.Kilo.Reinstall.text=Installer Kilo på nytt +action.Kilo.Reinstall.description=Pakk ut CLI-binærfilen på nytt og start på nytt +action.Kilo.Session.Open.text=Åpne +action.Kilo.Session.Open.description=Åpne valgt økt +action.Kilo.Session.Rename.text=Gi nytt navn +action.Kilo.Session.Rename.description=Gi valgt økt nytt navn +action.Kilo.Session.Delete.text=Slett +action.Kilo.Session.Delete.description=Slett valgte økt(er) +action.Kilo.History.ContextMenu.text=Historikkhandlinger diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties new file mode 100644 index 00000000000..53ddeb68b15 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pl.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Ładowanie… +session.connection.error.app=Błąd połączenia +session.connection.error.workspace=Błąd ładowania obszaru roboczego +session.connection.error.unknown=Nieznany błąd +session.connection.retry=Spróbuj ponownie +session.connection.warning.config=Ostrzeżenia konfiguracji + +session.empty.welcome=Kilo Code to asystent kodowania AI. Poproś go o tworzenie funkcji, naprawianie błędów lub wyjaśnianie bazy kodu. +session.empty.loading=Ładowanie… +session.empty.recent=OSTATNIE +session.showHistory=Pokaż historię +session.scroll.bottom=Przewiń na dół +session.tab.new=Nowa sesja +session.tab.untitled=Sesja bez tytułu + +session.permission.title=Prośba o uprawnienia +session.permission.meta=Narzędzie: {0} • Wzorce: {1} +session.permission.allow=Zezwól +session.permission.deny=Odmów +session.question.dismiss=Odrzuć + +session.status.considering=Rozważanie następnych kroków… +session.status.thinking=Myślenie… +session.status.writing=Pisanie odpowiedzi… +session.status.delegating=Delegowanie pracy… +session.status.planning=Planowanie… +session.status.gathering=Zbieranie kontekstu… +session.status.searching.codebase=Przeszukiwanie bazy kodu… +session.status.searching.web=Przeszukiwanie sieci… +session.status.editing=Dokonywanie edycji… +session.status.commands=Uruchamianie poleceń… + +session.part.reasoning=Rozumowanie +session.part.compaction=kontekst skompresowany +session.part.tool.copy=Kopiuj +session.part.tool.error=Błąd +session.part.tool.pending=Oczekuje +session.part.tool.read=Odczyt +session.part.tool.running=Uruchomione +session.part.tool.shell=Powłoka +session.part.tool.truncated=Wyjście skrócone w podglądzie. Pełne wyjście pozostaje w danych sesji. + +session.error.prompt=Błąd monitu +session.error.compact=Błąd kompresji sesji +session.error.unknown=Nieznany błąd + +session.header.tokens=Tokeny +session.header.tokens.description=Tokeny użyte przez ostatnią odpowiedź asystenta: wejściowe, wyjściowe, zapisy do bufora i odczyty z bufora. +session.header.input=we {0} +session.header.output=wy {0} +session.header.cache.write=zapis bufora {0} +session.header.cache.read=odczyt bufora {0} +session.header.compact=Kompresuj sesję +session.header.compact.description=Podsumuj sesję, aby zmniejszyć rozmiar kontekstu +session.header.expand=Pokaż metryki sesji +session.header.collapse=Ukryj metryki sesji +session.header.todos.progress={0}/{1} zadań ukończono +session.header.todos.done=Wszystkie {0} zadań ukończono +session.header.context.tooltip.percent={0} tokenów ({1}% kontekstu) +session.header.context.tooltip.tokens={0} tokenów +session.header.context.used={0} / {1} tokenów użito +session.header.context.reserved={0} zarezerwowane dla wyjścia +session.header.context.available={0} dostępne + +prompt.placeholder=Wpisz wiadomość… +prompt.placeholder.with.shortcuts=Wpisz wiadomość… ({0} aby wyslać, {1} nowa linia) +prompt.placeholder.with.send=Wpisz wiadomość… ({0} aby wyslać) +prompt.placeholder.with.newline=Wpisz wiadomość… ({0} nowa linia) +prompt.button.send=Wyślij +prompt.button.stop=Zatrzymaj +prompt.button.send.tooltip.stop=Aby zatrzymać, naciśnij {0} +mode.picker.tooltip=Wybierz tryb +mode.picker.deprecated=przestarzały +model.picker.tooltip=Wybierz model +model.picker.recommended=Zalecane +model.picker.favorites=Ulubione +model.picker.search=Szukaj modeli +model.picker.no.matches=Brak pasujących modeli +model.picker.favorite.add=Dodaj do ulubionych +model.picker.favorite.remove=Usuń z ulubionych +model.picker.free=Bezpłatny +model.picker.reset=Zresetuj model do domyślnego +reasoning.picker.tooltip=Wybierz poziom rozumowania + +history.tab.local=Lokalne +history.tab.cloud=Chmura +history.back=Wstecz +history.search.placeholder=Szukaj sesji +history.empty=Brak sesji +history.loading=Ładowanie… +history.untitled=Bez tytułu +history.delete.text=Usuń +history.delete.confirm.title=Usunąć sesję? +history.delete.confirm.message=Usunąć "{0}" z lokalnej historii? +history.delete.confirm.message.multiple=Usunąć {0} sesji z lokalnej historii? +history.rename.title=Zmień nazwę sesji +history.rename.prompt=Nowa nazwa sesji: +history.cloud.load.more=Załaduj więcej +history.cloud.repo.only=Tylko to repozytorium +history.group.today=Dzisiaj +history.group.yesterday=Wczoraj +history.group.week=Ten tydzień +history.group.month=Ten miesiąc +history.group.older=Starsze +history.time.moments=Przed chwilą +history.time.minutes={0} min temu +history.time.hours={0} godz temu +history.time.days={0} dni temu +history.time.months={0} mies temu +history.time.years={0} lat temu +history.error.local=Nie udało się załadować lokalnej historii +history.error.cloud=Nie udało się załadować historii w chmurze +history.error.local.delete=Nie udało się usunąć sesji +history.error.local.rename=Nie udało się zmienić nazwy sesji +history.error.cloud.delete=Sesji w chmurze nie można jeszcze usuwać + +action.Kilo.Settings.text=Ustawienia +action.Kilo.Settings.description=Ustawienia Kilo Code +action.Kilo.NewSession.text=Nowa sesja +action.Kilo.NewSession.description=Rozpocznij nową sesję Kilo +action.Kilo.History.text=Historia +action.Kilo.History.description=Pokaż historię sesji +action.Kilo.SendPrompt.text=Wyślij monit +action.Kilo.SendPrompt.description=Wyślij bieżący monit Kilo +action.Kilo.StopSession.text=Zatrzymaj sesję +action.Kilo.StopSession.description=Zatrzymaj bieżącą sesję Kilo +action.Kilo.SettingsGroup.text=Ustawienia +action.Kilo.SettingsGroup.description=Ustawienia Kilo Code +action.Kilo.Restart.text=Uruchom ponownie Kilo +action.Kilo.Restart.description=Zakończ i uruchom ponownie proces CLI +action.Kilo.Reinstall.text=Zainstaluj ponownie Kilo +action.Kilo.Reinstall.description=Ponownie wyodrębnij plik binarny CLI i uruchom ponownie +action.Kilo.Session.Open.text=Otwórz +action.Kilo.Session.Open.description=Otwórz wybraną sesję +action.Kilo.Session.Rename.text=Zmień nazwę +action.Kilo.Session.Rename.description=Zmień nazwę wybranej sesji +action.Kilo.Session.Delete.text=Usuń +action.Kilo.Session.Delete.description=Usuń wybraną sesję (wybrane sesje) +action.Kilo.History.ContextMenu.text=Akcje historii diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties new file mode 100644 index 00000000000..27c7eb48436 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_pt_BR.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Carregando… +session.connection.error.app=Falha na conexão +session.connection.error.workspace=Falha ao carregar o espaço de trabalho +session.connection.error.unknown=Erro desconhecido +session.connection.retry=Tentar novamente +session.connection.warning.config=Avisos de configuração + +session.empty.welcome=Kilo Code é um assistente de codificação com IA. Peça que construa funcionalidades, corrija bugs ou explique sua base de código. +session.empty.loading=Carregando… +session.empty.recent=RECENTE +session.showHistory=Mostrar histórico +session.scroll.bottom=Rolar para o fim +session.tab.new=Nova sessão +session.tab.untitled=Sessão sem título + +session.permission.title=Solicitação de permissão +session.permission.meta=Ferramenta: {0} • Padrões: {1} +session.permission.allow=Permitir +session.permission.deny=Negar +session.question.dismiss=Dispensar + +session.status.considering=Considerando os próximos passos… +session.status.thinking=Pensando… +session.status.writing=Escrevendo resposta… +session.status.delegating=Delegando trabalho… +session.status.planning=Planejando… +session.status.gathering=Coletando contexto… +session.status.searching.codebase=Pesquisando na base de código… +session.status.searching.web=Pesquisando na web… +session.status.editing=Realizando edições… +session.status.commands=Executando comandos… + +session.part.reasoning=Raciocínio +session.part.compaction=contexto compactado +session.part.tool.copy=Copiar +session.part.tool.error=Erro +session.part.tool.pending=Pendente +session.part.tool.read=Ler +session.part.tool.running=Executando +session.part.tool.shell=Shell +session.part.tool.truncated=Saída truncada na pré-visualização. A saída completa permanece nos dados da sessão. + +session.error.prompt=Falha no prompt +session.error.compact=Falha ao compactar a sessão +session.error.unknown=Erro desconhecido + +session.header.tokens=Tokens +session.header.tokens.description=Tokens usados pela última resposta do assistente: entrada, saída, escritas em cache e leituras em cache. +session.header.input=entrada {0} +session.header.output=saída {0} +session.header.cache.write=escrita em cache {0} +session.header.cache.read=leitura em cache {0} +session.header.compact=Compactar sessão +session.header.compact.description=Resumir a sessão para reduzir o tamanho do contexto +session.header.expand=Exibir métricas da sessão +session.header.collapse=Ocultar métricas da sessão +session.header.todos.progress={0}/{1} tarefas concluídas +session.header.todos.done=Todas as {0} tarefas concluídas +session.header.context.tooltip.percent={0} tokens ({1}% do contexto) +session.header.context.tooltip.tokens={0} tokens +session.header.context.used={0} / {1} tokens utilizados +session.header.context.reserved={0} reservados para saída +session.header.context.available={0} disponíveis + +prompt.placeholder=Digite uma mensagem… +prompt.placeholder.with.shortcuts=Digite uma mensagem… ({0} para enviar, {1} para nova linha) +prompt.placeholder.with.send=Digite uma mensagem… ({0} para enviar) +prompt.placeholder.with.newline=Digite uma mensagem… ({0} para nova linha) +prompt.button.send=Enviar +prompt.button.stop=Parar +prompt.button.send.tooltip.stop=Para parar, pressione {0} +mode.picker.tooltip=Selecionar modo +mode.picker.deprecated=obsoleto +model.picker.tooltip=Selecionar modelo +model.picker.recommended=Recomendado +model.picker.favorites=Favoritos +model.picker.search=Pesquisar modelos +model.picker.no.matches=Nenhum modelo correspondente +model.picker.favorite.add=Adicionar aos favoritos +model.picker.favorite.remove=Remover dos favoritos +model.picker.free=Gratuito +model.picker.reset=Redefinir modelo para o padrão +reasoning.picker.tooltip=Selecionar esforço de raciocínio + +history.tab.local=Local +history.tab.cloud=Nuvem +history.back=Voltar +history.search.placeholder=Pesquisar sessões +history.empty=Nenhuma sessão +history.loading=Carregando… +history.untitled=Sem título +history.delete.text=Excluir +history.delete.confirm.title=Excluir sessão? +history.delete.confirm.message=Excluir "{0}" do histórico local? +history.delete.confirm.message.multiple=Excluir {0} sessões do histórico local? +history.rename.title=Renomear sessão +history.rename.prompt=Novo nome da sessão: +history.cloud.load.more=Carregar mais +history.cloud.repo.only=Somente este repositório +history.group.today=Hoje +history.group.yesterday=Ontem +history.group.week=Esta semana +history.group.month=Este mês +history.group.older=Mais antigo +history.time.moments=Agora mesmo +history.time.minutes=Há {0} min +history.time.hours=Há {0} h +history.time.days=Há {0} d +history.time.months=Há {0} mês(es) +history.time.years=Há {0} ano(s) +history.error.local=Falha ao carregar o histórico local +history.error.cloud=Falha ao carregar o histórico na nuvem +history.error.local.delete=Falha ao excluir a sessão +history.error.local.rename=Falha ao renomear a sessão +history.error.cloud.delete=As sessões na nuvem ainda não podem ser excluídas + +action.Kilo.Settings.text=Configurações +action.Kilo.Settings.description=Configurações do Kilo Code +action.Kilo.NewSession.text=Nova sessão +action.Kilo.NewSession.description=Iniciar uma nova sessão do Kilo +action.Kilo.History.text=Histórico +action.Kilo.History.description=Exibir histórico de sessões +action.Kilo.SendPrompt.text=Enviar prompt +action.Kilo.SendPrompt.description=Enviar o prompt atual do Kilo +action.Kilo.StopSession.text=Parar sessão +action.Kilo.StopSession.description=Parar a sessão atual do Kilo +action.Kilo.SettingsGroup.text=Configurações +action.Kilo.SettingsGroup.description=Configurações do Kilo Code +action.Kilo.Restart.text=Reiniciar Kilo +action.Kilo.Restart.description=Encerrar e reiniciar o processo CLI +action.Kilo.Reinstall.text=Reinstalar Kilo +action.Kilo.Reinstall.description=Reextrair o binário CLI e reiniciar +action.Kilo.Session.Open.text=Abrir +action.Kilo.Session.Open.description=Abrir a sessão selecionada +action.Kilo.Session.Rename.text=Renomear +action.Kilo.Session.Rename.description=Renomear a sessão selecionada +action.Kilo.Session.Delete.text=Excluir +action.Kilo.Session.Delete.description=Excluir a(s) sessão(es) selecionada(s) +action.Kilo.History.ContextMenu.text=Ações do histórico diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties new file mode 100644 index 00000000000..e6bdb4d1c62 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_ru.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Загрузка… +session.connection.error.app=Ошибка подключения +session.connection.error.workspace=Ошибка загрузки рабочей области +session.connection.error.unknown=Неизвестная ошибка +session.connection.retry=Повторить +session.connection.warning.config=Предупреждения конфигурации + +session.empty.welcome=Kilo Code — это AI-ассистент по программированию. Попросите его разработать функции, исправить ошибки или объяснить кодовую базу. +session.empty.loading=Загрузка… +session.empty.recent=НЕДАВНИЕ +session.showHistory=Показать историю +session.scroll.bottom=Прокрутить вниз +session.tab.new=Новая сессия +session.tab.untitled=Незаголовок сессия + +session.permission.title=Запрос разрешения +session.permission.meta=Инструмент: {0} • Шаблоны: {1} +session.permission.allow=Разрешить +session.permission.deny=Отказать +session.question.dismiss=Отклонить + +session.status.considering=Обдумываю следующие шаги… +session.status.thinking=Размышляю… +session.status.writing=Пишу ответ… +session.status.delegating=Передаю задание… +session.status.planning=Планирую… +session.status.gathering=Сбор контекста… +session.status.searching.codebase=Поиск в кодовой базе… +session.status.searching.web=Поиск в интернете… +session.status.editing=Вношу изменения… +session.status.commands=Выполняю команды… + +session.part.reasoning=Рассуждение +session.part.compaction=контекст сжат +session.part.tool.copy=Копировать +session.part.tool.error=Ошибка +session.part.tool.pending=Ожидание +session.part.tool.read=Чтение +session.part.tool.running=Выполняется +session.part.tool.shell=Shell +session.part.tool.truncated=Вывод усечён в предпросмотре. Полный вывод сохраняется в данных сессии. + +session.error.prompt=Ошибка запроса +session.error.compact=Ошибка сжатия сессии +session.error.unknown=Неизвестная ошибка + +session.header.tokens=Токены +session.header.tokens.description=Токены, использованные последним ответом ассистента: входные, исходные, запись в кэш и чтение из кэша. +session.header.input=вход {0} +session.header.output=выход {0} +session.header.cache.write=запись в кэш {0} +session.header.cache.read=чтение из кэша {0} +session.header.compact=Сжать сессию +session.header.compact.description=Суммировать сессию для уменьшения размера контекста +session.header.expand=Показать метрики сессии +session.header.collapse=Скрыть метрики сессии +session.header.todos.progress={0}/{1} задач выполнено +session.header.todos.done=Все {0} задач выполнены +session.header.context.tooltip.percent={0} токенов ({1}% контекста) +session.header.context.tooltip.tokens={0} токенов +session.header.context.used={0} / {1} токенов использовано +session.header.context.reserved={0} зарезервировано для вывода +session.header.context.available={0} доступно + +prompt.placeholder=Введите сообщение… +prompt.placeholder.with.shortcuts=Введите сообщение… ({0} — отправить, {1} — новая строка) +prompt.placeholder.with.send=Введите сообщение… ({0} — отправить) +prompt.placeholder.with.newline=Введите сообщение… ({0} — новая строка) +prompt.button.send=Отправить +prompt.button.stop=Остановить +prompt.button.send.tooltip.stop=Для остановки нажмите {0} +mode.picker.tooltip=Выбрать режим +mode.picker.deprecated=устаревший +model.picker.tooltip=Выбрать модель +model.picker.recommended=Рекомендуемые +model.picker.favorites=Избранное +model.picker.search=Поиск моделей +model.picker.no.matches=Нет подходящих моделей +model.picker.favorite.add=Добавить в избранное +model.picker.favorite.remove=Удалить из избранного +model.picker.free=Бесплатно +model.picker.reset=Сбросить модель на по умолчанию +reasoning.picker.tooltip=Выбрать уровень рассуждения + +history.tab.local=Локальная +history.tab.cloud=Облако +history.back=Назад +history.search.placeholder=Поиск сессий +history.empty=Нет сессий +history.loading=Загрузка… +history.untitled=Без названия +history.delete.text=Удалить +history.delete.confirm.title=Удалить сессию? +history.delete.confirm.message=Удалить «{0}» из локальной истории? +history.delete.confirm.message.multiple=Удалить {0} сессий из локальной истории? +history.rename.title=Переименовать сессию +history.rename.prompt=Новое имя сессии: +history.cloud.load.more=Загрузить ещё +history.cloud.repo.only=Только этот репозиторий +history.group.today=Сегодня +history.group.yesterday=Вчера +history.group.week=На этой неделе +history.group.month=В этом месяце +history.group.older=Старше +history.time.moments=Только что +history.time.minutes={0} мин назад +history.time.hours={0} ч назад +history.time.days={0} д назад +history.time.months={0} мес назад +history.time.years={0} л назад +history.error.local=Ошибка загрузки локальной истории +history.error.cloud=Ошибка загрузки облачной истории +history.error.local.delete=Ошибка удаления сессии +history.error.local.rename=Ошибка переименования сессии +history.error.cloud.delete=Облачные сессии пока нельзя удалить + +action.Kilo.Settings.text=Настройки +action.Kilo.Settings.description=Настройки Kilo Code +action.Kilo.NewSession.text=Новая сессия +action.Kilo.NewSession.description=Запустить новую сессию Kilo +action.Kilo.History.text=История +action.Kilo.History.description=Показать историю сессий +action.Kilo.SendPrompt.text=Отправить запрос +action.Kilo.SendPrompt.description=Отправить текущий запрос Kilo +action.Kilo.StopSession.text=Остановить сессию +action.Kilo.StopSession.description=Остановить текущую сессию Kilo +action.Kilo.SettingsGroup.text=Настройки +action.Kilo.SettingsGroup.description=Настройки Kilo Code +action.Kilo.Restart.text=Перезапустить Kilo +action.Kilo.Restart.description=Завершить и перезапустить CLI-процесс +action.Kilo.Reinstall.text=Переустановить Kilo +action.Kilo.Reinstall.description=Переизвлечь CLI-бинарник и перезапустить +action.Kilo.Session.Open.text=Открыть +action.Kilo.Session.Open.description=Открыть выбранную сессию +action.Kilo.Session.Rename.text=Переименовать +action.Kilo.Session.Rename.description=Переименовать выбранную сессию +action.Kilo.Session.Delete.text=Удалить +action.Kilo.Session.Delete.description=Удалить выбранную(-ые) сессию(-ии) +action.Kilo.History.ContextMenu.text=Действия с историей diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties new file mode 100644 index 00000000000..049b1e1d1ab --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_th.properties @@ -0,0 +1,139 @@ +session.connection.connecting=กำลังโหลด… +session.connection.error.app=การเชื่อมต่อล้มเหลว +session.connection.error.workspace=โหลดพื้นที่ทำงานล้มเหลว +session.connection.error.unknown=ข้อผิดพลาดที่ไม่ทราบ +session.connection.retry=ลองอีกครั้ง +session.connection.warning.config=คำเตือนการกำหนดค่า + +session.empty.welcome=Kilo Code คือผู้ช่วยเขียนโค้ด AI ขอให้สร้างฟีเจอร์แก้สันข้อผิดพลาด หรืออธิบายโค้ดเบสของคุณ +session.empty.loading=กำลังโหลด… +session.empty.recent=ล่าสุด +session.showHistory=แสดงประวัติ +session.scroll.bottom=เลื่อนไปด้านล่าง +session.tab.new=เซสชันใหม่ +session.tab.untitled=เซสชันไม่มีชื่อ + +session.permission.title=ขอสิทธิ์ +session.permission.meta=เครื่องมือ: {0} • รูปแบบ: {1} +session.permission.allow=อนุญาต +session.permission.deny=ปฏิเสธ +session.question.dismiss=ปิด + +session.status.considering=กำลังพิจารณาขั้นตอนถัดไป… +session.status.thinking=กำลังคิด… +session.status.writing=กำลังเขียนคำตอบ… +session.status.delegating=กำลังมอบหมายงาน… +session.status.planning=กำลังวางแผน… +session.status.gathering=กำลังรวบรวมบริบท… +session.status.searching.codebase=กำลังค้นหาในโค้ดเบส… +session.status.searching.web=กำลังค้นหาบนเว็บ… +session.status.editing=กำลังแก้ไข… +session.status.commands=กำลังเรียกใช้คำสั่ง… + +session.part.reasoning=การใช้เหตุผล +session.part.compaction=บริบทถูกบีบอัดแล้ว +session.part.tool.copy=คัดลอก +session.part.tool.error=ข้อผิดพลาด +session.part.tool.pending=รอดำเนินการ +session.part.tool.read=อ่าน +session.part.tool.running=กำลังทำงาน +session.part.tool.shell=Shell +session.part.tool.truncated=ผลลัพธ์ถูกตัดทอนในส่วนตัวอย่าง ผลลัพธ์ทั้งหมดยังคงอยู่ในข้อมูลเซสชัน + +session.error.prompt=คำขอล้มเหลว +session.error.compact=การบีบอัดเซสชันล้มเหลว +session.error.unknown=ข้อผิดพลาดที่ไม่ทราบ + +session.header.tokens=โทเคน +session.header.tokens.description=โทเคนที่ใช้โดยคำตอบล่าสุดของผู้ช่วย: อินพุต เอาต์พุต การเขียนแคช และการอ่านแคช +session.header.input=อินพุต {0} +session.header.output=เอาต์พุต {0} +session.header.cache.write=เขียนแคช {0} +session.header.cache.read=อ่านแคช {0} +session.header.compact=บีบอัดเซสชัน +session.header.compact.description=สรุปเซสชันเพื่อลดขนาดบริบท +session.header.expand=แสดงเมตริกส์เซสชัน +session.header.collapse=ซ่อนเมตริกส์เซสชัน +session.header.todos.progress={0}/{1} งานเสร็จสิ้นแล้ว +session.header.todos.done=ครบทุก {0} งานเสร็จสิ้นแล้ว +session.header.context.tooltip.percent={0} โทเคน ({1}% ของบริบท) +session.header.context.tooltip.tokens={0} โทเคน +session.header.context.used=ใช้แล้ว {0} / {1} โทเคน +session.header.context.reserved={0} สำรองไว้สำหรับเอาต์พุต +session.header.context.available={0} พร้อมใช้งาน + +prompt.placeholder=พิมพ์ข้อความ… +prompt.placeholder.with.shortcuts=พิมพ์ข้อความ… ({0} เพื่อส่ง {1} สำหรับบรรทัดใหม่) +prompt.placeholder.with.send=พิมพ์ข้อความ… ({0} เพื่อส่ง) +prompt.placeholder.with.newline=พิมพ์ข้อความ… ({0} สำหรับบรรทัดใหม่) +prompt.button.send=ส่ง +prompt.button.stop=หยุด +prompt.button.send.tooltip.stop=กด {0} เพื่อหยุด +mode.picker.tooltip=เลือกโหมด +mode.picker.deprecated=ล้าสมัย +model.picker.tooltip=เลือกโมเดล +model.picker.recommended=แนะนำ +model.picker.favorites=รายการโปรด +model.picker.search=ค้นหาโมเดล +model.picker.no.matches=ไม่พบโมเดลที่ตรงกัน +model.picker.favorite.add=เพิ่มในรายการโปรด +model.picker.favorite.remove=นำออกจากรายการโปรด +model.picker.free=ฟรี +model.picker.reset=รีเซ็ตโมเดลเป็นค่าเริ่มต้น +reasoning.picker.tooltip=เลือกระดับการใช้เหตุผล + +history.tab.local=เครื่องนี้ +history.tab.cloud=คลาวด์ +history.back=ย้อนกลับ +history.search.placeholder=ค้นหาเซสชัน +history.empty=ไม่มีเซสชัน +history.loading=กำลังโหลด… +history.untitled=ไม่มีชื่อ +history.delete.text=ลบ +history.delete.confirm.title=ลบเซสชัน? +history.delete.confirm.message=ลบ "{0}" ออกจากประวัติในเครื่อง? +history.delete.confirm.message.multiple=ลบ {0} เซสชันออกจากประวัติในเครื่อง? +history.rename.title=เปลี่ยนชื่อเซสชัน +history.rename.prompt=ชื่อเซสชันใหม่: +history.cloud.load.more=โหลดเพิ่มเติม +history.cloud.repo.only=เฉพาะรีโปซิทอรีนี้ +history.group.today=วันนี้ +history.group.yesterday=เมื่อวาน +history.group.week=สัปดาห์นี้ +history.group.month=เดือนนี้ +history.group.older=เก่ากว่า +history.time.moments=เมื่อกี้นี้ +history.time.minutes={0} นาทีที่แล้ว +history.time.hours={0} ชั่วโมงที่แล้ว +history.time.days={0} วันที่แล้ว +history.time.months={0} เดือนที่แล้ว +history.time.years={0} ปีที่แล้ว +history.error.local=ไม่สามารถโหลดประวัติในเครื่อง +history.error.cloud=ไม่สามารถโหลดประวัติคลาวด์ +history.error.local.delete=ไม่สามารถลบเซสชัน +history.error.local.rename=ไม่สามารถเปลี่ยนชื่อเซสชัน +history.error.cloud.delete=ยังไม่สามารถลบเซสชันคลาวด์ได้ + +action.Kilo.Settings.text=การตั้งค่า +action.Kilo.Settings.description=การตั้งค่า Kilo Code +action.Kilo.NewSession.text=เซสชันใหม่ +action.Kilo.NewSession.description=เริ่มเซสชัน Kilo ใหม่ +action.Kilo.History.text=ประวัติ +action.Kilo.History.description=แสดงประวัติเซสชัน +action.Kilo.SendPrompt.text=ส่งคำขอ +action.Kilo.SendPrompt.description=ส่งคำขอ Kilo ปัจจุบัน +action.Kilo.StopSession.text=หยุดเซสชัน +action.Kilo.StopSession.description=หยุดเซสชัน Kilo ปัจจุบัน +action.Kilo.SettingsGroup.text=การตั้งค่า +action.Kilo.SettingsGroup.description=การตั้งค่า Kilo Code +action.Kilo.Restart.text=รีสตาร์ต Kilo +action.Kilo.Restart.description=สิ้นสุดและรีสตาร์ตกระบวนการ CLI +action.Kilo.Reinstall.text=ติดตั้ง Kilo ใหม่ +action.Kilo.Reinstall.description=แตกไฟล์ไบนารี CLI อีกครั้งและรีสตาร์ต +action.Kilo.Session.Open.text=เปิด +action.Kilo.Session.Open.description=เปิดเซสชันที่เลือก +action.Kilo.Session.Rename.text=เปลี่ยนชื่อ +action.Kilo.Session.Rename.description=เปลี่ยนชื่อเซสชันที่เลือก +action.Kilo.Session.Delete.text=ลบ +action.Kilo.Session.Delete.description=ลบเซสชันที่เลือก +action.Kilo.History.ContextMenu.text=การดำเนินการประวัติ diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties new file mode 100644 index 00000000000..096fcb1ee94 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_tr.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Yükleniyor… +session.connection.error.app=Bağlantı hatası +session.connection.error.workspace=Çalışma alanı yüklenemedi +session.connection.error.unknown=Bilinmeyen hata +session.connection.retry=Tekrar dene +session.connection.warning.config=Yapılandırma uyarıları + +session.empty.welcome=Kilo Code, bir yapay zeka kodlama asistanıdır. Özellik oluşturmasını, hata düzeltirlmesi veya kod tabanınızı açıklamasını isteyin. +session.empty.loading=Yükleniyor… +session.empty.recent=SON +session.showHistory=Geçmişi göster +session.scroll.bottom=En alta kaýr +session.tab.new=Yeni oturum +session.tab.untitled=Başlıksız oturum + +session.permission.title=İzin isteği +session.permission.meta=Araç: {0} • Desenler: {1} +session.permission.allow=İzin ver +session.permission.deny=Reddet +session.question.dismiss=Kapat + +session.status.considering=Sonraki adımlar düşünülüyor… +session.status.thinking=Düşünülüyor… +session.status.writing=Yanıt yazılıyor… +session.status.delegating=İş devrediliyor… +session.status.planning=Plan hazırlanıyor… +session.status.gathering=Bağlam toplanıyor… +session.status.searching.codebase=Kod tabanı aranıyor… +session.status.searching.web=Web aranıyor… +session.status.editing=Düzenleme yapılıyor… +session.status.commands=Komutlar çalıştırılıyor… + +session.part.reasoning=Muhakeme +session.part.compaction=bağlam sıkıştırıldı +session.part.tool.copy=Kopyala +session.part.tool.error=Hata +session.part.tool.pending=Bekliyor +session.part.tool.read=Oku +session.part.tool.running=Çalışıyor +session.part.tool.shell=Kabuk +session.part.tool.truncated=Önizlemede çıktı kısaltıldı. Tam çıktı oturum verilerinde kalıyor. + +session.error.prompt=İstem hatası +session.error.compact=Oturum sıkıştırma hatası +session.error.unknown=Bilinmeyen hata + +session.header.tokens=Jeton +session.header.tokens.description=Son asistan yanıtında kullanılan jetonlar: giriş, çıkış, önbelleğe yazma ve önbellekten okuma. +session.header.input=giriş {0} +session.header.output=çıkış {0} +session.header.cache.write=önbelleğe yazma {0} +session.header.cache.read=önbellekten okuma {0} +session.header.compact=Oturumu sıkıştır +session.header.compact.description=Bağlam boyutunu azaltmak için oturumu özetle +session.header.expand=Oturum metriklerini göster +session.header.collapse=Oturum metriklerini gizle +session.header.todos.progress={0}/{1} görev tamamlandı +session.header.todos.done=Tüm {0} görev tamamlandı +session.header.context.tooltip.percent={0} jeton (bağlamın %{1}’i) +session.header.context.tooltip.tokens={0} jeton +session.header.context.used={0} / {1} jeton kullanıldı +session.header.context.reserved={0} çıkış için ayrıldı +session.header.context.available={0} kullanılabilir + +prompt.placeholder=Bir mesaj yazın… +prompt.placeholder.with.shortcuts=Bir mesaj yazın… ({0} göndermek için, {1} yeni satır için) +prompt.placeholder.with.send=Bir mesaj yazın… ({0} göndermek için) +prompt.placeholder.with.newline=Bir mesaj yazın… ({0} yeni satır için) +prompt.button.send=Gönder +prompt.button.stop=Durdur +prompt.button.send.tooltip.stop=Durdurmak için {0} basın +mode.picker.tooltip=Mod seç +mode.picker.deprecated=geçersiz +model.picker.tooltip=Model seç +model.picker.recommended=Önerilen +model.picker.favorites=Favoriler +model.picker.search=Model ara +model.picker.no.matches=Eşleşen model yok +model.picker.favorite.add=Favorilere ekle +model.picker.favorite.remove=Favorilerden çıkar +model.picker.free=Ücretsiz +model.picker.reset=Modeli varsayılana sıfırla +reasoning.picker.tooltip=Muhakeme çabası seç + +history.tab.local=Yerel +history.tab.cloud=Bulut +history.back=Geri +history.search.placeholder=Oturum ara +history.empty=Oturum yok +history.loading=Yükleniyor… +history.untitled=Başlıksız +history.delete.text=Sil +history.delete.confirm.title=Oturum silinsin mi? +history.delete.confirm.message="{0}" yerel geçmişten silinsin mi? +history.delete.confirm.message.multiple={0} oturum yerel geçmişten silinsin mi? +history.rename.title=Oturumu yeniden adlandır +history.rename.prompt=Yeni oturum adı: +history.cloud.load.more=Daha fazla yükle +history.cloud.repo.only=Yalnızca bu depo +history.group.today=Bugün +history.group.yesterday=Dün +history.group.week=Bu hafta +history.group.month=Bu ay +history.group.older=Daha eski +history.time.moments=Az önce +history.time.minutes={0} dk önce +history.time.hours={0} sa önce +history.time.days={0} g önce +history.time.months={0} ay önce +history.time.years={0} yıl önce +history.error.local=Yerel geçmiş yüklenemedi +history.error.cloud=Bulut geçmişi yüklenemedi +history.error.local.delete=Oturum silinemedi +history.error.local.rename=Oturum yeniden adlandırılamadı +history.error.cloud.delete=Bulut oturumları henüz silinemiyor + +action.Kilo.Settings.text=Ayarlar +action.Kilo.Settings.description=Kilo Code ayarları +action.Kilo.NewSession.text=Yeni oturum +action.Kilo.NewSession.description=Yeni bir Kilo oturumu başlat +action.Kilo.History.text=Geçmiş +action.Kilo.History.description=Oturum geçmişini göster +action.Kilo.SendPrompt.text=İstem gönder +action.Kilo.SendPrompt.description=Geçerli Kilo istemini gönder +action.Kilo.StopSession.text=Oturumu durdur +action.Kilo.StopSession.description=Geçerli Kilo oturumunu durdur +action.Kilo.SettingsGroup.text=Ayarlar +action.Kilo.SettingsGroup.description=Kilo Code ayarları +action.Kilo.Restart.text=Kilo’yu yeniden başlat +action.Kilo.Restart.description=CLI sürecini sonlandır ve yeniden başlat +action.Kilo.Reinstall.text=Kilo’yu yeniden yükle +action.Kilo.Reinstall.description=CLI ikili dosyasını yeniden çıkar ve yeniden başlat +action.Kilo.Session.Open.text=Aç +action.Kilo.Session.Open.description=Seçilen oturumu aç +action.Kilo.Session.Rename.text=Yeniden adlandır +action.Kilo.Session.Rename.description=Seçilen oturumu yeniden adlandır +action.Kilo.Session.Delete.text=Sil +action.Kilo.Session.Delete.description=Seçilen oturumları sil +action.Kilo.History.ContextMenu.text=Geçmiş eylemleri diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties new file mode 100644 index 00000000000..73d770b3c05 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_uk.properties @@ -0,0 +1,139 @@ +session.connection.connecting=Завантаження… +session.connection.error.app=Помилка з'єднання +session.connection.error.workspace=Помилка завантаження робочого простору +session.connection.error.unknown=Невідома помилка +session.connection.retry=Спробувати знову +session.connection.warning.config=Попередження конфігурації + +session.empty.welcome=Kilo Code — це AI-асистент для програмування. Попросіть його створити функції, виправити помилки або пояснити ваш код. +session.empty.loading=Завантаження… +session.empty.recent=НЕДАВНІ +session.showHistory=Показати історію +session.scroll.bottom=Прокрутити донизу +session.tab.new=Нова сесія +session.tab.untitled=Сесія без назви + +session.permission.title=Запит дозволу +session.permission.meta=Інструмент: {0} • Шаблони: {1} +session.permission.allow=Дозволити +session.permission.deny=Заборонити +session.question.dismiss=Закрити + +session.status.considering=Обмірковую наступні кроки… +session.status.thinking=Міркую… +session.status.writing=Пишу відповідь… +session.status.delegating=Делегую роботу… +session.status.planning=Планую… +session.status.gathering=Збираю контекст… +session.status.searching.codebase=Шукаю в кодовій базі… +session.status.searching.web=Шукаю в інтернеті… +session.status.editing=Вношу зміни… +session.status.commands=Виконую команди… + +session.part.reasoning=Міркування +session.part.compaction=контекст стиснуто +session.part.tool.copy=Копіювати +session.part.tool.error=Помилка +session.part.tool.pending=Очікується +session.part.tool.read=Читання +session.part.tool.running=Виконується +session.part.tool.shell=Shell +session.part.tool.truncated=Вивід у попередньому перегляді усічено. Повний вивід зберігається в даних сесії. + +session.error.prompt=Помилка запиту +session.error.compact=Помилка стиснення сесії +session.error.unknown=Невідома помилка + +session.header.tokens=Токени +session.header.tokens.description=Токени, використані останній відповід䍎ю асистента: вхід, вихід, запис у кеш і читання з кешу. +session.header.input=вхід {0} +session.header.output=вихід {0} +session.header.cache.write=запис у кеш {0} +session.header.cache.read=читання з кешу {0} +session.header.compact=Стиснути сесію +session.header.compact.description=Стиснути сесію, щоб зменшити розмір контексту +session.header.expand=Показати метрики сесії +session.header.collapse=Приховати метрики сесії +session.header.todos.progress={0}/{1} завдань виконано +session.header.todos.done=Усі {0} завдань виконано +session.header.context.tooltip.percent={0} токенів ({1}% контексту) +session.header.context.tooltip.tokens={0} токенів +session.header.context.used={0} / {1} токенів використано +session.header.context.reserved={0} зарезервовано для виходу +session.header.context.available={0} доступно + +prompt.placeholder=Введіть повідомлення… +prompt.placeholder.with.shortcuts=Введіть повідомлення… ({0} — надіслати, {1} — новий рядок) +prompt.placeholder.with.send=Введіть повідомлення… ({0} — надіслати) +prompt.placeholder.with.newline=Введіть повідомлення… ({0} — новий рядок) +prompt.button.send=Надіслати +prompt.button.stop=Зупинити +prompt.button.send.tooltip.stop=Щоб зупинити, натисніть {0} +mode.picker.tooltip=Обрати режим +mode.picker.deprecated=застарілий +model.picker.tooltip=Обрати модель +model.picker.recommended=Рекомендовані +model.picker.favorites=Улюблені +model.picker.search=Пошук моделей +model.picker.no.matches=Немає відповідних моделей +model.picker.favorite.add=Додати до улюблених +model.picker.favorite.remove=Видалити з улюблених +model.picker.free=Безкоштовно +model.picker.reset=Скинути модель до стандартної +reasoning.picker.tooltip=Обрати рівень міркування + +history.tab.local=Локальна +history.tab.cloud=Хмара +history.back=Назад +history.search.placeholder=Пошук сесій +history.empty=Немає сесій +history.loading=Завантаження… +history.untitled=Без назви +history.delete.text=Видалити +history.delete.confirm.title=Видалити сесію? +history.delete.confirm.message=Видалити «{0}» з локальної історії? +history.delete.confirm.message.multiple=Видалити {0} сесій з локальної історії? +history.rename.title=Перейменувати сесію +history.rename.prompt=Нова назва сесії: +history.cloud.load.more=Завантажити більше +history.cloud.repo.only=Тільки цей репозиторій +history.group.today=Сьогодні +history.group.yesterday=Вчора +history.group.week=Цього тижня +history.group.month=Цього місяця +history.group.older=Старіше +history.time.moments=Щойно +history.time.minutes={0} хв тому +history.time.hours={0} год тому +history.time.days={0} дн тому +history.time.months={0} міс тому +history.time.years={0} р тому +history.error.local=Помилка завантаження локальної історії +history.error.cloud=Помилка завантаження хмарної історії +history.error.local.delete=Помилка видалення сесії +history.error.local.rename=Помилка перейменування сесії +history.error.cloud.delete=Хмарні сесії поки не можна видалити + +action.Kilo.Settings.text=Налаштування +action.Kilo.Settings.description=Налаштування Kilo Code +action.Kilo.NewSession.text=Нова сесія +action.Kilo.NewSession.description=Розпочати нову сесію Kilo +action.Kilo.History.text=Історія +action.Kilo.History.description=Показати історію сесій +action.Kilo.SendPrompt.text=Надіслати запит +action.Kilo.SendPrompt.description=Надіслати поточний запит Kilo +action.Kilo.StopSession.text=Зупинити сесію +action.Kilo.StopSession.description=Зупинити поточну сесію Kilo +action.Kilo.SettingsGroup.text=Налаштування +action.Kilo.SettingsGroup.description=Налаштування Kilo Code +action.Kilo.Restart.text=Перезапустити Kilo +action.Kilo.Restart.description=Завершити і перезапустити CLI-процес +action.Kilo.Reinstall.text=Перевстановити Kilo +action.Kilo.Reinstall.description=Перерозпакувати CLI-бінарний файл і перезапустити +action.Kilo.Session.Open.text=Відкрити +action.Kilo.Session.Open.description=Відкрити вибрану сесію +action.Kilo.Session.Rename.text=Перейменувати +action.Kilo.Session.Rename.description=Перейменувати вибрану сесію +action.Kilo.Session.Delete.text=Видалити +action.Kilo.Session.Delete.description=Видалити вибрану(-і) сесію(-ї) +action.Kilo.History.ContextMenu.text=Дії з історією diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties new file mode 100644 index 00000000000..2288060553f --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_CN.properties @@ -0,0 +1,139 @@ +session.connection.connecting=加载中… +session.connection.error.app=连接失败 +session.connection.error.workspace=工作区加载失败 +session.connection.error.unknown=未知错误 +session.connection.retry=重试 +session.connection.warning.config=配置警告 + +session.empty.welcome=Kilo Code 是一个 AI 编程助手。可请它构建功能、修复错误或解释您的代码库。 +session.empty.loading=加载中… +session.empty.recent=最近 +session.showHistory=显示历史 +session.scroll.bottom=滚动到底部 +session.tab.new=新建会话 +session.tab.untitled=无标题会话 + +session.permission.title=权限请求 +session.permission.meta=工具:{0} • 模式:{1} +session.permission.allow=允许 +session.permission.deny=拒绝 +session.question.dismiss=关闭 + +session.status.considering=正在考虑下一步… +session.status.thinking=思考中… +session.status.writing=正在编写回复… +session.status.delegating=正在委派工作… +session.status.planning=规划中… +session.status.gathering=收集上下文… +session.status.searching.codebase=搜索代码库… +session.status.searching.web=搜索网页… +session.status.editing=正在编辑… +session.status.commands=运行命令… + +session.part.reasoning=推理 +session.part.compaction=上下文已压缩 +session.part.tool.copy=复制 +session.part.tool.error=错误 +session.part.tool.pending=待处理 +session.part.tool.read=读取 +session.part.tool.running=运行中 +session.part.tool.shell=Shell +session.part.tool.truncated=预览中的输出已截断。完整输出仍保留在会话数据中。 + +session.error.prompt=提示失败 +session.error.compact=会话压缩失败 +session.error.unknown=未知错误 + +session.header.tokens=令牌 +session.header.tokens.description=最近一次助手回复中使用的令牌:输入、输出、缓存写入和缓存读取。 +session.header.input=输入 {0} +session.header.output=输出 {0} +session.header.cache.write=缓存写入 {0} +session.header.cache.read=缓存读取 {0} +session.header.compact=压缩会话 +session.header.compact.description=汇总会话以减少上下文大小 +session.header.expand=显示会话指标 +session.header.collapse=隐藏会话指标 +session.header.todos.progress={0}/{1} 个任务已完成 +session.header.todos.done=全部 {0} 个任务已完成 +session.header.context.tooltip.percent={0} 令牌(上下文的 {1}%) +session.header.context.tooltip.tokens={0} 令牌 +session.header.context.used=已使用 {0} / {1} 令牌 +session.header.context.reserved={0} 为输出预留 +session.header.context.available={0} 可用 + +prompt.placeholder=输入消息… +prompt.placeholder.with.shortcuts=输入消息…({0} 发送,{1} 换行) +prompt.placeholder.with.send=输入消息…({0} 发送) +prompt.placeholder.with.newline=输入消息…({0} 换行) +prompt.button.send=发送 +prompt.button.stop=停止 +prompt.button.send.tooltip.stop=按 {0} 停止 +mode.picker.tooltip=选择模式 +mode.picker.deprecated=已弃用 +model.picker.tooltip=选择模型 +model.picker.recommended=推荐 +model.picker.favorites=收藏夹 +model.picker.search=搜索模型 +model.picker.no.matches=无匹配模型 +model.picker.favorite.add=添加到收藏夹 +model.picker.favorite.remove=从收藏夹移除 +model.picker.free=免费 +model.picker.reset=重置模型为默认 +reasoning.picker.tooltip=选择推理力度 + +history.tab.local=本地 +history.tab.cloud=云端 +history.back=返回 +history.search.placeholder=搜索会话 +history.empty=暂无会话 +history.loading=加载中… +history.untitled=无标题 +history.delete.text=删除 +history.delete.confirm.title=删除会话? +history.delete.confirm.message=从本地历史中删除“{0}”? +history.delete.confirm.message.multiple=从本地历史中删除 {0} 个会话? +history.rename.title=重命名会话 +history.rename.prompt=新会话名称: +history.cloud.load.more=加载更多 +history.cloud.repo.only=仅此仓库 +history.group.today=今天 +history.group.yesterday=昨天 +history.group.week=本周 +history.group.month=本月 +history.group.older=更早以前 +history.time.moments=刚刚 +history.time.minutes={0} 分钟前 +history.time.hours={0} 小时前 +history.time.days={0} 天前 +history.time.months={0} 个月前 +history.time.years={0} 年前 +history.error.local=无法加载本地历史 +history.error.cloud=无法加载云端历史 +history.error.local.delete=删除会话失败 +history.error.local.rename=重命名会话失败 +history.error.cloud.delete=云端会话暂无法删除 + +action.Kilo.Settings.text=设置 +action.Kilo.Settings.description=Kilo Code 设置 +action.Kilo.NewSession.text=新建会话 +action.Kilo.NewSession.description=开始新的 Kilo 会话 +action.Kilo.History.text=历史 +action.Kilo.History.description=显示会话历史 +action.Kilo.SendPrompt.text=发送提示 +action.Kilo.SendPrompt.description=发送当前 Kilo 提示 +action.Kilo.StopSession.text=停止会话 +action.Kilo.StopSession.description=停止当前 Kilo 会话 +action.Kilo.SettingsGroup.text=设置 +action.Kilo.SettingsGroup.description=Kilo Code 设置 +action.Kilo.Restart.text=重启 Kilo +action.Kilo.Restart.description=终止并重启 CLI 进程 +action.Kilo.Reinstall.text=重新安装 Kilo +action.Kilo.Reinstall.description=重新解压 CLI 二进制文件并重启 +action.Kilo.Session.Open.text=打开 +action.Kilo.Session.Open.description=打开所选会话 +action.Kilo.Session.Rename.text=重命名 +action.Kilo.Session.Rename.description=重命名所选会话 +action.Kilo.Session.Delete.text=删除 +action.Kilo.Session.Delete.description=删除所选会话 +action.Kilo.History.ContextMenu.text=历史操作 diff --git a/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties new file mode 100644 index 00000000000..40fb78179b3 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/main/resources/messages/KiloBundle_zh_TW.properties @@ -0,0 +1,139 @@ +session.connection.connecting=載入中… +session.connection.error.app=連線失敗 +session.connection.error.workspace=工作區載入失敗 +session.connection.error.unknown=未知預設 +session.connection.retry=重試 +session.connection.warning.config=設定警告 + +session.empty.welcome=Kilo Code 是 AI 程式輔助。可請它建置功能、修復錯誤或解釋您的程式程式庫。 +session.empty.loading=載入中… +session.empty.recent=最近 +session.showHistory=顯示歷史 +session.scroll.bottom=滾動到底部 +session.tab.new=新建工作階段 +session.tab.untitled=未命名的工作階段 + +session.permission.title=權限請求 +session.permission.meta=工具:{0} • 樣式:{1} +session.permission.allow=允許 +session.permission.deny=拒絕 +session.question.dismiss=關閉 + +session.status.considering=考慮下一步… +session.status.thinking=思考中… +session.status.writing=正在撰寫回覆… +session.status.delegating=正在委派工作… +session.status.planning=計劃中… +session.status.gathering=收集上下文… +session.status.searching.codebase=搜尋程式程式庫… +session.status.searching.web=搜尋網頁… +session.status.editing=進行編輯… +session.status.commands=執行指令… + +session.part.reasoning=推理 +session.part.compaction=上下文已壓縮 +session.part.tool.copy=複製 +session.part.tool.error=錯誤 +session.part.tool.pending=待處理 +session.part.tool.read=讀取 +session.part.tool.running=執行中 +session.part.tool.shell=Shell +session.part.tool.truncated=預覽中的輸出已截斷。完整輸出仍保留在工作階段資料中。 + +session.error.prompt=提示失敗 +session.error.compact=工作階段壓縮失敗 +session.error.unknown=未知錯誤 + +session.header.tokens=記號 +session.header.tokens.description=最新助手回覆使用的記號:輸入、輸出、快取寫入和快取讀取。 +session.header.input=輸入 {0} +session.header.output=輸出 {0} +session.header.cache.write=快取寫入 {0} +session.header.cache.read=快取讀取 {0} +session.header.compact=壓縮工作階段 +session.header.compact.description=摘要工作階段以減少上下文大小 +session.header.expand=顯示工作階段度量 +session.header.collapse=隱藏工作階段度量 +session.header.todos.progress={0}/{1} 個待辦事項已完成 +session.header.todos.done=全部 {0} 個待辦事項已完成 +session.header.context.tooltip.percent={0} 記號(上下文的 {1}%) +session.header.context.tooltip.tokens={0} 記號 +session.header.context.used=已使用 {0} / {1} 記號 +session.header.context.reserved={0} 為輸出預留 +session.header.context.available={0} 可用 + +prompt.placeholder=輸入訊息… +prompt.placeholder.with.shortcuts=輸入訊息…({0} 發送,{1} 換行) +prompt.placeholder.with.send=輸入訊息…({0} 發送) +prompt.placeholder.with.newline=輸入訊息…({0} 換行) +prompt.button.send=發送 +prompt.button.stop=停止 +prompt.button.send.tooltip.stop=按 {0} 停止 +mode.picker.tooltip=選擇模式 +mode.picker.deprecated=已棄用 +model.picker.tooltip=選擇模型 +model.picker.recommended=推薦 +model.picker.favorites=收藏夾 +model.picker.search=搜尋模型 +model.picker.no.matches=無匹配模型 +model.picker.favorite.add=新增至收藏夾 +model.picker.favorite.remove=從收藏夾移除 +model.picker.free=免費 +model.picker.reset=重設模型為預設 +reasoning.picker.tooltip=選擇推理力度 + +history.tab.local=本機 +history.tab.cloud=雲端 +history.back=返回 +history.search.placeholder=搜尋工作階段 +history.empty=無工作階段 +history.loading=載入中… +history.untitled=未命名 +history.delete.text=刪除 +history.delete.confirm.title=刪除工作階段? +history.delete.confirm.message=從本機歷史中刪除「{0}」? +history.delete.confirm.message.multiple=從本機歷史中刪除 {0} 個工作階段? +history.rename.title=重命名工作階段 +history.rename.prompt=新工作階段名稱: +history.cloud.load.more=載入更多 +history.cloud.repo.only=僅此儲存庫 +history.group.today=今天 +history.group.yesterday=昨天 +history.group.week=本週 +history.group.month=本月 +history.group.older=更早以前 +history.time.moments=剛剛 +history.time.minutes={0} 分鐘前 +history.time.hours={0} 小時前 +history.time.days={0} 天前 +history.time.months={0} 個月前 +history.time.years={0} 年前 +history.error.local=無法載入本機歷史 +history.error.cloud=無法載入雲端歷史 +history.error.local.delete=刪除工作階段失敗 +history.error.local.rename=重命名工作階段失敗 +history.error.cloud.delete=雲端工作階段暫時無法刪除 + +action.Kilo.Settings.text=設定 +action.Kilo.Settings.description=Kilo Code 設定 +action.Kilo.NewSession.text=新建工作階段 +action.Kilo.NewSession.description=開始新的 Kilo 工作階段 +action.Kilo.History.text=歷史 +action.Kilo.History.description=顯示工作階段歷史 +action.Kilo.SendPrompt.text=發送提示 +action.Kilo.SendPrompt.description=發送目前的 Kilo 提示 +action.Kilo.StopSession.text=停止工作階段 +action.Kilo.StopSession.description=停止目前的 Kilo 工作階段 +action.Kilo.SettingsGroup.text=設定 +action.Kilo.SettingsGroup.description=Kilo Code 設定 +action.Kilo.Restart.text=重啟 Kilo +action.Kilo.Restart.description=終止並重啟 CLI 程序 +action.Kilo.Reinstall.text=重新安裝 Kilo +action.Kilo.Reinstall.description=重新解壓縮 CLI 二進位檔案並重啟 +action.Kilo.Session.Open.text=開啟 +action.Kilo.Session.Open.description=開啟所選工作階段 +action.Kilo.Session.Rename.text=重命名 +action.Kilo.Session.Rename.description=重命名所選工作階段 +action.Kilo.Session.Delete.text=刪除 +action.Kilo.Session.Delete.description=刪除所選工作階段 +action.Kilo.History.ContextMenu.text=歷史操作 diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt new file mode 100644 index 00000000000..9165e8d50ef --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/actions/HistorySessionActionsTest.kt @@ -0,0 +1,488 @@ +package ai.kilocode.client.actions + +import ai.kilocode.client.app.KiloSessionService +import ai.kilocode.client.app.KiloWorkspaceService +import ai.kilocode.client.app.Workspace +import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionManager +import ai.kilocode.client.session.SessionRef +import ai.kilocode.client.session.history.CloudHistoryItem +import ai.kilocode.client.session.history.HistoryController +import ai.kilocode.client.session.history.HistoryDataKeys +import ai.kilocode.client.session.history.HistorySelection +import ai.kilocode.client.session.history.HistorySource +import ai.kilocode.client.session.history.LocalHistoryItem +import ai.kilocode.client.testing.FakeSessionRpcApi +import ai.kilocode.client.testing.FakeWorkspaceRpcApi +import ai.kilocode.rpc.dto.CloudSessionDto +import ai.kilocode.rpc.dto.KiloWorkspaceStateDto +import ai.kilocode.rpc.dto.KiloWorkspaceStatusDto +import ai.kilocode.rpc.dto.SessionDto +import ai.kilocode.rpc.dto.SessionTimeDto +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.util.Disposer +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout + +@Suppress("UnstableApiUsage") +class HistorySessionActionsTest : BasePlatformTestCase() { + private lateinit var scope: CoroutineScope + private lateinit var rpc: FakeSessionRpcApi + private lateinit var sessions: KiloSessionService + private lateinit var workspace: Workspace + private lateinit var controller: HistoryController + private lateinit var manager: FakeManager + + override fun setUp() { + super.setUp() + scope = CoroutineScope(SupervisorJob()) + rpc = FakeSessionRpcApi() + sessions = KiloSessionService(project, scope, rpc) + val workspaces = KiloWorkspaceService(scope, FakeWorkspaceRpcApi().also { + it.state.value = KiloWorkspaceStateDto(status = KiloWorkspaceStatusDto.READY) + }) + workspace = workspaces.workspace("/test") + controller = HistoryController(sessions, workspace, scope) + manager = FakeManager() + } + + override fun tearDown() { + try { + scope.cancel() + } finally { + super.tearDown() + } + } + + // ------ OpenSessionAction.update ------ + + fun `test open action enabled for single local selection`() { + val action = OpenSessionAction() + val local = localItem("ses_1") + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(local)), controller) + + action.update(event) + + assertTrue(event.presentation.isEnabledAndVisible) + } + + fun `test open action enabled for single cloud selection`() { + val action = OpenSessionAction() + val item = cloudItem("cloud_1") + val event = event(action, manager, selection(HistorySource.CLOUD, emptyList(), listOf(item)), controller) + + action.update(event) + + assertTrue(event.presentation.isEnabledAndVisible) + } + + fun `test open action disabled without manager`() { + val action = OpenSessionAction() + val local = localItem("ses_1") + val event = event(action, null, selection(HistorySource.LOCAL, listOf(local)), controller) + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + fun `test open action disabled with no selection`() { + val action = OpenSessionAction() + val event = event(action, manager, selection(HistorySource.LOCAL, emptyList()), controller) + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + fun `test open action disabled with multiple local items`() { + val action = OpenSessionAction() + val items = listOf(localItem("ses_1"), localItem("ses_2")) + val event = event(action, manager, selection(HistorySource.LOCAL, items), controller) + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + // ------ OpenSessionAction.actionPerformed ------ + + fun `test open action performs opens local item`() { + val opened = mutableListOf() + val ctrl = HistoryController(sessions, workspace, scope, open = { ref -> + when (ref) { + is SessionRef.Local -> opened.add(ref.id) + is SessionRef.Cloud -> opened.add("cloud:${ref.id}") + } + }) + val local = localItem("ses_1") + val action = OpenSessionAction() + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(local)), ctrl) + + action.actionPerformed(event) + flush() + + assertEquals(listOf("ses_1"), opened) + } + + fun `test open action performs opens cloud item`() { + val opened = mutableListOf() + val ctrl = HistoryController(sessions, workspace, scope, open = { ref -> + when (ref) { + is SessionRef.Local -> opened.add(ref.id) + is SessionRef.Cloud -> opened.add("cloud:${ref.id}") + } + }) + val item = cloudItem("cloud_1") + val action = OpenSessionAction() + val event = event(action, manager, selection(HistorySource.CLOUD, emptyList(), listOf(item)), ctrl) + + action.actionPerformed(event) + flush() + + assertEquals(listOf("cloud:cloud_1"), opened) + } + + // ------ DeleteSessionAction.update ------ + + fun `test delete action enabled for non-empty local selection`() { + val action = DeleteSessionAction() + val local = localItem("ses_1") + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(local)), controller) + + action.update(event) + + assertTrue(event.presentation.isEnabledAndVisible) + } + + fun `test delete action disabled for cloud-only selection`() { + val action = DeleteSessionAction() + val item = cloudItem("cloud_1") + val event = event(action, manager, selection(HistorySource.CLOUD, emptyList(), listOf(item)), controller) + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + fun `test delete action disabled without manager`() { + val action = DeleteSessionAction() + val local = localItem("ses_1") + val event = event(action, null, selection(HistorySource.LOCAL, listOf(local)), controller) + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + fun `test delete action disabled with empty selection`() { + val action = DeleteSessionAction() + val event = event(action, manager, selection(HistorySource.LOCAL, emptyList()), controller) + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + // ------ DeleteSessionAction.actionPerformed ------ + + fun `test delete action deletes selected local items after confirmation`() { + rpc.listed += sessionDto("ses_1", "One") + rpc.listed += sessionDto("ses_2", "Two") + controller.reloadLocal() + flush() + + assertEquals(2, controller.local.items.size) + + val items = controller.local.items.toList() + val action = DeleteSessionAction().apply { confirm = { _, _ -> true } } + val event = event(action, manager, selection(HistorySource.LOCAL, items), controller) + + action.actionPerformed(event) + flush() + + assertEquals(listOf("ses_1", "ses_2"), rpc.deletes.map { it.first }) + assertTrue(controller.local.items.isEmpty()) + } + + fun `test delete action skips items already being deleted`() { + rpc.listed += sessionDto("ses_1", "One") + controller.reloadLocal() + flush() + + rpc.deleteGate = kotlinx.coroutines.CompletableDeferred() + val item = controller.local.items[0] + controller.delete(item) + waitFor { controller.deleting(item) } + + val action = DeleteSessionAction().apply { confirm = { _, _ -> true } } + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + + action.actionPerformed(event) + flush() + + assertTrue(rpc.deletes.isEmpty()) + + rpc.deleteGate?.complete(Unit) + waitFor { rpc.deletes.size == 1 } + assertEquals(listOf("ses_1"), rpc.deletes.map { it.first }) + } + + fun `test delete action cancelled when user says no`() { + rpc.listed += sessionDto("ses_1", "One") + controller.reloadLocal() + flush() + + val item = controller.local.items[0] + val action = DeleteSessionAction().apply { confirm = { _, _ -> false } } + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + + action.actionPerformed(event) + flush() + + assertTrue(rpc.deletes.isEmpty()) + } + + // ------ RenameSessionAction.update ------ + + fun `test rename action enabled for exactly one local item`() { + val action = RenameSessionAction() + val local = localItem("ses_1") + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(local)), controller) + + action.update(event) + + assertTrue(event.presentation.isEnabledAndVisible) + } + + fun `test rename action disabled with no selection`() { + val action = RenameSessionAction() + val event = event(action, manager, selection(HistorySource.LOCAL, emptyList()), controller) + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + fun `test rename action disabled with multiple local items`() { + val action = RenameSessionAction() + val items = listOf(localItem("ses_1"), localItem("ses_2")) + val event = event(action, manager, selection(HistorySource.LOCAL, items), controller) + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + fun `test rename action disabled for cloud selection`() { + val action = RenameSessionAction() + val item = cloudItem("cloud_1") + val event = event(action, manager, selection(HistorySource.CLOUD, emptyList(), listOf(item)), controller) + + action.update(event) + + assertFalse(event.presentation.isEnabledAndVisible) + } + + // ------ RenameSessionAction.actionPerformed ------ + + fun `test rename action calls controller with trimmed changed title`() { + rpc.listed += sessionDto("ses_1", "Original") + controller.reloadLocal() + flush() + + val item = controller.local.items[0] + val action = RenameSessionAction().apply { input = { _, _ -> " Renamed " } } + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + + action.actionPerformed(event) + flush() + + assertEquals(listOf(Triple("ses_1", "/test", "Renamed")), rpc.renames) + } + + fun `test rename action passes displayed current title to input`() { + rpc.listed += sessionDto("ses_1", "Original") + controller.reloadLocal() + flush() + + val prompts = mutableListOf() + val item = controller.local.items[0] + val action = RenameSessionAction().apply { + input = { _, current -> + prompts.add(current) + null + } + } + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + + action.actionPerformed(event) + flush() + + assertEquals(listOf("Original"), prompts) + assertTrue(rpc.renames.isEmpty()) + } + + fun `test rename action passes untitled fallback to input`() { + rpc.listed += sessionDto("ses_1", "") + controller.reloadLocal() + flush() + + val prompts = mutableListOf() + val item = controller.local.items[0] + val action = RenameSessionAction().apply { + input = { _, current -> + prompts.add(current) + null + } + } + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + + action.actionPerformed(event) + flush() + + assertEquals(listOf(KiloBundle.message("history.untitled")), prompts) + assertTrue(rpc.renames.isEmpty()) + } + + fun `test rename action ignores blank input`() { + rpc.listed += sessionDto("ses_1", "Original") + controller.reloadLocal() + flush() + + val item = controller.local.items[0] + val action = RenameSessionAction().apply { input = { _, _ -> " " } } + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + + action.actionPerformed(event) + flush() + + assertTrue(rpc.renames.isEmpty()) + } + + fun `test rename action ignores unchanged input`() { + rpc.listed += sessionDto("ses_1", "Original") + controller.reloadLocal() + flush() + + val item = controller.local.items[0] + val action = RenameSessionAction().apply { input = { _, _ -> "Original" } } + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + + action.actionPerformed(event) + flush() + + assertTrue(rpc.renames.isEmpty()) + } + + fun `test rename action ignores null input`() { + rpc.listed += sessionDto("ses_1", "Original") + controller.reloadLocal() + flush() + + val item = controller.local.items[0] + val action = RenameSessionAction().apply { input = { _, _ -> null } } + val event = event(action, manager, selection(HistorySource.LOCAL, listOf(item)), controller) + + action.actionPerformed(event) + flush() + + assertTrue(rpc.renames.isEmpty()) + } + + fun `test frontend descriptor registers history actions`() { + val xml = javaClass.classLoader.getResourceAsStream("kilo.jetbrains.frontend.xml") + ?.bufferedReader() + ?.use { it.readText() } + ?: error("missing frontend descriptor") + + assertTrue(xml.contains("id=\"Kilo.Session.Open\"")) + assertTrue(xml.contains("id=\"Kilo.Session.Rename\"")) + assertTrue(xml.contains("id=\"Kilo.Session.Delete\"")) + assertTrue(xml.contains("id=\"Kilo.History.ContextMenu\"")) + assertTrue(xml.contains("ref=\"Kilo.Session.Open\"")) + assertTrue(xml.contains("ref=\"Kilo.Session.Rename\"")) + assertTrue(xml.contains("ref=\"Kilo.Session.Delete\"")) + } + + // ------ Helpers ------ + + private fun event( + action: com.intellij.openapi.actionSystem.AnAction, + manager: SessionManager?, + selection: HistorySelection, + ctrl: HistoryController, + ): AnActionEvent { + val presentation = Presentation().apply { copyFrom(action.templatePresentation) } + val context = DataContext { id -> + when { + CommonDataKeys.PROJECT.`is`(id) -> project + SessionManager.KEY.`is`(id) -> manager + HistoryDataKeys.SELECTION.`is`(id) -> selection + HistoryDataKeys.CONTROLLER.`is`(id) -> ctrl + else -> null + } + } + return AnActionEvent.createFromDataContext("", presentation, context) + } + + private fun selection( + source: HistorySource, + local: List, + cloud: List = emptyList(), + ) = HistorySelection(source, local, cloud) + + private fun localItem(id: String, title: String = id) = LocalHistoryItem(sessionDto(id, title)) + + private fun sessionDto(id: String, title: String) = SessionDto( + id = id, + projectID = "prj", + directory = "/test", + title = title, + version = "1", + time = SessionTimeDto(created = 1.0, updated = 2.0), + ) + + private fun cloudItem(id: String, title: String = id) = CloudHistoryItem( + CloudSessionDto( + id = id, + title = title, + createdAt = "2026-01-01T00:00:00Z", + updatedAt = "2026-01-02T00:00:00Z", + version = 1.0, + ) + ) + + private fun flush() = runBlocking { + repeat(10) { + delay(100) + ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() } + } + } + + private fun waitFor(done: () -> Boolean) = runBlocking { + withTimeout(5_000) { + while (!done()) { + delay(25) + ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() } + } + } + ApplicationManager.getApplication().invokeAndWait { UIUtil.dispatchAllInvocationEvents() } + } + + private class FakeManager : SessionManager { + override fun newSession() {} + override fun showHistory() {} + override fun openSession(ref: SessionRef) {} + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloSessionServiceTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloSessionServiceTest.kt new file mode 100644 index 00000000000..3a6ae7a3d99 --- /dev/null +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/app/KiloSessionServiceTest.kt @@ -0,0 +1,100 @@ +package ai.kilocode.client.app + +import ai.kilocode.client.testing.FakeSessionRpcApi +import ai.kilocode.rpc.dto.SessionDto +import ai.kilocode.rpc.dto.SessionTimeDto +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +@Suppress("UnstableApiUsage") +class KiloSessionServiceTest : BasePlatformTestCase() { + private lateinit var scope: CoroutineScope + private lateinit var rpc: FakeSessionRpcApi + private lateinit var service: KiloSessionService + + override fun setUp() { + super.setUp() + scope = CoroutineScope(SupervisorJob()) + rpc = FakeSessionRpcApi() + service = KiloSessionService(project, scope, rpc) + } + + override fun tearDown() { + try { + scope.cancel() + } finally { + super.tearDown() + } + } + + fun `test rename replaces cached session in sessions value`() = runBlocking(Dispatchers.Default) { + rpc.listed += session("ses_1", "Original") + rpc.listed += session("ses_2", "Other") + service.list("/test") + + assertEquals(2, service.sessions.value.size) + assertEquals("Original", service.sessions.value.find { it.id == "ses_1" }!!.title) + + service.renameSession("ses_1", "/test", "Renamed") + + assertEquals(2, service.sessions.value.size) + assertEquals("Renamed", service.sessions.value.find { it.id == "ses_1" }!!.title) + assertEquals("Other", service.sessions.value.find { it.id == "ses_2" }!!.title) + } + + fun `test rename of unknown id does not insert new item`() = runBlocking(Dispatchers.Default) { + rpc.listed += session("ses_1", "Original") + service.list("/test") + + assertEquals(1, service.sessions.value.size) + + service.renameSession("ses_unknown", "/test", "Should Not Insert") + + // Size remains 1 — no unexpected insert + assertEquals(1, service.sessions.value.size) + assertEquals("ses_1", service.sessions.value[0].id) + } + + fun `test rename failure propagates exception without mutating cache`() = runBlocking(Dispatchers.Default) { + rpc.listed += session("ses_1", "Original") + service.list("/test") + + val before = service.sessions.value.toList() + rpc.renameThrows = RuntimeException("server error") + + var threw = false + try { + service.renameSession("ses_1", "/test", "Renamed") + } catch (_: RuntimeException) { + threw = true + } + + assertTrue(threw) + assertEquals(before.map { it.id to it.title }, service.sessions.value.map { it.id to it.title }) + } + + fun `test list populates sessions value`() = runBlocking(Dispatchers.Default) { + rpc.listed += session("ses_1", "One") + rpc.listed += session("ses_2", "Two") + + service.list("/test") + + assertEquals(2, service.sessions.value.size) + assertTrue(service.sessions.value.any { it.id == "ses_1" }) + assertTrue(service.sessions.value.any { it.id == "ses_2" }) + } + + private fun session(id: String, title: String) = SessionDto( + id = id, + projectID = "prj", + directory = "/test", + title = title, + version = "1", + time = SessionTimeDto(created = 1.0, updated = 2.0), + ) +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index 250877766f5..7cddaef3a4d 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.session.SessionManager import ai.kilocode.client.session.SessionRef import ai.kilocode.client.testing.FakeSessionRpcApi import ai.kilocode.client.testing.FakeWorkspaceRpcApi @@ -26,6 +27,7 @@ import java.awt.Cursor import java.awt.event.KeyEvent import java.time.Instant import java.time.temporal.ChronoUnit +import java.util.concurrent.atomic.AtomicInteger import javax.swing.JComponent import javax.swing.KeyStroke import javax.swing.event.ListDataEvent @@ -80,13 +82,13 @@ class HistoryControllerTest : BasePlatformTestCase() { rpc.cloudCursor = "next_1" val controller = controller() - controller.reloadCloud(gitUrl = "git@example.com:repo.git") + controller.reloadCloud() flush() assertEquals(1, controller.cloud.items.size) assertEquals("cloud_1", controller.cloud.items[0].id) assertEquals("next_1", controller.cloud.cursor) - assertEquals(FakeSessionRpcApi.CloudCall("/test", null, 150, "git@example.com:repo.git"), rpc.cloudCalls[0]) + assertEquals(FakeSessionRpcApi.CloudCall("/test", null, 50, null), rpc.cloudCalls[0]) rpc.cloud.clear() rpc.cloud += cloud("cloud_2", "Cloud Two") @@ -95,7 +97,7 @@ class HistoryControllerTest : BasePlatformTestCase() { flush() assertEquals(listOf("cloud_1", "cloud_2"), controller.cloud.items.map { it.id }) - assertEquals(FakeSessionRpcApi.CloudCall("/test", "next_1", 150, "git@example.com:repo.git"), rpc.cloudCalls[1]) + assertEquals(FakeSessionRpcApi.CloudCall("/test", "next_1", 50, null), rpc.cloudCalls[1]) } fun `test local delete calls rpc and removes item`() { @@ -197,7 +199,7 @@ class HistoryControllerTest : BasePlatformTestCase() { flush() val local = panel.defaultFocusedComponent - assertFalse(panel.listFocusable()) + assertTrue(panel.listFocusable()) assertEquals(-1, panel.selectedIndex()) key(local, KeyEvent.VK_DOWN) assertEquals(0, panel.selectedIndex()) @@ -211,7 +213,7 @@ class HistoryControllerTest : BasePlatformTestCase() { val cloud = panel.defaultFocusedComponent assertNotSame(local, cloud) - assertFalse(panel.listFocusable()) + assertTrue(panel.listFocusable()) assertEquals(-1, panel.selectedIndex()) key(cloud, KeyEvent.VK_DOWN) assertEquals(0, panel.selectedIndex()) @@ -297,21 +299,382 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(KiloBundle.message("history.time.hours", 10), HistoryTime.relative(offset, now.toEpochMilli())) } - fun `test local renderer exposes delete and cloud renderer hides it`() { - rpc.listed += session("ses_1", "Local") - rpc.cloud += cloud("cloud_1", "Cloud") + fun `test history section boundaries are inclusive at 7 and 30 days`() { + val now = Instant.now() + val exactly7 = LocalHistoryItem(session("s7", "7d", now.minus(7, ChronoUnit.DAYS).toEpochMilli().toDouble())) + val exactly30 = LocalHistoryItem(session("s30", "30d", now.minus(30, ChronoUnit.DAYS).toEpochMilli().toDouble())) + val within7 = LocalHistoryItem(session("s6", "6d", now.minus(6, ChronoUnit.DAYS).toEpochMilli().toDouble())) + val within30 = LocalHistoryItem(session("s29", "29d", now.minus(29, ChronoUnit.DAYS).toEpochMilli().toDouble())) + val beyond30 = LocalHistoryItem(session("s31", "31d", now.minus(31, ChronoUnit.DAYS).toEpochMilli().toDouble())) + + assertEquals(HistorySection.WEEK, HistoryTime.section(exactly7, now.toEpochMilli())) + assertEquals(HistorySection.WEEK, HistoryTime.section(within7, now.toEpochMilli())) + assertEquals(HistorySection.MONTH, HistoryTime.section(exactly30, now.toEpochMilli())) + assertEquals(HistorySection.MONTH, HistoryTime.section(within30, now.toEpochMilli())) + assertEquals(HistorySection.OLDER, HistoryTime.section(beyond30, now.toEpochMilli())) + } + + fun `test list is focusable and uses multiple interval selection`() { val panel = HistoryPanel(parent, controller()) flush() - assertTrue(panel.deleteVisible(0)) + assertTrue(panel.listFocusable()) + assertEquals(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION, panel.listSelectionMode()) + } + + fun `test load more button is focusable`() { + rpc.cloud += cloud("cloud_1", "Cloud") + rpc.cloudCursor = "next" + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() + + assertTrue(panel.loadMoreFocusable()) + } + + fun `test rename updates local item title`() { + rpc.listed += session("ses_1", "Original") + val controller = controller() + flush() + + controller.reloadLocal() + flush() + + val item = controller.local.items[0] + controller.rename(item, "Renamed") + flush() + + assertEquals("Renamed", controller.local.items[0].title) + assertEquals(listOf(Triple("ses_1", "/test", "Renamed")), rpc.renames) + } + + fun `test data context exposes selection and controller`() { + rpc.listed += session("ses_1", "Alpha") + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() + + panel.select(0) + + val sel = panel.getData(HistoryDataKeys.SELECTION.name) as? HistorySelection + assertNotNull(sel) + assertEquals(1, sel!!.selectedLocal.size) + assertEquals("ses_1", sel.selectedLocal[0].id) + + val ctrl = panel.getData(HistoryDataKeys.CONTROLLER.name) + assertSame(controller, ctrl) + } + + // ------ Multi-selection and cloud selection data context ------ + + fun `test data context exposes local multi-selection`() { + rpc.listed += session("ses_1", "Alpha") + rpc.listed += session("ses_2", "Beta") + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() + + panel.selectIndices(0, 1) + + val sel = panel.getData(HistoryDataKeys.SELECTION.name) as? HistorySelection + assertNotNull(sel) + assertEquals(HistorySource.LOCAL, sel!!.source) + assertEquals(2, sel.selectedLocal.size) + assertTrue(sel.selectedLocal.map { it.id }.containsAll(listOf("ses_1", "ses_2"))) + } + + fun `test data context exposes cloud selection`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() panel.clickCloud() flush() - assertFalse(panel.cloudDeleteVisible(0)) + panel.select(0) + + val sel = panel.getData(HistoryDataKeys.SELECTION.name) as? HistorySelection + assertNotNull(sel) + assertEquals(HistorySource.CLOUD, sel!!.source) + assertTrue(sel.selectedLocal.isEmpty()) + assertEquals(1, sel.cloudItems.size) + assertEquals("cloud_1", sel.cloudItems[0].id) + } + + fun `test data context exposes session manager`() { + val manager = FakeManager() + val controller = controller() + val panel = HistoryPanel(parent, controller, manager = manager) + flush() + + assertSame(manager, panel.getData(SessionManager.KEY.name)) + } + + fun `test data context returns null for absent session manager`() { + val controller = controller() + val panel = HistoryPanel(parent, controller) + flush() + + assertNull(panel.getData(SessionManager.KEY.name)) + } + + fun `test local list uses multiple interval selection mode`() { + val panel = HistoryPanel(parent, controller()) + flush() + + assertEquals(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION, panel.listSelectionMode()) + } + + fun `test cloud list uses single selection mode`() { + val panel = HistoryPanel(parent, controller()) + flush() + + panel.clickCloud() + flush() + + assertEquals(javax.swing.ListSelectionModel.SINGLE_SELECTION, panel.listSelectionMode()) + } + + // ------ Rename failure and directory selection ------ + + fun `test rename failure keeps original title and sets error`() { + rpc.listed += session("ses_1", "Original") + val controller = controller() + controller.reloadLocal() + flush() + + rpc.renameThrows = IllegalStateException("rename failed") + val item = controller.local.items[0] + controller.rename(item, "Renamed") + flush() + + assertEquals("Original", controller.local.items[0].title) + assertNotNull(controller.local.error) + } + + fun `test rename uses item directory when present`() { + val dto = session("ses_1", "Original").copy(directory = "/worktree/path") + rpc.listed += dto + val controller = controller() + controller.reloadLocal() + flush() + + val item = controller.local.items[0] + assertEquals("/worktree/path", item.directory) + + controller.rename(item, "Renamed") + flush() + + assertEquals(listOf(Triple("ses_1", "/worktree/path", "Renamed")), rpc.renames) + } + + fun `test rename falls back to workspace directory when item directory is workspace`() { + // When session.directory matches workspace directory (not a worktree override) + rpc.listed += session("ses_1", "Original") + val controller = controller() + controller.reloadLocal() + flush() + + val item = controller.local.items[0] + controller.rename(item, "Renamed") + flush() + + assertEquals(listOf(Triple("ses_1", "/test", "Renamed")), rpc.renames) + } + + // ------ HistoryModel update/sorting ------ + + fun `test model update re-sorts items by updated time`() { + val now = java.time.Instant.now() + rpc.listed += session("ses_1", "Alpha", now.toEpochMilli().toDouble()) + rpc.listed += session("ses_2", "Beta", now.minusSeconds(100).toEpochMilli().toDouble()) + val controller = controller() + controller.reloadLocal() + flush() + + // ses_1 is newer so comes first + assertEquals("ses_1", controller.local.items[0].id) + + // Update ses_2 to be newer + val updated = session("ses_2", "Beta Updated", now.plusSeconds(100).toEpochMilli().toDouble()) + controller.local.update(LocalHistoryItem(updated)) + + // Now ses_2 should come first + assertEquals("ses_2", controller.local.items[0].id) + assertEquals("Beta Updated", controller.local.items[0].title) + } + + fun `test model update with unknown id leaves model unchanged`() { + rpc.listed += session("ses_1", "Alpha") + val controller = controller() + controller.reloadLocal() + flush() + + val before = controller.local.items.toList() + val unknown = session("unknown_id", "Unknown") + controller.local.update(LocalHistoryItem(unknown)) + + assertEquals(before.map { it.id }, controller.local.items.map { it.id }) + } + + fun `test model update removes renamed item from active filter`() { + rpc.listed += session("ses_1", "Alpha") + rpc.listed += session("ses_2", "Beta") + val controller = controller() + controller.reloadLocal() + flush() + + controller.local.setFilter("alpha") + assertEquals(listOf("ses_1"), controller.local.visibleItems.map { it.id }) + + controller.local.update(LocalHistoryItem(session("ses_1", "Gamma"))) + + assertTrue(controller.local.visibleItems.isEmpty()) + } + + fun `test model update adds renamed item to active filter`() { + rpc.listed += session("ses_1", "Alpha") + rpc.listed += session("ses_2", "Beta") + val controller = controller() + controller.reloadLocal() + flush() + + controller.local.setFilter("gamma") + assertTrue(controller.local.visibleItems.isEmpty()) + + controller.local.update(LocalHistoryItem(session("ses_2", "Gamma"))) + + assertEquals(listOf("ses_2"), controller.local.visibleItems.map { it.id }) + } + + fun `test cloud load passes git url when repo only enabled`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + val url = "git@github.com:test/repo.git" + val controller = controllerWithGit(url) + + controller.reloadCloud() + flush() + + assertEquals(1, rpc.cloudCalls.size) + assertEquals(url, rpc.cloudCalls[0].gitUrl) + assertEquals(true, controller.repoOnly) + assertEquals(url, controller.gitUrl) + } + + fun `test cloud load passes null when no git url`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + val controller = controllerWithGit(null) + + controller.reloadCloud() + flush() + + assertEquals(1, rpc.cloudCalls.size) + assertNull(rpc.cloudCalls[0].gitUrl) + assertEquals(false, controller.repoOnly) + assertNull(controller.gitUrl) + } + + fun `test cloud git url resolves once across overlapping reloads`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + val calls = AtomicInteger() + val controller = HistoryController(sessions, workspace, scope, gitUrlProvider = { + calls.incrementAndGet() + Thread.sleep(100) + "git@github.com:test/repo.git" + }) + + controller.reloadCloud() + controller.reloadCloud() + flush() + + assertEquals(1, calls.get()) + assertEquals(2, rpc.cloudCalls.size) + } + + fun `test cloud load passes null when repo only disabled`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + val url = "git@github.com:test/repo.git" + val controller = controllerWithGit(url) + + controller.reloadCloud() + flush() + assertEquals(url, rpc.cloudCalls[0].gitUrl) + + controller.applyRepoOnly(false) + flush() + + assertEquals(2, rpc.cloudCalls.size) + assertNull(rpc.cloudCalls[1].gitUrl) + } + + fun `test load more passes git url when repo only enabled`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + rpc.cloudCursor = "next_1" + val url = "git@github.com:test/repo.git" + val controller = controllerWithGit(url) + + controller.reloadCloud() + flush() + + rpc.cloud.clear() + rpc.cloud += cloud("cloud_2", "Cloud Two") + rpc.cloudCursor = null + controller.loadMoreCloud() + flush() + + assertEquals(2, rpc.cloudCalls.size) + assertEquals(url, rpc.cloudCalls[1].gitUrl) + } + + fun `test repo only checkbox visible only when git url exists`() { + val url = "git@github.com:test/repo.git" + val panel = HistoryPanel(parent, controllerWithGit(url)) + flush() + + panel.clickCloud() + flush() + + assertTrue(panel.repoOnlyVisible()) + assertTrue(panel.repoOnlySelected()) + } + + fun `test repo only checkbox hidden when no git url`() { + val panel = HistoryPanel(parent, controllerWithGit(null)) + flush() + + panel.clickCloud() + flush() + + assertFalse(panel.repoOnlyVisible()) + } + + fun `test repo only checkbox toggle reloads cloud history`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + val url = "git@github.com:test/repo.git" + val panel = HistoryPanel(parent, controllerWithGit(url)) + flush() + + panel.clickCloud() + flush() + + val before = rpc.cloudCalls.size + + panel.clickRepoOnly() + flush() + + assertTrue(rpc.cloudCalls.size > before) + assertFalse(panel.repoOnlySelected()) } private fun controller() = HistoryController(sessions, workspace, scope) + private fun controllerWithGit(url: String?) = HistoryController( + sessions, + workspace, + scope, + gitUrlProvider = { url }, + ) + private fun controller(opened: MutableList) = HistoryController(sessions, workspace, scope, open = { open -> val id = when (open) { is SessionRef.Local -> open.id @@ -320,6 +683,12 @@ class HistoryControllerTest : BasePlatformTestCase() { opened.add(id) }) + private class FakeManager : SessionManager { + override fun newSession() {} + override fun showHistory() {} + override fun openSession(ref: SessionRef) {} + } + private fun collect(controller: HistoryController): MutableList { val events = mutableListOf() val listener = object : ListDataListener { diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt index dd7c8537930..ca5d29db1cc 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeSessionRpcApi.kt @@ -86,6 +86,9 @@ class FakeSessionRpcApi : KiloSessionRpcApi { val questionReplies = mutableListOf>() val questionRejects = mutableListOf>() val deletes = mutableListOf>() + var deleteGate: CompletableDeferred? = null + val renames = mutableListOf>() + var renameThrows: Exception? = null val lists = mutableListOf() val recentCalls = mutableListOf>() val cloudCalls = mutableListOf() @@ -127,10 +130,23 @@ class FakeSessionRpcApi : KiloSessionRpcApi { override suspend fun delete(id: String, directory: String) { assertNotEdt("delete") + deleteGate?.await() deletes.add(id to directory) listed.removeAll { it.id == id } } + override suspend fun rename(id: String, directory: String, title: String): SessionDto { + assertNotEdt("rename") + renameThrows?.let { throw it } + renames.add(Triple(id, directory, title)) + val updated = listed.indexOfFirst { it.id == id } + if (updated >= 0) { + listed[updated] = listed[updated].copy(title = title) + return listed[updated] + } + return session.copy(id = id, title = title) + } + override suspend fun cloudSessions(directory: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto { assertNotEdt("cloudSessions") cloudCalls.add(CloudCall(directory, cursor, limit, gitUrl)) diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt index c467af110d1..25c8a63c3ab 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloSessionRpcApi.kt @@ -51,6 +51,9 @@ interface KiloSessionRpcApi : RemoteApi { /** Delete a session. */ suspend fun delete(id: String, directory: String) + /** Rename a session. */ + suspend fun rename(id: String, directory: String, title: String): SessionDto + /** List cloud-backed sessions. */ suspend fun cloudSessions(directory: String, cursor: String?, limit: Int, gitUrl: String?): CloudSessionListDto diff --git a/packages/kilo-ui/src/components/basic-tool.tsx b/packages/kilo-ui/src/components/basic-tool.tsx index 66d15277b35..1cf5a25bf1f 100644 --- a/packages/kilo-ui/src/components/basic-tool.tsx +++ b/packages/kilo-ui/src/components/basic-tool.tsx @@ -1 +1,27 @@ -export * from "@opencode-ai/ui/basic-tool" +import { BasicTool as Base, GenericTool } from "@opencode-ai/ui/basic-tool" +import type { BasicToolProps as BaseProps, TriggerTitle } from "@opencode-ai/ui/basic-tool" +import { toolOpenKey, readToolOpen, writeToolOpen } from "./tool-open-state" + +export { GenericTool } +export type { TriggerTitle } + +export interface BasicToolProps extends BaseProps { + tool?: string + callID?: string + partID?: string +} + +export function BasicTool(props: BasicToolProps) { + const key = () => toolOpenKey(props) + const initial = () => (props.forceOpen ? true : readToolOpen(key(), props.defaultOpen)) + return ( + { + writeToolOpen(key(), open) + props.onOpenChange?.(open) + }} + /> + ) +} diff --git a/packages/kilo-ui/src/components/message-part.css b/packages/kilo-ui/src/components/message-part.css index bb6f99c515a..5494f7c0c71 100644 --- a/packages/kilo-ui/src/components/message-part.css +++ b/packages/kilo-ui/src/components/message-part.css @@ -195,11 +195,69 @@ html[data-theme="kilo-vscode"] [data-component="bash-output"] { color: var(--vscode-terminal-foreground, var(--vscode-editor-foreground)); } - [data-slot="bash-copy"] [data-component="icon-button"][data-variant="secondary"] { - background: var(--vscode-terminal-background, var(--vscode-panel-background)); + [data-slot="bash-divider"] { + background: var(--vscode-panel-border, var(--border-weak-base)); } } +/* Bash tool: section layout (command + output) */ +[data-component="bash-output"] [data-slot="bash-section"] { + position: relative; +} + +[data-component="bash-output"] [data-slot="bash-section-code"] { + overflow-y: auto; + overflow-x: hidden; + max-height: 240px; + scrollbar-width: none; + &::-webkit-scrollbar { + display: none; + } +} + +[data-component="bash-output"] [data-slot="bash-section-actions"] { + position: absolute; + top: 4px; + right: 4px; + display: flex; + align-items: center; + gap: 2px; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s ease; +} + +[data-component="bash-output"] [data-slot="bash-section"]:hover [data-slot="bash-section-actions"], +[data-component="bash-output"] [data-slot="bash-section"]:focus-within [data-slot="bash-section-actions"] { + opacity: 1; + pointer-events: auto; +} + +/* Edge-to-edge divider (matches MCP tool divider) */ +[data-component="bash-output"] [data-slot="bash-divider"] { + height: 1px; + background: var(--border-weak-base); +} + +/* Shiki syntax-highlighted shell output */ +[data-component="bash-output"] .shiki, +[data-component="shell-expanded-output"] .shiki { + margin: 0; + padding: 12px; + background: transparent !important; +} + +[data-component="bash-output"] .shiki code, +[data-component="shell-expanded-output"] .shiki code { + font-family: var(--font-family-mono); + font-feature-settings: var(--font-family-mono--font-feature-settings); + font-size: var(--font-size-small); + line-height: var(--line-height-large); + white-space: pre-wrap; + overflow-wrap: anywhere; + background: transparent; +} + /* Tool error inline card (upstream removed these styles in v1.2.25) */ [data-component="tool-error"] { display: flex; diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 5df2fd231a1..759c3d96dff 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -50,10 +50,13 @@ import { TextShimmer } from "@opencode-ai/ui/text-shimmer" import { GrowBox } from "./grow-box" import { COLLAPSIBLE_SPRING } from "./motion" import { busy, createThrottledValue, useToolFade, useContextToolPending } from "./tool-utils" +import { readToolOpen, toolOpenKey } from "./tool-open-state" import { ContextToolGroupHeader, ContextToolExpandedList, ContextToolRollingResults } from "./context-tool-results" import { ShellRollingResults } from "./shell-rolling-results" import { extractFilePathFromHref } from "../file-path" import { normalize } from "./session-diff" +import { deferredHighlight } from "../context/marked" +import { escapeHtml } from "../util/escape-html" // Windows CLI tools (e.g. winget) use \r to overwrite progress bars in-place. // Without this, every progress frame renders as a separate visual line. @@ -1117,6 +1120,9 @@ function McpTool(props: ToolProps) { string; label: string }) { + const i18n = useI18n() + const [copied, setCopied] = createSignal(false) + const handler = async () => { + const text = props.value() + if (!text) return + await navigator.clipboard.writeText(text) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + return ( + + + + ) +} + +function BashHighlightedOutput(props: { cmd: string; output: string; outputPath?: string; active?: boolean }) { + const data = useData() + const i18n = useI18n() + const cmdState = { signal: { aborted: false } } + const outState = { signal: { aborted: false } } + let cmdRef: HTMLDivElement | undefined + let outRef: HTMLDivElement | undefined + + createEffect(() => { + cmdState.signal.aborted = true + if (!props.active) return + const cmd = props.cmd + if (!cmdRef || !cmd) return + const signal = { aborted: false } + cmdState.signal = signal + cmdRef.innerHTML = `
${escapeHtml(cmd)}
` + void deferredHighlight(cmdRef, undefined, signal) + }) + + createEffect(() => { + outState.signal.aborted = true + if (!props.active) return + const out = props.output + if (!outRef || !out) return + const signal = { aborted: false } + outState.signal = signal + outRef.innerHTML = `
${escapeHtml(out)}
` + void deferredHighlight(outRef, undefined, signal) + }) + + onCleanup(() => { + cmdState.signal.aborted = true + outState.signal.aborted = true + }) + + const openInEditor = () => { + // When output was truncated, open the full output file on disk + if (props.outputPath && data.openFile) { + data.openFile(props.outputPath) + return + } + if (!data.openContent) return + data.openContent(props.output, "log") + } + + return ( +
+ +
{i18n.t("ui.messagePart.shell.command")}
+
+
+
+ props.cmd} label={i18n.t("ui.message.copy")} /> +
+
+ + +
+ + +
{i18n.t("ui.messagePart.shell.output")}
+
+
+
+ + + + + + props.output} label={i18n.t("ui.message.copy")} /> +
+
+ +
+ ) +} + ToolRegistry.register({ name: "bash", render(props) { @@ -2035,6 +2147,8 @@ ToolRegistry.register({ const pending = () => busy(props.status) const reveal = useToolReveal(pending, () => props.reveal !== false) const subtitle = () => props.input.description ?? props.metadata.description + const key = () => toolOpenKey(props) + const [open, setOpen] = createSignal(readToolOpen(key(), props.defaultOpen ?? true) ?? true) // also apply processCarriageReturns for Windows CLI tools const cmd = createMemo(() => { @@ -2048,19 +2162,6 @@ ToolRegistry.register({ return "" }) const out = createMemo(() => processCarriageReturns(stripAnsi(rawOutput()))) - const text = createMemo(() => `$ ${cmd()}${out() ? "\n\n" + out() : ""}`) - - const hasOutput = createMemo(() => out().length > 0) - const [copied, setCopied] = createSignal(false) - - const handleCopy = async () => { - const command = cmd() - if (!command) return - const content = out() ? `${command}\n\n${out()}` : command - await navigator.clipboard.writeText(content) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } return ( @@ -2080,29 +2182,7 @@ ToolRegistry.register({
} > -
-
- - e.preventDefault()} - onClick={handleCopy} - aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.message.copy")} - /> - -
-
-
-              {text()}
-            
-
-
+ ) }, diff --git a/packages/kilo-ui/src/components/shell-rolling-results.tsx b/packages/kilo-ui/src/components/shell-rolling-results.tsx index e5310b6ce08..f5a639cb695 100644 --- a/packages/kilo-ui/src/components/shell-rolling-results.tsx +++ b/packages/kilo-ui/src/components/shell-rolling-results.tsx @@ -3,6 +3,8 @@ import stripAnsi from "strip-ansi" import type { ToolPart } from "@kilocode/sdk/v2" import { useReducedMotion } from "../hooks/use-reduced-motion" import { useI18n } from "../context/i18n" +import { deferredHighlight } from "../context/marked" +import { escapeHtml } from "../util/escape-html" import { RollingResults } from "./rolling-results" import { Icon } from "./icon" import { IconButton } from "./icon-button" @@ -11,6 +13,7 @@ import { Tooltip } from "./tooltip" import { GROW_SPRING } from "./motion" import { useSpring } from "./motion-spring" import { busy, createThrottledValue, updateScrollMask, useCollapsible, useRowWipe, useToolFade } from "./tool-utils" +import { readToolOpen, toolOpenKey, writeToolOpen } from "./tool-open-state" function ShellRollingSubtitle(props: { text: string; animate?: boolean }) { let ref: HTMLSpanElement | undefined @@ -57,6 +60,28 @@ function ShellRollingCommand(props: { text: string; animate?: boolean }) { ) } +function ShellOutputHighlight(props: { code: string; active?: boolean }) { + const state = { signal: { aborted: false } } + let ref: HTMLDivElement | undefined + + createEffect(() => { + state.signal.aborted = true + if (!props.active) return + const code = props.code + if (!ref || !code) return + const signal = { aborted: false } + state.signal = signal + ref.innerHTML = `
${escapeHtml(code)}
` + void deferredHighlight(ref, undefined, signal) + }) + + onCleanup(() => { + state.signal.aborted = true + }) + + return
+} + function ShellExpanded(props: { cmd: string; out: string; open: boolean }) { const i18n = useI18n() const rows = 10 @@ -156,9 +181,7 @@ function ShellExpanded(props: { cmd: string; out: string; open: boolean }) { onScroll={updateMask} style={{ "max-height": `${cap()}px` }} > -
-                  {props.out}
-                
+
@@ -173,7 +196,8 @@ export function ShellRollingResults(props: { part: ToolPart; animate?: boolean; const reduce = useReducedMotion() const wiped = new Set() const [mounted, setMounted] = createSignal(false) - const [open, setOpen] = createSignal(props.defaultOpen ?? true) + const key = () => toolOpenKey({ tool: props.part.tool, callID: props.part.callID, partID: props.part.id }) + const [open, setOpen] = createSignal(readToolOpen(key(), props.defaultOpen ?? true) ?? true) onMount(() => setMounted(true)) const state = createMemo(() => props.part.state as Record) const pending = createMemo(() => busy(props.part.state.status)) @@ -205,7 +229,9 @@ export function ShellRollingResults(props: { part: ToolPart; animate?: boolean; const el = headerClipRef const viewport = el?.closest(".scroll-view__viewport") as HTMLElement | null const beforeY = el?.getBoundingClientRect().top ?? 0 - setOpen((prev) => !prev) + const next = !open() + setOpen(next) + writeToolOpen(key(), next) if (viewport && el) { requestAnimationFrame(() => { const afterY = el.getBoundingClientRect().top diff --git a/packages/kilo-ui/src/components/tool-open-state.ts b/packages/kilo-ui/src/components/tool-open-state.ts new file mode 100644 index 00000000000..3e95617ffe4 --- /dev/null +++ b/packages/kilo-ui/src/components/tool-open-state.ts @@ -0,0 +1,33 @@ +type Props = { + tool?: string + callID?: string + partID?: string +} + +const MAX = 2000 +const state = new Map() + +export function toolOpenKey(props: Props) { + const id = props.callID || props.partID + if (!id) return + if (!props.tool) return id + return `${props.tool}:${id}` +} + +export function readToolOpen(key: string | undefined, fallback: boolean | undefined) { + if (key && state.has(key)) return state.get(key) + return fallback +} + +export function writeToolOpen(key: string | undefined, value: boolean) { + if (!key) return + if (!state.has(key) && state.size >= MAX) { + const first = state.keys().next().value + if (first) state.delete(first) + } + state.set(key, value) +} + +export function resetToolOpenState() { + state.clear() +} diff --git a/packages/kilo-ui/src/util/escape-html.ts b/packages/kilo-ui/src/util/escape-html.ts new file mode 100644 index 00000000000..243d85be4c6 --- /dev/null +++ b/packages/kilo-ui/src/util/escape-html.ts @@ -0,0 +1,3 @@ +export function escapeHtml(s: string): string { + return s.replace(/&/g, "&").replace(//g, ">") +} diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 96932f34d35..0aa9c66aba8 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -607,6 +607,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await routeSuggestionWebviewMessage(this.questionCtx, message) if (await ModelState.handleMessage(message.type, message, this.client, (msg) => this.postMessage(msg))) return if (await routeAutocompleteMessage(message, (msg) => this.postMessage(msg))) return + if (this.handleEditorOpenMessage(message)) return if ( await handleSidebarWorktreeMessage(message, { post: (msg) => this.postMessage(msg), @@ -771,11 +772,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper break case "saveImage": return saveImage(this.getWorkspaceDirectory(this.currentSession?.id), message) - case "openFile": - if (message.filePath) { - this.handleOpenFile(message.filePath, message.line, message.column) - } - break case "requestProviders": this.fetchAndSendProviders().catch((e) => console.error("[Kilo New] fetchAndSendProviders failed:", e)) break @@ -2897,6 +2893,35 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper .then(open, (err) => console.error("[Kilo New] KiloProvider: Failed to preview image:", err)) } + private handleEditorOpenMessage(message: { + type?: string + filePath?: string + line?: number + column?: number + content?: string + language?: string + }): boolean { + if (message.type === "openFile") { + if (message.filePath) this.handleOpenFile(message.filePath, message.line, message.column) + return true + } + if (message.type === "openContent") { + if (message.content) this.handleOpenContent(message.content, message.language) + return true + } + return false + } + + /** + * Handle openContent request - open arbitrary text in an untitled VS Code editor tab. + */ + private handleOpenContent(content: string, language?: string): void { + vscode.workspace.openTextDocument({ content, language: language || "log" }).then( + (doc) => vscode.window.showTextDocument(doc, { preview: true }), + (err) => console.error("[Kilo New] KiloProvider: Failed to open content:", err), + ) + } + /** * Handle openFile request from the webview — open a file in the VS Code editor. * Resolves relative paths against the current session's directory (which may be diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 77237b86975..f88e32e770c 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -120,7 +120,6 @@ export class AgentManagerProvider implements Disposable { getState: () => this.getStateManager(), getRoot: () => this.getRoot(), getStateReady: () => this.stateReady, - getClient: () => this.connectionService.getClient(), git: this.gitOps, localDiff: (dir, base) => localDiffSummary(this.gitOps, dir, base, (...args) => this.log(...args)), localDiffFile: (dir, base, file) => localDiffFile(this.gitOps, dir, base, file, (...args) => this.log(...args)), @@ -529,6 +528,10 @@ export class AgentManagerProvider implements Disposable { this.state?.setSessionsCollapsed(m.collapsed) return null } + if (m.type === "agentManager.setSidebarCollapsed") { + this.state?.setSidebarCollapsed(m.collapsed) + return null + } if (this.handleSection(m)) return null if (m.type === "agentManager.setReviewDiffStyle") { this.state?.setReviewDiffStyle(m.style) @@ -855,6 +858,7 @@ export class AgentManagerProvider implements Disposable { case "agentManager.setTabOrder": case "agentManager.setWorktreeOrder": case "agentManager.setSessionsCollapsed": + case "agentManager.setSidebarCollapsed": case "agentManager.setReviewDiffStyle": case "agentManager.setDefaultBaseBranch": case "agentManager.createSection": @@ -1497,6 +1501,7 @@ export class AgentManagerProvider implements Disposable { tabOrder: state.getTabOrder(), worktreeOrder: state.getWorktreeOrder(), sessionsCollapsed: state.getSessionsCollapsed(), + sidebarCollapsed: state.getSidebarCollapsed(), reviewDiffStyle: state.getReviewDiffStyle(), reviewMarkdownRender: getDiffMarkdownRender(), isGitRepo: true, diff --git a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts index 74fea89b555..352932fde03 100644 --- a/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts +++ b/packages/kilo-vscode/src/agent-manager/WorktreeStateManager.ts @@ -71,6 +71,7 @@ interface StateFile { tabOrder?: Record worktreeOrder?: string[] sessionsCollapsed?: boolean + sidebarCollapsed?: boolean reviewDiffStyle?: "unified" | "split" defaultBaseBranch?: string } @@ -99,6 +100,7 @@ export class WorktreeStateManager { private tabOrder: Record = {} private worktreeOrder: string[] = [] private collapsed = false + private sidebar = false private reviewDiffStyle: "unified" | "split" = "unified" private defaultBase: string | undefined private readonly log: (msg: string) => void @@ -522,6 +524,19 @@ export class WorktreeStateManager { void this.save() } + // --------------------------------------------------------------------------- + // Sidebar collapsed + // --------------------------------------------------------------------------- + + getSidebarCollapsed(): boolean { + return this.sidebar + } + + setSidebarCollapsed(value: boolean): void { + this.sidebar = value + void this.save() + } + // --------------------------------------------------------------------------- // Review diff style // --------------------------------------------------------------------------- @@ -636,6 +651,7 @@ export class WorktreeStateManager { } const repaired = this.setNormalizedWorktreeOrder(this.worktreeOrder) this.collapsed = data.sessionsCollapsed ?? false + this.sidebar = data.sidebarCollapsed ?? false if (data.reviewDiffStyle === "split") { this.reviewDiffStyle = "split" } @@ -741,6 +757,9 @@ export class WorktreeStateManager { if (this.collapsed) { data.sessionsCollapsed = true } + if (this.sidebar) { + data.sidebarCollapsed = true + } if (this.reviewDiffStyle === "split") { data.reviewDiffStyle = "split" } diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index ef52f68ad15..4a74212b7a4 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -126,6 +126,7 @@ interface StateMessage { tabOrder?: Record worktreeOrder?: string[] sessionsCollapsed?: boolean + sidebarCollapsed?: boolean reviewDiffStyle?: "unified" | "split" reviewMarkdownRender?: boolean isGitRepo?: boolean @@ -456,6 +457,11 @@ interface SetSessionsCollapsedIn { collapsed: boolean } +interface SetSidebarCollapsedIn { + type: "agentManager.setSidebarCollapsed" + collapsed: boolean +} + interface SetReviewDiffStyleIn { type: "agentManager.setReviewDiffStyle" style: "unified" | "split" @@ -736,6 +742,7 @@ export type AgentManagerInMessage = | SetTabOrderIn | SetWorktreeOrderIn | SetSessionsCollapsedIn + | SetSidebarCollapsedIn | SetReviewDiffStyleIn | SetReviewMarkdownRenderIn | SetDefaultBaseBranchIn diff --git a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts index 94625e0544e..79694070a7e 100644 --- a/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts +++ b/packages/kilo-vscode/src/agent-manager/worktree-diff-controller.ts @@ -1,7 +1,6 @@ -import type { KiloClient } from "@kilocode/sdk/v2/client" import { SourceController } from "../diff/SourceController" import { resolveLocalDiffTarget } from "../diff/shared/target" -import { WorktreeDiffClient } from "../diff/shared/client" +import { WorktreeDiffReverter, type StatusResolver } from "../diff/shared/reverter" import type { DiffFile } from "../diff/types" import type { DiffSource, DiffSourceDescriptor, DiffSourceFetch } from "../diff/sources/types" import type { ApplyConflict, GitOps } from "./GitOps" @@ -20,12 +19,9 @@ export interface WorktreeDiffControllerContext { getRoot: () => string | undefined getStateReady: () => Promise | undefined /** - * SDK client — used by `revert()` via `WorktreeDiffClient` for the one-shot - * file-status lookup. Hot polling paths (`request`, `requestFile`, polling) - * deliberately bypass the client and go through `localDiff`/`localDiffFile` - * to keep git spawns out of the Bun `kilo serve` process (see oven-sh/bun#18265). + * In-process diff paths deliberately bypass the SDK client to keep git spawns + * out of the Bun `kilo serve` process (see oven-sh/bun#18265). */ - getClient: () => KiloClient git: GitOps /** In-process diff summary (replaces client.worktree.diffSummary). */ localDiff: (dir: string, base: string) => Promise @@ -272,7 +268,11 @@ export class WorktreeDiffController { if (!target) return { ok: false, message: "Could not resolve diff target" } try { - const diff = new WorktreeDiffClient(this.ctx.getClient(), this.ctx.git, (...args) => this.ctx.log(...args)) + const status: StatusResolver = async (current, item) => { + const diff = await this.ctx.localDiffFile(current.directory, current.baseBranch, item) + return diff?.status + } + const diff = new WorktreeDiffReverter(this.ctx.git, status, (...args) => this.ctx.log(...args)) return await diff.revertFile(target, file) } catch (error) { const msg = error instanceof Error ? error.message : String(error) diff --git a/packages/kilo-vscode/src/diff/shared/client.ts b/packages/kilo-vscode/src/diff/shared/reverter.ts similarity index 51% rename from packages/kilo-vscode/src/diff/shared/client.ts rename to packages/kilo-vscode/src/diff/shared/reverter.ts index 18922a77a20..84900a4ccf7 100644 --- a/packages/kilo-vscode/src/diff/shared/client.ts +++ b/packages/kilo-vscode/src/diff/shared/reverter.ts @@ -1,39 +1,34 @@ -import type { KiloClient } from "@kilocode/sdk/v2/client" import type { GitOps } from "../../agent-manager/GitOps" /** - * A worktree diff target: the working directory and the base branch we diff + * A worktree target: the working directory and the base branch we diff * against (usually the tracking branch). */ export type DiffTarget = { directory: string; baseBranch: string } -type Status = "added" | "deleted" | "modified" +export type DiffStatus = "added" | "deleted" | "modified" +export type StatusResolver = (target: DiffTarget, file: string) => Promise /** - * Thin coordinator that wraps (KiloClient, GitOps, DiffTarget) and exposes the - * small set of operations used by the Changes panel's WorktreeDiffSource and - * the agent manager's WorktreeDiffController. + * Thin coordinator that wraps local diff status lookup with GitOps revert + * behavior used by the Changes panel and Agent Manager. */ -export class WorktreeDiffClient { +export class WorktreeDiffReverter { constructor( - private readonly client: KiloClient, private readonly git: GitOps, + private readonly status: StatusResolver, private readonly log: (...args: unknown[]) => void, ) {} /** * Look up the diff status for a single file. Used by revert flows to pick - * the right git strategy (added → delete, modified/deleted → checkout). + * the right git strategy (added means delete, modified/deleted means checkout). * Returns `undefined` on error so callers can still attempt a best-effort - * revert — `GitOps.revertFile` defaults to a modified-file strategy. + * revert, `GitOps.revertFile` defaults to a modified-file strategy. */ - async fileStatus(target: DiffTarget, file: string): Promise { + async fileStatus(target: DiffTarget, file: string): Promise { try { - const { data } = await this.client.worktree.diffFile( - { directory: target.directory, base: target.baseBranch, file }, - { throwOnError: true }, - ) - return data?.status + return await this.status(target, file) } catch (err) { this.log("Failed to look up file status for revert:", err) return undefined @@ -41,7 +36,7 @@ export class WorktreeDiffClient { } /** - * Revert a single file in the worktree. Composes `fileStatus` + `GitOps.revertFile`. + * Revert a single file in the worktree. Composes `fileStatus` and `GitOps.revertFile`. * Returns a normalized result; callers handle UI/messaging. */ async revertFile(target: DiffTarget, file: string): Promise<{ ok: boolean; message: string }> { diff --git a/packages/kilo-vscode/src/diff/sources/catalog.ts b/packages/kilo-vscode/src/diff/sources/catalog.ts index 2c29b3536f0..fa36aaecf2d 100644 --- a/packages/kilo-vscode/src/diff/sources/catalog.ts +++ b/packages/kilo-vscode/src/diff/sources/catalog.ts @@ -91,7 +91,7 @@ export class DiffSourceCatalog implements vscode.Disposable { build(id: string, ctx: PanelContext): DiffSource { if (id === WORKSPACE_SOURCE_ID) { - return createWorktreeDiffSource(this.connection, { baseBranchOverride: ctx.baseBranchOverride }) + return createWorktreeDiffSource({ baseBranchOverride: ctx.baseBranchOverride }) } if (id === STAGED_SOURCE_ID) return createStagedDiffSource() diff --git a/packages/kilo-vscode/src/diff/sources/worktree.ts b/packages/kilo-vscode/src/diff/sources/worktree.ts index fdb2ebe9328..fdeea76a44a 100644 --- a/packages/kilo-vscode/src/diff/sources/worktree.ts +++ b/packages/kilo-vscode/src/diff/sources/worktree.ts @@ -1,9 +1,8 @@ import * as vscode from "vscode" -import type { KiloConnectionService } from "../../services/cli-backend" import { GitOps } from "../../agent-manager/GitOps" import { diffSummary, diffFile } from "../../agent-manager/local-diff" import type { WorktreeDiffEntry } from "../../agent-manager/types" -import { WorktreeDiffClient, type DiffTarget } from "../shared/client" +import { WorktreeDiffReverter, type DiffTarget, type StatusResolver } from "../shared/reverter" import { resolveLocalDiffTarget } from "../shared/target" import { appendOutput, getWorkspaceRoot } from "../../review-utils" import type { DiffFile } from "../types" @@ -32,10 +31,7 @@ export interface WorktreeDiffSourceOptions { * `before`/`after` per file on demand via `fetchFile`. Runs entirely in the * extension host — no `kilo serve` round-trip. */ -export function createWorktreeDiffSource( - connection: KiloConnectionService, - opts: WorktreeDiffSourceOptions = {}, -): DiffSource { +export function createWorktreeDiffSource(opts: WorktreeDiffSourceOptions = {}): DiffSource { const output = vscode.window.createOutputChannel("Kilo Diff: Workspace") const log = (...args: unknown[]) => appendOutput(output, "WorktreeDiffSource", ...args) const git = new GitOps({ log }) @@ -65,6 +61,11 @@ export function createWorktreeDiffSource( return target } + const status: StatusResolver = async (current, file) => { + const entry = await diffFile(git, current.directory, current.baseBranch, file, log) + return entry?.status + } + return { descriptor: WORKSPACE_DESCRIPTOR, @@ -98,8 +99,7 @@ export function createWorktreeDiffSource( if (!current) return { ok: false, message: "Could not resolve diff target" } try { - const client = connection.getClient() - const diff = new WorktreeDiffClient(client, git, log) + const diff = new WorktreeDiffReverter(git, status, log) return await diff.revertFile(current, file) } catch (err) { const message = err instanceof Error ? err.message : String(err) diff --git a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts index c3bf14b286c..c1ce05633dd 100644 --- a/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts +++ b/packages/kilo-vscode/tests/unit/agent-manager-arch.test.ts @@ -35,6 +35,7 @@ const TSX_FILES = [ path.join(ROOT, "webview-ui/agent-manager/WorktreeItem.tsx"), path.join(ROOT, "webview-ui/agent-manager/SectionHeader.tsx"), path.join(ROOT, "webview-ui/agent-manager/CurrentTabsMenu.tsx"), + path.join(ROOT, "webview-ui/agent-manager/SidebarToggleButton.tsx"), path.join(ROOT, "webview-ui/agent-manager/tab-rendering.tsx"), path.join(ROOT, "webview-ui/agent-manager/terminal/TerminalTab.tsx"), path.join(ROOT, "webview-ui/agent-manager/terminal/SortableTerminalTab.tsx"), diff --git a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts index 727c6a9b2e7..1397f191010 100644 --- a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts @@ -128,6 +128,13 @@ describe("DataProvider contract (runtime)", () => { expect(src).toContain("OpenDiffFn") expect(src).toMatch(/openDiff:\s*props\.onOpenDiff/) }) + + it("DataProvider accepts onOpenContent prop and exports OpenContentFn (source)", () => { + const src = fs.readFileSync(DATA_CONTEXT_FILE, "utf-8") + expect(src).toContain("onOpenContent") + expect(src).toContain("OpenContentFn") + expect(src).toMatch(/openContent:\s*props\.onOpenContent/) + }) }) describe("Edit tool diff-first click contract (source)", () => { @@ -164,6 +171,53 @@ describe("Write and apply_patch patch rendering contracts (source)", () => { }) }) +describe("Bash tool syntax highlighting and section labels (source)", () => { + const src = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8") + const block = + src.match(/ToolRegistry\.register\(\{\s*name:\s*"bash"[\s\S]*?(?=ToolRegistry\.register\(|$)/)?.[0] ?? "" + + it("bash tool renders BashHighlightedOutput", () => { + expect(block).toContain("BashHighlightedOutput") + }) + + it("BashHighlightedOutput uses shellscript grammar for commands without $ prefix", () => { + // The command should be highlighted as shellscript, but the $ prompt must + // NOT be inside the highlighted code (it breaks Shiki's parse context) + expect(src).toMatch(/data-lang="shellscript">\$\{escapeHtml\(cmd\)\}/) + expect(src).not.toMatch(/data-lang="shellscript">\$\s/) + }) + + it("BashHighlightedOutput uses log grammar for output", () => { + expect(src).toMatch(/data-lang="log"/) + }) + + it("BashHighlightedOutput renders section labels matching MCP tool pattern", () => { + // Must use the same data-slot as MCP tools for consistent styling + expect(src).toMatch(/data-slot="mcp-section-label".*shell\.command/) + expect(src).toMatch(/data-slot="mcp-section-label".*shell\.output/) + }) + + it("BashHighlightedOutput has edge-to-edge divider between sections", () => { + expect(src).toContain('data-slot="bash-divider"') + }) + + it("BashHighlightedOutput supports openContent for opening output in editor", () => { + expect(src).toContain("data.openContent") + expect(src).toContain("openInEditor") + }) + + it("BashHighlightedOutput opens full output file when truncated", () => { + // When the CLI truncates output, metadata.outputPath holds the full file. + // openInEditor should prefer openFile(outputPath) over openContent. + expect(src).toContain("props.outputPath") + expect(src).toMatch(/props\.outputPath.*data\.openFile/) + }) + + it("bash tool passes outputPath from metadata to BashHighlightedOutput", () => { + expect(block).toContain("props.metadata.outputPath") + }) +}) + describe("BasicTool export contract (runtime)", () => { it("BasicTool and GenericTool are exported from basic-tool", () => { const result = check(` diff --git a/packages/kilo-vscode/tests/unit/local-diff.test.ts b/packages/kilo-vscode/tests/unit/local-diff.test.ts index 3d6c6892706..cd9e35f9490 100644 --- a/packages/kilo-vscode/tests/unit/local-diff.test.ts +++ b/packages/kilo-vscode/tests/unit/local-diff.test.ts @@ -4,12 +4,24 @@ import * as os from "os" import * as path from "path" import { diffSummary, diffFile, generatedLike, resolveBase, MAX_DETAIL_BYTES } from "../../src/agent-manager/local-diff" import { GitOps } from "../../src/agent-manager/GitOps" +import { WorktreeDiffReverter } from "../../src/diff/shared/reverter" import { resolveLocalDiffTarget } from "../../src/diff/shared/target" function git(): GitOps { return new GitOps({ log: () => undefined }) } +function reverter(ops: GitOps): WorktreeDiffReverter { + return new WorktreeDiffReverter( + ops, + async (target, file) => { + const entry = await diffFile(ops, target.directory, target.baseBranch, file) + return entry?.status + }, + () => undefined, + ) +} + function runSync(cwd: string, args: string[]): string { const result = Bun.spawnSync({ cmd: ["git", ...args], @@ -332,4 +344,128 @@ describe("resolveLocalDiffTarget + revertFile", () => { expect(restored).toBe("seed\n") }) }) + + it("uses local diff status for reverting modified tracked files", async () => { + await withRepo(async (dir, base) => { + await fs.writeFile(path.join(dir, "seed.txt"), "seed\nchanged\n") + + const ops = git() + const result = await reverter(ops).revertFile({ directory: dir, baseBranch: base }, "seed.txt") + + expect(result.ok).toBe(true) + expect(await fs.readFile(path.join(dir, "seed.txt"), "utf-8")).toBe("seed\n") + ops.dispose() + }) + }) + + it("uses local diff status for reverting staged modified files", async () => { + await withRepo(async (dir, base) => { + await fs.writeFile(path.join(dir, "seed.txt"), "seed\nstaged\n") + runSync(dir, ["add", "seed.txt"]) + + const ops = git() + const result = await reverter(ops).revertFile({ directory: dir, baseBranch: base }, "seed.txt") + + expect(result.ok).toBe(true) + expect(await fs.readFile(path.join(dir, "seed.txt"), "utf-8")).toBe("seed\n") + expect(runSync(dir, ["status", "--short", "--", "seed.txt"])).toBe("") + ops.dispose() + }) + }) + + it("uses local diff status for reverting deleted tracked files", async () => { + await withRepo(async (dir, base) => { + const file = path.join(dir, "seed.txt") + await fs.rm(file) + + const ops = git() + const result = await reverter(ops).revertFile({ directory: dir, baseBranch: base }, "seed.txt") + + expect(result.ok).toBe(true) + expect(await fs.readFile(file, "utf-8")).toBe("seed\n") + ops.dispose() + }) + }) + + it("uses local diff status for reverting staged deleted files", async () => { + await withRepo(async (dir, base) => { + const file = path.join(dir, "seed.txt") + await fs.rm(file) + runSync(dir, ["add", "-A"]) + + const ops = git() + const result = await reverter(ops).revertFile({ directory: dir, baseBranch: base }, "seed.txt") + + expect(result.ok).toBe(true) + expect(await fs.readFile(file, "utf-8")).toBe("seed\n") + expect(runSync(dir, ["status", "--short", "--", "seed.txt"])).toBe("") + ops.dispose() + }) + }) + + it("uses local diff status for reverting tracked files added after base", async () => { + await withRepo(async (dir, base) => { + const file = path.join(dir, "tracked.txt") + await fs.writeFile(file, "tracked\n") + runSync(dir, ["add", "tracked.txt"]) + runSync(dir, ["commit", "-m", "add tracked"]) + + const ops = git() + const result = await reverter(ops).revertFile({ directory: dir, baseBranch: base }, "tracked.txt") + + expect(result.ok).toBe(true) + await expect(fs.stat(file)).rejects.toThrow() + ops.dispose() + }) + }) + + it("uses local diff status for reverting staged added files", async () => { + await withRepo(async (dir, base) => { + const file = path.join(dir, "staged.txt") + await fs.writeFile(file, "staged\n") + runSync(dir, ["add", "staged.txt"]) + + const ops = git() + const result = await reverter(ops).revertFile({ directory: dir, baseBranch: base }, "staged.txt") + + expect(result.ok).toBe(true) + await expect(fs.stat(file)).rejects.toThrow() + expect(runSync(dir, ["status", "--short", "--", "staged.txt"])).toBe("") + ops.dispose() + }) + }) + + it("uses local diff status for reverting untracked files", async () => { + await withRepo(async (dir, base) => { + const file = path.join(dir, "fresh.txt") + await fs.writeFile(file, "fresh\n") + + const ops = git() + const result = await reverter(ops).revertFile({ directory: dir, baseBranch: base }, "fresh.txt") + + expect(result.ok).toBe(true) + await expect(fs.stat(file)).rejects.toThrow() + ops.dispose() + }) + }) + + it("falls back to modified-file revert when local status lookup fails", async () => { + await withRepo(async (dir, base) => { + await fs.writeFile(path.join(dir, "seed.txt"), "seed\nchanged\n") + const ops = git() + const diff = new WorktreeDiffReverter( + ops, + async () => { + throw new Error("status failed") + }, + () => undefined, + ) + + const result = await diff.revertFile({ directory: dir, baseBranch: base }, "seed.txt") + + expect(result.ok).toBe(true) + expect(await fs.readFile(path.join(dir, "seed.txt"), "utf-8")).toBe("seed\n") + ops.dispose() + }) + }) }) diff --git a/packages/kilo-vscode/tests/unit/navigate.test.ts b/packages/kilo-vscode/tests/unit/navigate.test.ts index 819a6c15f89..3d0e5e9eb8d 100644 --- a/packages/kilo-vscode/tests/unit/navigate.test.ts +++ b/packages/kilo-vscode/tests/unit/navigate.test.ts @@ -5,6 +5,7 @@ import { adjacentHint, restoreLocalSessions, reconcileLocalSessions, + filterUnassignedSessions, LOCAL, } from "../../webview-ui/agent-manager/navigate" @@ -188,6 +189,105 @@ describe("adjacentHint", () => { }) }) +describe("filterUnassignedSessions", () => { + const at = (day: number) => `2026-01-${String(day).padStart(2, "0")}T00:00:00.000Z` + const info = (id: string, day: number, parentID?: string | null) => ({ + id, + createdAt: at(day), + ...(parentID === undefined ? {} : { parentID }), + }) + + it("keeps root sessions with undefined parent IDs", () => { + const result = filterUnassignedSessions([info("old", 1), info("new", 3)], new Set(), new Set()) + + expect(result.map((s) => s.id)).toEqual(["new", "old"]) + }) + + it("keeps root sessions with null parent IDs", () => { + const result = filterUnassignedSessions([info("root", 1, null)], new Set(), new Set()) + + expect(result.map((s) => s.id)).toEqual(["root"]) + }) + + it("filters child sessions with parent IDs", () => { + const result = filterUnassignedSessions( + [info("parent", 2), info("child", 3, "parent"), info("orphan", 4, "missing")], + new Set(), + new Set(), + ) + + expect(result.map((s) => s.id)).toEqual(["parent"]) + }) + + it("filters string parent IDs even when they are empty", () => { + const result = filterUnassignedSessions([info("blank", 2, ""), info("root", 1)], new Set(), new Set()) + + expect(result.map((s) => s.id)).toEqual(["root"]) + }) + + it("filters worktree sessions while keeping other roots", () => { + const result = filterUnassignedSessions( + [info("root", 1), info("worktree", 3), info("other", 2)], + new Set(["worktree"]), + new Set(), + ) + + expect(result.map((s) => s.id)).toEqual(["other", "root"]) + }) + + it("filters local tab sessions while keeping other roots", () => { + const result = filterUnassignedSessions( + [info("root", 1), info("local", 3), info("other", 2)], + new Set(), + new Set(["local"]), + ) + + expect(result.map((s) => s.id)).toEqual(["other", "root"]) + }) + + it("applies child, worktree, and local filters before sorting", () => { + const result = filterUnassignedSessions( + [info("old-root", 1), info("child", 6, "old-root"), info("worktree", 5), info("local", 4), info("new-root", 3)], + new Set(["worktree"]), + new Set(["local"]), + ) + + expect(result.map((s) => s.id)).toEqual(["new-root", "old-root"]) + }) + + it("returns an empty list when every session is filtered", () => { + const result = filterUnassignedSessions( + [info("child", 3, "root"), info("worktree", 2), info("local", 1)], + new Set(["worktree"]), + new Set(["local"]), + ) + + expect(result).toEqual([]) + }) + + it("does not mutate the input order", () => { + const sessions = [info("old", 1), info("new", 3), info("mid", 2)] + + filterUnassignedSessions(sessions, new Set(), new Set()) + + expect(sessions.map((s) => s.id)).toEqual(["old", "new", "mid"]) + }) + + it("preserves session objects and extra fields", () => { + const root = { ...info("root", 1), title: "Existing session" } + const result = filterUnassignedSessions([root], new Set(), new Set()) + + expect(result[0]).toBe(root) + expect(result[0]?.title).toBe("Existing session") + }) + + it("keeps a parent root when its child is filtered", () => { + const result = filterUnassignedSessions([info("root", 1), info("child", 2, "root")], new Set(), new Set()) + + expect(result.map((s) => s.id)).toEqual(["root"]) + }) +}) + describe("restoreLocalSessions", () => { const identity = (items: { id: string }[], _order: string[]) => items const isPending = (id: string) => id.startsWith("pending-") diff --git a/packages/kilo-vscode/tests/unit/tool-open-state.test.ts b/packages/kilo-vscode/tests/unit/tool-open-state.test.ts new file mode 100644 index 00000000000..871d2f42305 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/tool-open-state.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it, beforeEach } from "bun:test" +import { + readToolOpen, + resetToolOpenState, + toolOpenKey, + writeToolOpen, +} from "../../../kilo-ui/src/components/tool-open-state" + +describe("tool open state", () => { + beforeEach(() => { + resetToolOpenState() + }) + + it("keys state by tool and stable call id", () => { + const key = toolOpenKey({ tool: "bash", callID: "call_1", partID: "part_1" }) + expect(key).toBe("bash:call_1") + }) + + it("falls back until the user toggles a tool", () => { + const key = toolOpenKey({ tool: "bash", callID: "call_1" }) + expect(readToolOpen(key, false)).toBe(false) + writeToolOpen(key, true) + expect(readToolOpen(key, false)).toBe(true) + }) + + it("separates different tool instances", () => { + const bash = toolOpenKey({ tool: "bash", callID: "call_1" }) + const grep = toolOpenKey({ tool: "grep", callID: "call_1" }) + writeToolOpen(bash, true) + writeToolOpen(grep, false) + expect(readToolOpen(bash, false)).toBe(true) + expect(readToolOpen(grep, true)).toBe(false) + }) +}) 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 71c84b1a786..af5dde5456f 100644 --- a/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts +++ b/packages/kilo-vscode/tests/unit/worktree-state-manager.test.ts @@ -425,6 +425,40 @@ describe("WorktreeStateManager", () => { }) }) + describe("sidebarCollapsed", () => { + it("defaults to false", () => { + expect(manager.getSidebarCollapsed()).toBe(false) + }) + + it("sets and gets collapsed state", () => { + manager.setSidebarCollapsed(true) + expect(manager.getSidebarCollapsed()).toBe(true) + + manager.setSidebarCollapsed(false) + expect(manager.getSidebarCollapsed()).toBe(false) + }) + + it("persists and loads collapsed state", async () => { + manager.setSidebarCollapsed(true) + await manager.flush() + await manager.save() + + const loaded = new WorktreeStateManager(root, () => {}) + await loaded.load() + expect(loaded.getSidebarCollapsed()).toBe(true) + }) + + it("does not persist when false", async () => { + manager.setSidebarCollapsed(false) + await manager.flush() + await manager.save() + + const content = fs.readFileSync(path.join(root, ".kilo", "agent-manager.json"), "utf-8") + const data = JSON.parse(content) + expect(data.sidebarCollapsed).toBeUndefined() + }) + }) + describe("validate", () => { it("removes worktrees whose directories do not exist and prunes their sessions", async () => { const existing = path.join(root, "wt-exists") diff --git a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx index 71477796778..8cc186ff2ea 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/AgentManagerApp.tsx @@ -87,7 +87,14 @@ import { NewWorktreeDialog } from "./NewWorktreeDialog" import { LanguageBridge, DataBridge, MermaidDownloadBridge } from "../src/App" import { useLanguage } from "../src/context/language" import { formatRelativeDate } from "../src/utils/date" -import { nextSelectionAfterDelete, adjacentHint, restoreLocalSessions, reconcileLocalSessions, LOCAL } from "./navigate" +import { + nextSelectionAfterDelete, + adjacentHint, + restoreLocalSessions, + reconcileLocalSessions, + filterUnassignedSessions, + LOCAL, +} from "./navigate" import { reorderTabs, applyTabOrder, firstOrderedTitle } from "./tab-order" import { createTabOrderSync } from "./tab-order-sync" import { ConstrainDragYAxis } from "./sortable-tab" @@ -120,6 +127,8 @@ import { ConstrainDragXAxis } from "./constrain-drag-x" import { mergeWorktreeDiffs } from "./diff-state" import { initialMessage, seedInitialVariant } from "./initial-message" import { createMarkdownRender } from "./review-preferences" +import { createSidebarCollapse } from "./sidebar-collapse" +import { SidebarToggleButton } from "./SidebarToggleButton" import { setTabWidths } from "./tab-widths" import "./agent-manager.css" import "./agent-manager-review.css" @@ -284,6 +293,10 @@ const AgentManagerContent: Component = () => { setLocalSessionIDs((prev) => (prev.includes(sid) ? prev.filter((id) => id !== sid) : prev)) const [sidebarWidth, setSidebarWidth] = createSignal(persisted?.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH) const [sessionsCollapsed, setSessionsCollapsed] = createSignal(false) + const sidebar = createSidebarCollapse(vscode) + const sidebarCollapsed = sidebar.collapsed + const expandSidebar = sidebar.expand + const toggleSidebar = sidebar.toggle const [sections, setSections] = createSignal([]) // rAF coalescing for resize handlers — at most one signal write per frame @@ -717,9 +730,7 @@ const AgentManagerContent: Component = () => { // Sessions NOT in any worktree and not local const unassignedSessions = createMemo(() => - [...session.sessions()] - .filter((s) => !worktreeSessionIds().has(s.id) && !localSet().has(s.id)) - .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()), + filterUnassignedSessions(session.sessions(), worktreeSessionIds(), localSet()), ) // Local sessions (resolved from session list + pending tabs, in insertion order) @@ -1280,6 +1291,8 @@ const AgentManagerContent: Component = () => { if (restored) setLocalSessionIDs(restored) // Recover sessions collapsed state from extension-persisted state if (state.sessionsCollapsed !== undefined) setSessionsCollapsed(state.sessionsCollapsed) + // Recover sidebar collapsed state and mark hydrated so transitions enable + sidebar.hydrate(state.sidebarCollapsed) // Clear busy state for worktrees that have been removed const ids = new Set(state.worktrees.map((wt) => wt.id)) setBusyWorktrees((prev) => { @@ -1515,6 +1528,7 @@ const AgentManagerContent: Component = () => { const openReviewTab = () => { const sel = selection() if (sel === null) return + terms.setActiveId(undefined) setSidePanel(null) setReviewOpenForContext(sel, true) setReviewActive(true) @@ -1757,12 +1771,14 @@ const AgentManagerContent: Component = () => { const handleCreateWorktree = () => { if (!loaded()) return + expandSidebar() vscode.postMessage({ type: "agentManager.createWorktree" }) } // Advanced worktree dialog — opens a full dialog with prompt, versions, model, mode const showAdvancedWorktreeDialog = () => { if (!loaded()) return + expandSidebar() dialog.show(() => dialog.close()} defaultBaseBranch={repoDefaultBranch()} />) } @@ -1857,6 +1873,7 @@ const AgentManagerContent: Component = () => { const openLocally = (sid: string) => { saveTabMemory() + expandSidebar() const pending = activePendingId() if (pending) { setLocalSessionIDs((prev) => prev.map((id) => (id === pending ? sid : id))) @@ -1870,6 +1887,7 @@ const AgentManagerContent: Component = () => { const handleAddSession = () => { const sel = selection() + expandSidebar() if (sel === LOCAL) return addPendingTab() if (sel) { // Deactivate any focused terminal so the new session is visible. @@ -2110,8 +2128,17 @@ const AgentManagerContent: Component = () => { } return ( -
e.preventDefault()}> -
+
e.preventDefault()} + > +
{
- {/* Tab bar — visible when a section is selected and has tabs or a pending new session */} - + {/* Tab bar — full version with tabs renders when a section is selected + and has tabs; otherwise a minimal version still renders so the + sidebar toggle button stays at a fixed position. */} + +
+ +
+
+ } + > {
+ { /> - {/* Stacking container: the terminal layer is an absolutely- - positioned overlay on top of the chat. This sub-container - is position:relative so the overlay does not cover the - tab bar above (which lives outside this stack). */} + {/* Terminal overlay is scoped to the main pane so it does not cover the tab bar or side panel. */}
- {/* Terminal tabs are kept mounted regardless of visibility so - output keeps streaming across worktree switches without - detaching xterm from the paint tree. */} - {renderTerminalLayer({ state: terms })} - {/* Chat + side diff panel (hidden when review tab or terminal tab is active) */} + {/* Chat/terminal + side diff panel. Keep it mounted under the + review tab so live xterm canvases never leave the paint tree. */}
-
- { - if (localSessionIDs().includes(id)) { - session.selectSession(id) - if (selection() === null) setSelection(LOCAL) - return - } - // Navigate to owning worktree instead of forcing into local mode - if (worktreeSessionIds().has(id)) { - const ms = managedSessions().find((s) => s.id === id) - if (ms?.worktreeId) { - selectWorktree(ms.worktreeId) +
+ {/* Keep terminal tabs mounted so output streams across worktree switches. */} + {renderTerminalLayer({ state: terms })} +
+ { + if (localSessionIDs().includes(id)) { session.selectSession(id) - setReviewActive(false) + if (selection() === null) setSelection(LOCAL) return } - } - openLocally(id) - }} - onShowHistory={() => setHistory(true)} - onForkMessage={readOnly() ? undefined : handleForkSession} - readonly={readOnly()} - continueInWorktree={selection() === LOCAL} - promptBoxId={`agent-manager:${selection() ?? "unassigned"}`} - pendingSessionID={selection() === LOCAL ? activePendingId() : undefined} - /> - -
- - {t("agentManager.session.readonly")} - - -
-
+ // Navigate to owning worktree instead of forcing into local mode + if (worktreeSessionIds().has(id)) { + const ms = managedSessions().find((s) => s.id === id) + if (ms?.worktreeId) { + selectWorktree(ms.worktreeId) + session.selectSession(id) + setReviewActive(false) + return + } + } + openLocally(id) + }} + onShowHistory={() => setHistory(true)} + onForkMessage={readOnly() ? undefined : handleForkSession} + readonly={readOnly()} + continueInWorktree={selection() === LOCAL} + promptBoxId={`agent-manager:${selection() ?? "unassigned"}`} + pendingSessionID={selection() === LOCAL ? activePendingId() : undefined} + /> + +
+ + {t("agentManager.session.readonly")} + + +
+
+
@@ -3083,6 +3119,7 @@ const AgentManagerContent: Component = () => { }} onRevertFile={revertCtl.revert} revertingFiles={revertCtl.reverting()} + activeTerminalId={terms.activeId()} />
@@ -3113,6 +3150,7 @@ const AgentManagerContent: Component = () => { }} onRevertFile={revertCtl.revert} revertingFiles={revertCtl.reverting()} + activeTerminalId={terms.activeId()} onClose={closeReviewTab} />
diff --git a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx index 21e95375cfe..72de493aabd 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/DiffPanel.tsx @@ -54,6 +54,7 @@ interface DiffPanelProps { onOpenFile?: (relativePath: string, line?: number) => void onRevertFile?: (file: string) => void revertingFiles?: Set + activeTerminalId?: string } export const DiffPanel: Component = (props) => { @@ -305,6 +306,7 @@ export const DiffPanel: Component = (props) => { deleteComment, cancelDraft, labels: labels(), + activeTerminalId: props.activeTerminalId, }) } @@ -328,7 +330,14 @@ export const DiffPanel: Component = (props) => { const all = comments() if (all.length === 0) return window.dispatchEvent( - new MessageEvent("message", { data: { type: "appendReviewComments", comments: all, autoSend: true } }), + new MessageEvent("message", { + data: { + type: props.activeTerminalId ? "appendReviewCommentsToTerminal" : "appendReviewComments", + comments: all, + autoSend: true, + targetTerminalId: props.activeTerminalId, + }, + }), ) preserveScroll(() => setComments([])) props.onSendAll?.() diff --git a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx index 596fa54f465..6f8b8828f77 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/FullScreenDiffView.tsx @@ -59,6 +59,7 @@ interface FullScreenDiffViewProps { onOpenFile?: (relativePath: string, line?: number) => void onRevertFile?: (file: string) => void revertingFiles?: Set + activeTerminalId?: string /** Defaults to true. Hides the per-file Revert action when false. */ canRevert?: boolean /** Defaults to true. Disables comment creation and "Send all" when false. */ @@ -320,6 +321,7 @@ export const FullScreenDiffView: Component = (props) => deleteComment, cancelDraft, labels: labels(), + activeTerminalId: props.activeTerminalId, }) } @@ -336,7 +338,14 @@ export const FullScreenDiffView: Component = (props) => const all = comments() if (all.length === 0) return window.dispatchEvent( - new MessageEvent("message", { data: { type: "appendReviewComments", comments: all, autoSend: true } }), + new MessageEvent("message", { + data: { + type: props.activeTerminalId ? "appendReviewCommentsToTerminal" : "appendReviewComments", + comments: all, + autoSend: true, + targetTerminalId: props.activeTerminalId, + }, + }), ) preserveScroll(() => setComments([])) props.onSendAll?.() diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 5f3e40399ee..6099c173db7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -397,6 +397,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran if (pid && mid) setModel({ providerID: pid, modelID: mid }) }} placement="top-start" + portal={false} deferDismiss /> void; defaultBaseBran placement="top-start" flip={false} sameWidth + portal={false} deferDismiss class="am-compare-popover" trigger={ diff --git a/packages/kilo-vscode/webview-ui/agent-manager/SidebarToggleButton.tsx b/packages/kilo-vscode/webview-ui/agent-manager/SidebarToggleButton.tsx new file mode 100644 index 00000000000..bfd9503d1a1 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/SidebarToggleButton.tsx @@ -0,0 +1,29 @@ +import { IconButton } from "@kilocode/kilo-ui/icon-button" +import { Tooltip } from "@kilocode/kilo-ui/tooltip" +import { useLanguage } from "../src/context/language" + +interface Props { + collapsed: boolean + onClick: () => void +} + +/** + * Sidebar collapse/expand toggle rendered inside the tab bar's leading slot. + * Same pixel position in both states; only the icon's left-column fill flips + * to indicate state. + */ +export function SidebarToggleButton(props: Props) { + const { t } = useLanguage() + const label = () => (props.collapsed ? t("agentManager.sidebar.expand") : t("agentManager.sidebar.collapse")) + return ( + + + + ) +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css index d94558636cd..305359b3dc7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css +++ b/packages/kilo-vscode/webview-ui/agent-manager/agent-manager.css @@ -23,6 +23,38 @@ background: var(--surface-interactive-base); } +/* Collapsed sidebar: width animates closed; the layout-hydrated class delays + the transition until after the first state push so we don't see a flash on + initial render when the persisted state is "collapsed". */ +.am-layout-hydrated .am-sidebar { + transition: + width 180ms ease, + min-width 180ms ease, + padding 180ms ease, + border-right-width 180ms ease; +} + +.am-sidebar-collapsed { + min-width: 0; + padding-left: 0; + padding-right: 0; + border-right-width: 0; + overflow: hidden; + /* Pointer events, focus, and a11y tree exclusion are all handled by the + inert attribute on the element. No need for pointer-events: none. */ +} + +.am-sidebar-collapsed > [data-component="resize-handle"] { + display: none; +} + +/* Empty tab bar — rendered when no session is selected so the sidebar + toggle button sits in the same pixel position regardless of state. + Drops the leading slot's border-right because there's nothing after it. */ +.am-tab-bar-empty .am-tab-leading { + border-right: none; +} + /* Fixed local repo item */ .am-local-item { @@ -1618,16 +1650,38 @@ body.am-wt-dragging-active * { flex-direction: column; flex: 1; min-height: 0; + min-width: 0; isolation: isolate; } +.am-detail-content-hidden { + position: absolute; + inset: 0; + opacity: 0; + pointer-events: none; + z-index: 0; +} + .am-detail-split { flex-direction: row; } -.am-detail-split .am-chat-wrapper { +.am-main-pane { + display: flex; + flex-direction: column; flex: 1; + min-height: 0; min-width: 0; + position: relative; + isolation: isolate; +} + +.am-main-pane-terminal-active .am-chat-wrapper { + opacity: 0; + pointer-events: none; +} + +.am-detail-split .am-main-pane { border-right: 1px solid var(--border-weak-base); } @@ -4299,9 +4353,9 @@ body.vscode-high-contrast-light { * subtree and stop xterm's internal rAF render loop, causing the * "press Enter to see content" bug on worktree switches. * - * Stack: position:relative container hosting the chat, review, and - * terminal layer. Sits below the tab bar inside `.am-detail`. - * Layer: absolute-positioned overlay covering the stack. + * Stack: position:relative container hosting the main pane, review, + * and side panel. Sits below the tab bar inside `.am-detail`. + * Layer: absolute-positioned overlay covering the main pane. * `pointer-events: none` when no terminal is active, letting * clicks fall through to the chat behind it. * Slots: absolute-positioned and stacked inside the layer; only the @@ -4323,7 +4377,7 @@ body.vscode-high-contrast-light { opacity: 0; pointer-events: none; background: var(--vscode-terminal-background, #1e1e1e); - z-index: 1; + z-index: 30; /* Force a dedicated compositor layer so opacity flips do not re-lay-out the xterm canvases underneath. */ will-change: opacity; diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts index 449dcb4e950..6d0ba49475f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ar.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "محلي", + "agentManager.sidebar.collapse": "طي الشريط الجانبي", + "agentManager.sidebar.expand": "إظهار الشريط الجانبي", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "الجلسات", "agentManager.notGitRepo": "ليس مستودع git", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts index 0e0e36a5ccc..06af21198fa 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/br.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "local", + "agentManager.sidebar.collapse": "Recolher barra lateral", + "agentManager.sidebar.expand": "Mostrar barra lateral", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "SESSÕES", "agentManager.notGitRepo": "Não é um repositório git", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts index 098aeb80604..9a3a505be80 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/bs.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "lokalno", + "agentManager.sidebar.collapse": "Skupi bočnu traku", + "agentManager.sidebar.expand": "Prikaži bočnu traku", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "SESIJE", "agentManager.notGitRepo": "Nije git repozitorij", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts index 9a53d313ab2..45ca2d4d686 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/da.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "lokal", + "agentManager.sidebar.collapse": "Skjul sidebjælke", + "agentManager.sidebar.expand": "Vis sidebjælke", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "SESSIONER", "agentManager.notGitRepo": "Ikke et git-repository", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts index 658958fc0fc..abbe80bc4c7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/de.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "lokal", + "agentManager.sidebar.collapse": "Seitenleiste einklappen", + "agentManager.sidebar.expand": "Seitenleiste anzeigen", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "SITZUNGEN", "agentManager.notGitRepo": "Kein Git-Repository", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts index 082a29d9a91..782b69947a7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/en.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "local", + "agentManager.sidebar.collapse": "Collapse sidebar", + "agentManager.sidebar.expand": "Show sidebar", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "SESSIONS", "agentManager.notGitRepo": "Not a git repository", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts index 5ee42b117b8..8222560aa15 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/es.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "local", + "agentManager.sidebar.collapse": "Contraer barra lateral", + "agentManager.sidebar.expand": "Mostrar barra lateral", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "SESIONES", "agentManager.notGitRepo": "No es un repositorio git", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts index 66e3e2a7bcf..cd98f2c9cc1 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/fr.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "local", + "agentManager.sidebar.collapse": "Réduire la barre latérale", + "agentManager.sidebar.expand": "Afficher la barre latérale", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "SESSIONS", "agentManager.notGitRepo": "Ce n'est pas un dépôt git", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts index 4b1843043b9..a309686e066 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ja.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "ローカル", + "agentManager.sidebar.collapse": "サイドバーを折りたたむ", + "agentManager.sidebar.expand": "サイドバーを表示", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "セッション", "agentManager.notGitRepo": "gitリポジトリではありません", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts index e2cc019eb19..eb99b32efd7 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ko.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "로컬", + "agentManager.sidebar.collapse": "사이드바 접기", + "agentManager.sidebar.expand": "사이드바 표시", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "세션", "agentManager.notGitRepo": "git 저장소가 아닙니다", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts index 09f47b76e5f..00c0c118313 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/nl.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "lokaal", + "agentManager.sidebar.collapse": "Zijbalk inklappen", + "agentManager.sidebar.expand": "Zijbalk tonen", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "SESSIES", "agentManager.notGitRepo": "Geen git repository", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts index 9966510f9a7..347c89a4d9c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/no.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "lokal", + "agentManager.sidebar.collapse": "Skjul sidefelt", + "agentManager.sidebar.expand": "Vis sidefelt", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "ØKTER", "agentManager.notGitRepo": "Ikke et git-repositorium", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts index 14511d94582..e4f4160b297 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/pl.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "lokalne", + "agentManager.sidebar.collapse": "Zwiń pasek boczny", + "agentManager.sidebar.expand": "Pokaż pasek boczny", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "SESJE", "agentManager.notGitRepo": "Nie jest repozytorium git", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts index d6f8d9a8082..5ef80e41d14 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/ru.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "локальный", + "agentManager.sidebar.collapse": "Свернуть боковую панель", + "agentManager.sidebar.expand": "Показать боковую панель", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "СЕССИИ", "agentManager.notGitRepo": "Не является git-репозиторием", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts index 43260a7affb..057eb848eaa 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/th.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "ในเครื่อง", + "agentManager.sidebar.collapse": "ย่อแถบด้านข้าง", + "agentManager.sidebar.expand": "แสดงแถบด้านข้าง", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "เซสชัน", "agentManager.notGitRepo": "ไม่ใช่ git repository", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts index e0863aa3f57..a674fa96f97 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/tr.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "yerel", + "agentManager.sidebar.collapse": "Kenar çubuğunu daralt", + "agentManager.sidebar.expand": "Kenar çubuğunu göster", "agentManager.section.worktrees": "WORKTREE'LER", "agentManager.section.sessions": "OTURUMLAR", "agentManager.notGitRepo": "Bir git deposu değil", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts index e6e338e4258..e5b19bcd13f 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/uk.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "локальний", + "agentManager.sidebar.collapse": "Згорнути бічну панель", + "agentManager.sidebar.expand": "Показати бічну панель", "agentManager.section.worktrees": "РОБОЧІ ДЕРЕВА", "agentManager.section.sessions": "СЕСІЇ", "agentManager.notGitRepo": "Не є git-репозиторієм", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts index 26b4c666f8b..e047ef4db92 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zh.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "本地", + "agentManager.sidebar.collapse": "折叠侧边栏", + "agentManager.sidebar.expand": "显示侧边栏", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "会话", "agentManager.notGitRepo": "不是 git 仓库", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts index e1b9335685d..62fffe71911 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/i18n/zht.ts @@ -1,5 +1,7 @@ export const dict = { "agentManager.local": "本機", + "agentManager.sidebar.collapse": "收合側邊欄", + "agentManager.sidebar.expand": "顯示側邊欄", "agentManager.section.worktrees": "WORKTREES", "agentManager.section.sessions": "工作階段", "agentManager.notGitRepo": "不是 git 儲存庫", diff --git a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts index d61a8265efb..3ab7375bec3 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/navigate.ts @@ -12,6 +12,18 @@ export const LOCAL = "local" as const type NavResult = { action: "select"; id: string } | { action: typeof LOCAL } | { action: "none" } +type SessionLike = { id: string; parentID?: string | null; createdAt: string } + +export function filterUnassignedSessions( + sessions: T[], + worktree: Set, + local: Set, +): T[] { + return [...sessions] + .filter((s) => (s.parentID === undefined || s.parentID === null) && !worktree.has(s.id) && !local.has(s.id)) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) +} + export function resolveNavigation(direction: "up" | "down", current: string | undefined, ids: string[]): NavResult { // Determine current position: -1 = local, 0..N-1 = session index if (!current) { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts b/packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts index 3271f3ad854..3c46aac071c 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts +++ b/packages/kilo-vscode/webview-ui/agent-manager/review-annotations.ts @@ -35,6 +35,7 @@ interface AnnotationHandlers { deleteComment: (id: string) => void cancelDraft: () => void labels: AnnotationLabels + activeTerminalId?: string } function focusWhenConnected(el: HTMLTextAreaElement): void { @@ -269,7 +270,12 @@ export function buildReviewAnnotation( makeActionButton(handlers.labels.sendToChat, makeIcon("M1 1l14 7-14 7V9l10-1L1 7z"), () => { window.dispatchEvent( new MessageEvent("message", { - data: { type: "appendReviewComments", comments: [comment], autoSend: true }, + data: { + type: handlers.activeTerminalId ? "appendReviewCommentsToTerminal" : "appendReviewComments", + comments: [comment], + autoSend: true, + targetTerminalId: handlers.activeTerminalId, + }, }), ) handlers.deleteComment(comment.id) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/sidebar-collapse.ts b/packages/kilo-vscode/webview-ui/agent-manager/sidebar-collapse.ts new file mode 100644 index 00000000000..01d232f8680 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/agent-manager/sidebar-collapse.ts @@ -0,0 +1,38 @@ +import { createSignal } from "solid-js" +import type { WebviewMessage } from "../src/types/messages" + +export interface VsCodePoster { + postMessage: (msg: WebviewMessage) => void +} + +/** + * Encapsulates the sidebar collapsed signal, persistence postMessage, and + * a single-frame "hydrated" flag that gates the width transition so the + * initial render (after restart with a persisted-collapsed state) does not + * animate from open to closed. + */ +export function createSidebarCollapse(vscode: VsCodePoster) { + const [collapsed, setCollapsed] = createSignal(false) + const [hydrated, setHydrated] = createSignal(false) + + const persist = (next: boolean) => { + setCollapsed(next) + vscode.postMessage({ type: "agentManager.setSidebarCollapsed", collapsed: next }) + } + + return { + collapsed, + hydrated, + /** Apply state from extension push without re-broadcasting. */ + hydrate: (value: boolean | undefined) => { + if (value !== undefined) setCollapsed(value) + if (!hydrated()) requestAnimationFrame(() => setHydrated(true)) + }, + /** Ensure the sidebar is visible; no-op + no message when already open. */ + expand: () => { + if (collapsed()) persist(false) + }, + /** Toggle and persist. */ + toggle: () => persist(!collapsed()), + } +} diff --git a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx index 551e8c482f3..14cb63a4de8 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/terminal/TerminalTab.tsx @@ -19,6 +19,7 @@ import { UnicodeGraphemesAddon } from "@xterm/addon-unicode-graphemes" import "@xterm/xterm/css/xterm.css" import { useVSCode } from "../../src/context/vscode" import { useLanguage } from "../../src/context/language" +import { formatReviewCommentsMarkdown } from "../../src/utils/review-comment-markdown" interface Props { terminalId: string @@ -279,6 +280,14 @@ export const TerminalTab: Component = (props) => { pendingFrame = requestAnimationFrame(runRepaint) } const fontSub = vscode.onMessage((message) => { + if (message.type === "appendReviewCommentsToTerminal") { + if (message.targetTerminalId !== props.terminalId) return + const comments = message.comments + if (!Array.isArray(comments) || comments.length === 0) return + term.paste(`${formatReviewCommentsMarkdown(comments)}\n`) + return + } + const size = message.type === "fontSizeChanged" ? message.fontSize : message.type === "ready" ? message.fontSize : undefined if (size === undefined) return diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index 70e596921ad..9843ad7d7d9 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -144,6 +144,10 @@ export const DataBridge: Component<{ children: any }> = (props) => { vscode.postMessage({ type: "openExternal", url }) } + const openContent = (content: string, language?: string) => { + vscode.postMessage({ type: "openContent", content, language }) + } + const directory = () => { const dir = server.workspaceDirectory() if (!dir) return "" @@ -161,6 +165,7 @@ export const DataBridge: Component<{ children: any }> = (props) => { onOpenFile={open} onOpenDiff={openDiff} onOpenUrl={openUrl} + onOpenContent={openContent} > {props.children} diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx index 9acf713193d..91754c748fa 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx @@ -85,6 +85,8 @@ function TodoToolCard(props: { part: ToolPart }) { input={state()?.input ?? {}} metadata={state()?.metadata ?? {}} tool={props.part.tool} + partID={props.part.id} + callID={props.part.callID} output={state()?.output} status={state()?.status} defaultOpen diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx index 54971539405..3b0ff7e6734 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/TaskToolExpanded.tsx @@ -116,7 +116,15 @@ const TaskToolRenderer: Component = (props) => { return (
- +
diff --git a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx index 6011b7a3040..1513cc22781 100644 --- a/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/shared/ModelSelector.tsx @@ -96,6 +96,8 @@ export interface ModelSelectorBaseProps { favorites?: boolean /** Delay outside dismissal while the popover opens inside a dialog. */ deferDismiss?: boolean + /** Render inline instead of through a portal when nested in a dialog. */ + portal?: boolean } export const ModelSelectorBase: Component = (props) => { @@ -537,6 +539,7 @@ export const ModelSelectorBase: Component = (props) => { minHeight={200} placement={props.placement ?? "top-start"} deferDismiss={props.deferDismiss} + portal={props.portal} open={open()} onOpenChange={setOpen} triggerAs={Button} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts index 370cbf149c3..a7718ae35e4 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts @@ -243,6 +243,13 @@ export interface AppendReviewCommentsMessage { autoSend?: boolean } +export interface AppendReviewCommentsToTerminalMessage { + type: "appendReviewCommentsToTerminal" + comments: ReviewComment[] + autoSend?: boolean + targetTerminalId: string +} + export interface TriggerTaskMessage { type: "triggerTask" text: string @@ -512,6 +519,7 @@ export interface AgentManagerStateMessage { tabOrder?: Record worktreeOrder?: string[] sessionsCollapsed?: boolean + sidebarCollapsed?: boolean reviewDiffStyle?: "unified" | "split" reviewMarkdownRender?: boolean isGitRepo?: boolean @@ -957,6 +965,7 @@ export type ExtensionMessage = | SetChatBoxMessage | AppendChatBoxMessage | AppendReviewCommentsMessage + | AppendReviewCommentsToTerminalMessage | TriggerTaskMessage | VariantsLoadedMessage | CloudSessionDataLoadedMessage diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 935a034b9b5..51c37652c15 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -131,6 +131,12 @@ export interface OpenFileRequest { column?: number } +export interface OpenContentRequest { + type: "openContent" + content: string + language?: string +} + export interface CancelLoginRequest { type: "cancelLogin" } @@ -636,6 +642,12 @@ export interface SetSessionsCollapsedRequest { collapsed: boolean } +// Persist sidebar collapsed state +export interface SetSidebarCollapsedRequest { + type: "agentManager.setSidebarCollapsed" + collapsed: boolean +} + // Persist review diff style preference export interface SetReviewDiffStyleRequest { type: "agentManager.setReviewDiffStyle" @@ -1116,6 +1128,7 @@ export type WebviewMessage = | SetTabOrderRequest | SetWorktreeOrderRequest | SetSessionsCollapsedRequest + | SetSidebarCollapsedRequest | SetReviewDiffStyleRequest | SetReviewMarkdownRenderRequest | PersistVariantRequest @@ -1191,6 +1204,7 @@ export type WebviewMessage = | ToggleSectionCollapsedRequest | MoveToSectionRequest | MoveSectionRequest + | OpenContentRequest | AgentManagerTerminalCreateRequest | AgentManagerTerminalCloseRequest | AgentManagerTerminalResizeRequest diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index e1527387f9c..fc09a91294e 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -35,6 +35,7 @@ export interface BasicToolProps { animated?: boolean allowPendingToggle?: boolean // kilocode_change onSubtitleClick?: () => void + onOpenChange?: (open: boolean) => void // kilocode_change onTriggerClick?: JSX.EventHandlerUnion triggerHref?: string clickable?: boolean @@ -124,6 +125,7 @@ export function BasicTool(props: BasicToolProps) { if (props.hideDetails) return // kilocode_change if (props.locked && !value) return setState("open", value) + props.onOpenChange?.(value) // kilocode_change } const trigger = () => ( diff --git a/packages/ui/src/context/data.tsx b/packages/ui/src/context/data.tsx index f6c46c0f1ba..c5d913c2ee6 100644 --- a/packages/ui/src/context/data.tsx +++ b/packages/ui/src/context/data.tsx @@ -43,6 +43,8 @@ export type OpenDiffFn = (diff: { }) => void export type OpenUrlFn = (url: string) => void + +export type OpenContentFn = (content: string, language?: string) => void // kilocode_change // kilocode_change end export const { use: useData, provider: DataProvider } = createSimpleContext({ @@ -55,6 +57,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ onOpenFile?: OpenFileFn // kilocode_change onOpenDiff?: OpenDiffFn // kilocode_change onOpenUrl?: OpenUrlFn // kilocode_change + onOpenContent?: OpenContentFn // kilocode_change }) => { return { get store() { @@ -68,6 +71,7 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ openFile: props.onOpenFile, // kilocode_change openDiff: props.onOpenDiff, // kilocode_change openUrl: props.onOpenUrl, // kilocode_change + openContent: props.onOpenContent, // kilocode_change } }, })