Merge branch 'main' into nvidia-nim-origin-header
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Hide Task tool subagent sessions from Agent Manager session lists.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep the New Worktree model picker open when interacting with its search field.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/kilo-ui": patch
|
||||
---
|
||||
|
||||
Keep chat tool cards open or closed when streaming updates remount them.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
]
|
||||
|
||||
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 690 KiB |
|
After Width: | Height: | Size: 639 KiB |
|
After Width: | Height: | Size: 372 KiB |
|
After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 652 KiB |
|
After Width: | Height: | Size: 650 KiB |
|
After Width: | Height: | Size: 529 KiB |
|
After Width: | Height: | Size: 371 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
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
|
||||
|
||||
@@ -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": "كان هذا مفيدًا",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "役に立ちました",
|
||||
|
||||
@@ -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": "도움이 됐어요",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Это было полезно",
|
||||
|
||||
@@ -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": "สิ่งนี้มีประโยชน์",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Це було корисно",
|
||||
|
||||
@@ -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": "这有帮助",
|
||||
|
||||
@@ -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=履歴アクション
|
||||