Merge branch 'main' into nvidia-nim-origin-header

This commit is contained in:
Mark IJbema
2026-05-14 16:10:39 +02:00
committed by GitHub
173 changed files with 7429 additions and 359 deletions
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Hide Task tool subagent sessions from Agent Manager session lists.
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep the New Worktree model picker open when interacting with its search field.
+6
View File
@@ -0,0 +1,6 @@
---
"kilo-code": patch
"@kilocode/kilo-ui": patch
---
Keep chat tool cards open or closed when streaming updates remount them.
+5
View File
@@ -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.
@@ -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.
+6
View File
@@ -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-<version>.md`, or from the newest relevant report
@@ -87,7 +87,23 @@ export function BrowserFrame({
)}
</div>
{/* Content */}
<div style={{ lineHeight: 0 }}>{children}</div>
{/*
* 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.
*/}
<div className="browser-frame-content" style={{ lineHeight: 0 }}>
{children}
</div>
<style jsx>{`
.browser-frame-content :global(figure) {
margin: 0 !important;
}
.browser-frame-content :global(p) {
margin: 0;
}
`}</style>
</div>
{caption && (
<figcaption
@@ -0,0 +1,213 @@
import type { Node, Edge, Position } from "@xyflow/react"
import type { DiagramDefinition } from "./index"
const laneStyle = (rgb: string, color: string) => ({
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",
}
@@ -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<string, DiagramDefinition> = {
"bead-lifecycle": beadLifecycle,
"adversarial-loop": adversarialLoop,
"convoy-execution": convoyExecution,
"wasteland-federation": wastelandFederation,
"wanted-lifecycle": wantedLifecycle,
"claim-to-stamp": claimToStamp,
}
@@ -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)",
}
@@ -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",
}
@@ -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<HTMLDivElement | null>(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 <div ref={containerRef} style={{ display: "none" }} />
}
/**
* 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 (
<figure style={{ margin: "24px 0" }}>
@@ -77,25 +105,30 @@ export function FlowDiagram({ name, height = "400px" }: { name: string; height?:
overflow: "hidden",
}}
>
<ReactFlow
nodes={diagram.nodes}
edges={diagram.edges}
fitView
fitViewOptions={{ padding: 0.2 }}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={false}
panOnDrag={false}
panOnScroll={false}
zoomOnScroll={false}
zoomOnPinch={false}
zoomOnDoubleClick={false}
preventScrolling={false}
proOptions={{ hideAttribution: true }}
style={{ background: "#08080c" }}
>
<Background variant={BackgroundVariant.Dots} color="rgba(248,160,32,0.15)" gap={20} size={1} />
</ReactFlow>
<ReactFlowProvider>
<ReactFlow
nodes={diagram.nodes}
edges={diagram.edges}
fitView
fitViewOptions={{ padding: 0.15 }}
minZoom={0.1}
maxZoom={1}
nodesDraggable={false}
nodesConnectable={false}
elementsSelectable={false}
panOnDrag={false}
panOnScroll={false}
zoomOnScroll={false}
zoomOnPinch={false}
zoomOnDoubleClick={false}
preventScrolling={false}
proOptions={{ hideAttribution: true }}
style={{ background: "#08080c" }}
>
<Background variant={BackgroundVariant.Dots} color="rgba(248,160,32,0.15)" gap={20} size={1} />
<FitOnResize useReactFlow={useReactFlow} />
</ReactFlow>
</ReactFlowProvider>
</div>
{diagram.caption && (
<figcaption
+3 -2
View File
@@ -6,6 +6,7 @@ interface ImageProps {
width?: string
height?: string
caption?: string
compact?: boolean
}
// Helper to add 'px' to numeric values that don't have units
@@ -17,7 +18,7 @@ function addPxIfNeeded(value: string): string {
return value
}
export function Image({ src, alt, width, height, caption }: ImageProps) {
export function Image({ src, alt, width, height, caption, compact }: ImageProps) {
const imgStyle: React.CSSProperties = {
maxWidth: "100%",
height: "auto",
@@ -27,7 +28,7 @@ export function Image({ src, alt, width, height, caption }: ImageProps) {
if (height) imgStyle.height = addPxIfNeeded(height)
const figureStyle: React.CSSProperties = {
margin: "1.5rem 0",
margin: compact ? "0" : "1.5rem 0",
maxWidth: "100%",
overflow: "hidden",
}
@@ -0,0 +1,52 @@
import React, { Children, ReactElement, ReactNode, cloneElement, isValidElement } from "react"
interface ImageGalleryProps {
children: ReactNode
columns?: string
width?: string
}
interface GalleryImageProps {
compact?: boolean
}
function addPxIfNeeded(value: string) {
if (/^\d+(\.\d+)?$/.test(value)) return `${value}px`
return value
}
export function ImageGallery({ children, columns = "3", width = "220px" }: ImageGalleryProps) {
const count = Number(columns)
const cols = Number.isFinite(count) && count > 0 ? Math.min(Math.floor(count), 4) : 3
const size = addPxIfNeeded(width)
return (
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(auto-fit, minmax(min(100%, ${size}), ${size}))`,
gap: "1rem",
alignItems: "start",
justifyContent: "center",
margin: "1.5rem 0",
maxWidth: `calc(${cols} * ${size} + ${cols - 1}rem)`,
}}
>
{Children.map(children, (child) => {
const item = isValidElement<GalleryImageProps>(child)
? cloneElement(child as ReactElement<GalleryImageProps>, { compact: true })
: child
return (
<div
style={{
minWidth: 0,
}}
>
{item}
</div>
)
})}
</div>
)
}
+1
View File
@@ -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"
@@ -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" },
],
},
],
@@ -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",
},
},
}
+1
View File
@@ -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"
+2
View File
@@ -126,6 +126,7 @@ export default function MyApp({ Component, pageProps }: AppProps<MyAppProps>) {
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<MyAppProps>) {
<meta name="referrer" content="strict-origin" />
<meta name="title" content={title} />
<meta name="description" content={description} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
<link rel="icon" href="/docs/favicon/favicon.ico" sizes="48x48" type="image/x-icon" />
<link rel="shortcut icon" href="/docs/favicon/favicon.ico" />
<link rel="icon" href="/docs/favicon/favicon.svg" type="image/svg+xml" />
@@ -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 <org/db>` 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`.
<!-- TODO: verify — the "Create your own" entry point in the Connect dialog (WS-Admin 7) has not shipped yet. Currently the dialog uses a single linear flow. The split into "Join" and "Create" branches is planned. For now, creating a new wasteland uses the existing dialog with the upstream field populated manually, followed by `createUpstream` after the credential is stored. -->
### 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
<!-- TODO: verify — confirm whether Kilo Cloud offers a hosted wasteland creation flow or if self-serve is CLI/dashboard only -->
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**.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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 <!-- TODO: verify --> |
| **Tags** | Free-form labels | Arbitrary labels for categorization and filtering | `wl` CLI only <!-- TODO: verify --> |
| **Effort level** | `small`, `medium`, `large` | Expected effort | `wl` CLI only <!-- TODO: verify --> |
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 %}
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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
<!-- TODO: verify — confirm whether confidence is a separate numeric score or a categorical level in the current `wl accept` CLI -->
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 <id> --quality <q>` | PR is merged, stamp is created, item → `completed` |
| **Reject** | `wl reject <id> --reason "..."` | PR is closed, item → `claimed` (back to the claimer for rework) |
| **Close** | `wl close <id>` | 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 | ✅ | ✅ | ❌ <!-- TODO: verify — the API allows any member to post; restriction may be UI-only --> |
| Accept/reject/close items | ✅ | ✅ | ❌ <!-- TODO: verify — the API allows any member to accept/reject/close; restriction is enforced by the Review tab requiring owner+admin --> |
| 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 <id>` — 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?
<!-- TODO: verify — confirm whether there is a configuration option to accept/reject incoming reputation from other wasteland instances, or if reputation portability is always-on by protocol design -->
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 <handle>`).
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 `<owner>/<db>` repo on DoltHub.
@@ -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.
<!-- TODO: verify — confirm whether rig handle changes are disallowed or just discouraged -->
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.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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.
<!-- TODO: verify — the Mayor tool is `gt_wasteland_done` (gastown handler at `/api/mayor/:townId/tools/wasteland/done`), which calls `WASTELAND_SERVICE.markWantedItemDone`. Confirm this is the canonical tool name exposed to the Mayor's agent. -->
In the standalone `wl` CLI, you'd use `wl done <id> --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.
<!-- TODO: verify — confirm whether Gas Town adds a Creativity dimension on top of the open-source wl protocol's Quality/Reliability/Severity -->
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.
<!-- TODO: verify — confirm whether confidence is a numeric score or a categorical level; the schema accepts string or number -->
## 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.
<!-- TODO: verify — confirm exact validator permission model and how validators are assigned in admin settings -->
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/<rig-handle>/<wanted-id>` 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.
@@ -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.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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 %}
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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 %}
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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
@@ -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 <your-handle>`.
4. Click **Connect**.
Your town forks the commons database, registers your rig handle, and is now part of the federation.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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/<rig-handle>/<wanted-id>` 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.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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/<rig-handle>/<wanted-id>` branch. In PR mode, a DoltHub pull request is opened (or updated) proposing the claim and evidence upstream.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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 <id> --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.
<!-- TODO: verify — confirm whether Gas Town adds a Creativity dimension on top of the open-source wl protocol's Quality/Reliability/Severity -->
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 <handle>` 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
@@ -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.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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"*
<!-- TODO: verify — check cloud repo WastelandSettingsSection.tsx for filter UI that may have been added. CLI supports: --project, --type, --status, --priority, --limit, --search, --sort, --mine, --claimed-by, --posted-by -->
## 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. <!-- TODO: verify — confirm whether Gas Town adds a toggle per .plans/wasteland-gastown-poc.md workstream 4 -->
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 <id> --evidence "<url>"` 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.
@@ -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.
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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/<rig-handle>/<wanted-id>` 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
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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 <id> --evidence "<url>"` 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 <id> --evidence "<url>"` via the `gt_wasteland_done` tool
4. Evidence is pushed to your wasteland fork as a commit on the `wl/<rig-handle>/<wanted-id>` 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
<!-- TODO(screenshots): replace placeholder with real UI capture -->
{% 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`) |
<!-- TODO: verify — confirm whether confidence is a numeric score (1–5) or a categorical level in the current `wl accept` CLI, and whether reliability is set separately or always mirrors quality -->
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 = '<your-handle>'`.
- **`wl profile`** — Run `wl profile <handle>` 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 <id>`, 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. |
@@ -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)
@@ -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,
},
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c3f37193389ad7937c7b2103ad2fe3dd63123a0d4f60a0fbfd65b31d3982c092
size 12105
oid sha256:dbaab3a1ff10eac8f710b0ed8ddc99d96188c06a09c227e634ca712bcee12130
size 11543
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:74a42ec77a8ac8d5a1c555b46e3d550c9acf3198f45cc87da6582c0ff924d5f9
size 17677
oid sha256:7cabfff215cef485ec913b9a082aaac844275e506e88ba0fab112d95ff6de329
size 18200
+4
View File
@@ -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": "كان هذا مفيدًا",
+4
View File
@@ -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",
+4
View File
@@ -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",
+5 -1
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+4
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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": "役に立ちました",
+5 -1
View File
@@ -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": "도움이 됐어요",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+4
View File
@@ -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",
+5 -1
View File
@@ -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": "Это было полезно",
+4
View File
@@ -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": "สิ่งนี้มีประโยชน์",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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": "Це було корисно",
+4
View File
@@ -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": "这有帮助",
+4
View File
@@ -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": "這有幫助",
@@ -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",
@@ -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()
}
@@ -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<String, String> {
val obj = this?.get(key)?.jsonObject ?: return emptyMap()
return obj.entries.mapNotNull { (name, value) ->
@@ -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)
@@ -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<RuntimeException> {
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<IllegalStateException> {
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)
}
}
@@ -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<ProviderList200Response>(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<ProviderList200Response>(src)
assertEquals(1, obj.all.size)
@@ -132,7 +133,8 @@ class GeneratedApiModelSerializationTest {
}
}],
"default": {},
"connected": []
"connected": [],
"failed": []
}"""
val obj = json.decodeFromString<ProviderList200Response>(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<ProviderList200Response>(src)
assertTrue(obj.all.isEmpty())
}
@Test
fun `provider list defaults omitted collection fields`() {
val obj = json.decodeFromString<ProviderList200Response>("{}")
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<List<AppSkills200ResponseInner>>("[]")
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<ProviderList200Response>(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<ProviderList200Response>(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<ProviderList200Response>(src)
val model = obj.all[0].models["big-model"]!!
assertEquals(1000000.0, model.limit.context)
assertEquals(32000.0, model.limit.output)
}
}
@@ -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
// ================================================================
@@ -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<ProviderList200Response>(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<ProviderList200Response>(src)
assertEquals(1, obj.all.size)
@@ -115,7 +116,8 @@ class ProjectModelSerializationTest {
}
}],
"default": {},
"connected": []
"connected": [],
"failed": []
}"""
val obj = json.decodeFromString<ProviderList200Response>(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<ProviderList200Response>(src)
assertTrue(obj.all.isEmpty())
}
@Test
fun `provider list defaults omitted failed field`() {
val src = """{"all":[],"default":{},"connected":[]}"""
val obj = json.decodeFromString<ProviderList200Response>(src)
assertTrue(obj.failed.isEmpty())
}
// ------ Agent ------
@Test
@@ -270,4 +280,18 @@ class ProjectModelSerializationTest {
val list = json.decodeFromString<List<AppSkills200ResponseInner>>("[]")
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<ProviderList200Response>(src)
assertEquals(listOf("openai", "gemini"), obj.failed)
}
}
@@ -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<Session>(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<Session>(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<Session>(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<SessionStatus>(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<Map<String, SessionStatus>>("{}")
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<Session>(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<Session>(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<SessionStatus>(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<SessionStatus>(src)
assertEquals(0L, obj.attempt)
assertEquals(0L, obj.next)
}
}
@@ -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
@@ -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)) {
@@ -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) }
}
}
@@ -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) }
}
}
@@ -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)
}
}
@@ -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) }
@@ -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() {
@@ -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()
@@ -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<LocalHistoryItem>()
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<String>()
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) {
@@ -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<LocalHistoryItem>,
val cloudItems: List<CloudHistoryItem>,
) {
val selectedLocal: List<LocalHistoryItem> get() = if (source == HistorySource.LOCAL) localItems else emptyList()
}
object HistoryDataKeys {
val SELECTION: DataKey<HistorySelection> = DataKey.create("ai.kilocode.client.session.history.HistorySelection")
val CONTROLLER: DataKey<HistoryController> = DataKey.create("ai.kilocode.client.session.history.HistoryController")
}
@@ -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<T : HistoryItem>(
private val model: HistoryModel<T>,
@@ -31,14 +30,14 @@ internal open class HistoryRenderer<T : HistoryItem>(
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<HistoryItem>, index: Int): String? {
@@ -48,13 +47,6 @@ internal open class HistoryRenderer<T : HistoryItem>(
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<T : HistoryItem>(
}
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<T : HistoryItem>(
)
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<LocalHistoryItem>) : HistoryRenderer<LocalHistoryItem>(model, deletable = true)
@@ -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 <T> clicked(list: JBList<T>, e: MouseEvent): T? {
internal fun <T> itemAt(list: JBList<T>, 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)
}
@@ -48,6 +48,12 @@ open class HistoryModel<T : HistoryItem> : AbstractListModel<T>() {
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<T : HistoryItem> : AbstractListModel<T>() {
filter()
}
fun hasFilter(): Boolean = query.isNotEmpty()
fun refresh() {
val end = size.coerceAtLeast(1) - 1
fireContentsChanged(this, 0, end)
@@ -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<out HistoryItem>, 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<LocalHistoryItem>, 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<LocalHistoryItem>() else emptyList()
val cloud = if (source == HistorySource.CLOUD) cloudList.selectedValuesList.filterIsInstance<CloudHistoryItem>() else emptyList()
return HistorySelection(source, local, cloud)
}
return null
}
private fun installContextMenu(list: JBList<out HistoryItem>) {
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<LocalHistoryItem>) {
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<LocalHistoryItem>()
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<out HistoryItem> = 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
@@ -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
}
@@ -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))
}
@@ -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 {
@@ -58,5 +58,25 @@
<keyboard-shortcut keymap="$default"
first-keystroke="alt S"/>
</action>
<action id="Kilo.Session.Open"
class="ai.kilocode.client.actions.OpenSessionAction">
<keyboard-shortcut keymap="$default" first-keystroke="ENTER"/>
</action>
<action id="Kilo.Session.Rename"
class="ai.kilocode.client.actions.RenameSessionAction"
use-shortcut-of="RenameElement"/>
<action id="Kilo.Session.Delete"
class="ai.kilocode.client.actions.DeleteSessionAction"
use-shortcut-of="$Delete"/>
<group id="Kilo.History.ContextMenu">
<reference ref="Kilo.Session.Open"/>
<separator/>
<reference ref="Kilo.Session.Rename"/>
<reference ref="Kilo.Session.Delete"/>
</group>
</actions>
</idea-plugin>
@@ -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
@@ -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=إجراءات السجل
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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=履歴アクション

Some files were not shown because too many files have changed in this diff Show More