Merge pull request #12422 from Kilo-Org/johnnyeric/fix-memory-ui-feedback

fix(memory): refine CLI and extension experience
This commit is contained in:
Johnny Eric Amancio
2026-07-21 12:27:03 +02:00
committed by GitHub
72 changed files with 907 additions and 2124 deletions
+8
View File
@@ -0,0 +1,8 @@
---
"@kilocode/cli": patch
"@kilocode/kilo-memory": patch
"@kilocode/kilo-ui": patch
"kilo-code": patch
---
Simplify project memory settings and activity visibility, replace direct editing with folder inspection, add nested memory slash-command completion and status views, improve empty-project handling, compact native tool-call summaries, and remove legacy memory audit logs.
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a89d1a9a31f7de1780d772b7caf71d96f0b4b895df954c77dcd508a7febe7c9
size 1950
@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:431a0c12cb844b5d4e535cc4c7bf5546fd9e66a178b8023b00978d07c291a531
size 3919
+6 -20
View File
@@ -2,13 +2,12 @@ export const MEMORY_COMMAND_CATALOG = [
{ usage: "on", description: "Enable project memory" },
{ usage: "off", description: "Disable project memory" },
{ usage: "status", description: "Storage location and stored memory overview" },
{ usage: "show", description: "Full audit view (sources, index, changes, decisions)" },
{ usage: "show", description: "Stored project memory overview" },
{ usage: "remember <text>", description: "Save a project memory note" },
{ usage: "correct <text>", description: "Save a correction to project memory" },
{ usage: "forget <query>", description: "Remove matching project memory" },
{ usage: "auto on|off", description: "Turn automatic memory saves on or off" },
{ usage: "verbose on|off", description: "Turn verbose memory details on or off" },
{ usage: "edit", description: "Open project.md in $VISUAL/$EDITOR, then rebuild" },
{ usage: "inspect", description: "Reveal the project memory folder" },
{ usage: "rebuild", description: "Rebuild the memory index from source files" },
{ usage: "purge confirm", description: "Delete all project memory files" },
] as const
@@ -18,7 +17,7 @@ export const MEMORY_USAGE = `/memory [project] ${MEMORY_COMMAND_CATALOG.map((ite
export const MEMORY_OPERATIONS = [
"enable",
"status",
"edit",
"inspect",
"disable",
"rebuild",
"remember",
@@ -26,21 +25,13 @@ export const MEMORY_OPERATIONS = [
"forget",
"purge",
"auto",
"verbose",
] as const
export const MEMORY_PROMPT_OPERATIONS = ["remember", "forget"] as const
export type MemoryOperation = (typeof MEMORY_OPERATIONS)[number]
export type MemoryPromptOperation = (typeof MEMORY_PROMPT_OPERATIONS)[number]
export function isMemoryOperation(input: unknown): input is MemoryOperation {
return typeof input === "string" && (MEMORY_OPERATIONS as readonly string[]).includes(input)
}
export function isMemoryPromptOperation(input: unknown): input is MemoryPromptOperation {
return typeof input === "string" && (MEMORY_PROMPT_OPERATIONS as readonly string[]).includes(input)
}
type Help = {
kind: "help"
}
@@ -62,7 +53,7 @@ type Operation =
}
| {
kind: "operation"
operation: "auto" | "verbose"
operation: "auto"
mode: "on" | "off"
}
| {
@@ -72,7 +63,7 @@ type Operation =
}
| {
kind: "operation"
operation: Exclude<MemoryOperation, "remember" | "correct" | "forget" | "purge" | "auto" | "verbose">
operation: Exclude<MemoryOperation, "remember" | "correct" | "forget" | "purge" | "auto">
}
type Usage = {
@@ -104,7 +95,7 @@ function usage(reason: string): ParsedMemoryCommand {
function operation(verb: string, text: string): ParsedMemoryCommand | undefined {
if (verb === "on" || verb === "enable") return { kind: "operation", operation: "enable" }
if (verb === "off" || verb === "disable") return { kind: "operation", operation: "disable" }
if (verb === "status" || verb === "edit" || verb === "rebuild") {
if (verb === "status" || verb === "inspect" || verb === "rebuild") {
return { kind: "operation", operation: verb }
}
if (verb === "purge") {
@@ -116,11 +107,6 @@ function operation(verb: string, text: string): ParsedMemoryCommand | undefined
if (mode === "on" || mode === "off") return { kind: "operation", operation: "auto", mode }
return usage("Missing auto mode. Run /memory auto on or /memory auto off.")
}
if (verb === "verbose") {
const mode = text.toLowerCase()
if (mode === "on" || mode === "off") return { kind: "operation", operation: "verbose", mode }
return usage("Missing verbose mode. Run /memory verbose on or /memory verbose off.")
}
if (verb === "remember") {
if (text) return { kind: "operation", operation: "remember", text }
return usage("Missing text.")
+3 -1
View File
@@ -46,7 +46,9 @@ export namespace KiloMemory {
}
export async function prepare(input: Input) {
return root(input)
const dir = root(input)
await MemoryFiles.cleanup(dir)
return dir
}
export async function status(input: Input) {
+6 -51
View File
@@ -1,13 +1,7 @@
import { appendFile, chmod } from "fs/promises"
import path from "path"
import z from "zod"
import { MemoryFs } from "./fs"
import { MemoryPaths } from "./paths"
import { MemoryRedact } from "../capture/redact"
export namespace MemoryAudit {
const MAX_LOG = 128_000
const LOG_MARGIN = 16_000
const Log = z
.object({
kind: z.literal("log"),
@@ -49,45 +43,10 @@ export namespace MemoryAudit {
}[]
}
function cap(input: string) {
if (Buffer.byteLength(input) <= MAX_LOG) return input
const lines = input.split("\n").reverse()
const kept: string[] = []
lines.reduce((sum, line) => {
if (sum >= MAX_LOG) return sum
kept.push(line)
return sum + Buffer.byteLength(`${line}\n`)
}, 0)
return kept.reverse().join("\n")
}
async function line(file: string, text: string) {
await MemoryFs.dir(path.dirname(file))
const info = await MemoryFs.guard(file)
if (info && !info.isFile()) throw new Error(`memory path is not a file: ${file}`)
await appendFile(file, text, { mode: MemoryFs.FILE })
await chmod(file, MemoryFs.FILE).catch((error: unknown) => {
if (process.platform === "win32") return
throw error
})
const next = await MemoryFs.guard(file)
if (!next?.isFile()) throw new Error(`memory path is not a file: ${file}`)
if (next.size <= MAX_LOG + LOG_MARGIN) return
await MemoryFs.write(file, cap((await MemoryFs.read(file)) ?? ""))
}
async function audit(root: string, input: Decision) {
const data = MemoryRedact.value(input) as Decision
await MemoryFs.queue(root, () =>
line(
MemoryPaths.files(root).decisions,
`${JSON.stringify({
v: 1,
time: new Date().toISOString(),
...data,
})}\n`,
),
)
function audit(root: string, input: Decision) {
void root
void input
return Promise.resolve()
}
export async function append(root: string, text: string) {
@@ -99,12 +58,8 @@ export namespace MemoryAudit {
}
export async function readDecisions(root: string) {
return MemoryFs.read(MemoryPaths.files(root).decisions)
.then((text) => text ?? "")
.catch((error: unknown) => {
if (MemoryFs.miss(error)) return ""
throw error
})
void root
return ""
}
function record(input: string) {
+9
View File
@@ -109,6 +109,15 @@ export namespace MemoryFs {
return readFile(file, "utf8")
}
export async function remove(file: string) {
await parents(path.dirname(file))
const info = await guard(file)
if (!info) return false
if (!info.isFile()) throw new Error(`memory path is not a file: ${file}`)
await rm(file, { force: true })
return true
}
export async function json(file: string) {
const text = await read(file)
return text === undefined ? undefined : JSON.parse(text)
+38 -2
View File
@@ -10,6 +10,9 @@ import { MemoryText } from "../text"
import { MemoryTopics } from "../recall/topics"
export namespace MemoryState {
const CLEAN_LIMIT = 128
const CLEAN_RETRY_MS = 60_000
const cleaned = new Map<string, number>()
const seed: Record<MemorySchema.Source, string> = {
"project.md": "# Project Memory\n\n## Facts\n\n## Decisions\n\n## Constraints\n\n## Open Questions\n",
"environment.md": "# Environment Memory\n\n## Commands\n\n## Paths\n\n## Tooling\n",
@@ -92,6 +95,38 @@ export namespace MemoryState {
)
}
export async function cleanup(root: string) {
const retry = cleaned.get(root)
if (retry !== undefined && retry > Date.now()) return false
cleaned.delete(root)
const owns = await owned(root).catch((error: unknown) => {
MemoryFs.warn("failed to inspect legacy memory audit", { error, root })
return undefined
})
if (owns !== true) {
if (owns === undefined) cache(root, Date.now() + CLEAN_RETRY_MS)
return false
}
const removed = await MemoryFs.remove(MemoryPaths.files(root).decisions).catch((error: unknown) => {
MemoryFs.warn("failed to remove legacy memory audit", { error, root })
return undefined
})
if (removed === undefined) {
cache(root, Date.now() + CLEAN_RETRY_MS)
return false
}
cache(root, Number.POSITIVE_INFINITY)
return removed
}
function cache(root: string, retry: number) {
cleaned.delete(root)
cleaned.set(root, retry)
if (cleaned.size <= CLEAN_LIMIT) return
const key = cleaned.keys().next().value
if (typeof key === "string") cleaned.delete(key)
}
export async function readIndex(root: string) {
const file = MemoryPaths.files(root).index
return MemoryFs.read(file)
@@ -218,8 +253,9 @@ export namespace MemoryState {
index: await readIndex(root),
inventory,
items: await inspect(root, inventory),
changes: await MemoryAudit.readChanges(root),
decisions: await MemoryAudit.readDecisions(root),
// Retain empty fields for wire compatibility while the legacy audit file is removed.
changes: "",
decisions: "",
}
}
@@ -27,6 +27,7 @@ export namespace MemoryFiles {
export const indexExpired = MemoryState.indexExpired
export const scaffold = MemoryState.scaffold
export const owned = MemoryState.owned
export const cleanup = MemoryState.cleanup
export const writeSession = MemorySessions.writeSession
export const readSession = MemorySessions.readSession
+3 -29
View File
@@ -26,10 +26,10 @@
"operation": "status"
},
{
"name": "edit operation",
"input": "/memory edit",
"name": "inspect operation",
"input": "/memory inspect",
"result": "operation",
"operation": "edit"
"operation": "inspect"
},
{
"name": "on operation",
@@ -94,26 +94,6 @@
"operation": "auto",
"mode": "off"
},
{
"name": "verbose mode usage",
"input": "/memory verbose",
"result": "usage",
"reason": "Missing verbose mode"
},
{
"name": "verbose on operation",
"input": "/memory verbose on",
"result": "operation",
"operation": "verbose",
"mode": "on"
},
{
"name": "verbose off operation",
"input": "/memory verbose off",
"result": "operation",
"operation": "verbose",
"mode": "off"
},
{
"name": "remember operation keeps text",
"input": "/memory remember use bun test from packages/opencode",
@@ -148,12 +128,6 @@
"operation": "auto",
"mode": "off"
},
{
"name": "inspect action is unknown",
"input": "/memory inspect",
"result": "usage",
"reason": "Unknown memory action"
},
{
"name": "unknown action",
"input": "/memory wat",
+19 -2
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { parseMemoryCommand, type MemoryOperation, type ParsedMemoryCommand } from "../src/commands"
import { MEMORY_USAGE, parseMemoryCommand, type MemoryOperation, type ParsedMemoryCommand } from "../src/commands"
type Case = {
name: string
@@ -29,7 +29,7 @@ function expected(item: Case): ParsedMemoryCommand | undefined {
if (!item.query) throw new Error(`Missing query for fixture: ${item.name}`)
return { kind: "operation", operation: item.operation, query: item.query }
}
if (item.operation === "auto" || item.operation === "verbose") {
if (item.operation === "auto") {
if (!item.mode) throw new Error(`Missing mode for fixture: ${item.name}`)
return { kind: "operation", operation: item.operation, mode: item.mode }
}
@@ -41,6 +41,23 @@ function expected(item: Case): ParsedMemoryCommand | undefined {
}
describe("memory commands", () => {
test("does not expose verbose mode", () => {
expect(MEMORY_USAGE).not.toContain("verbose")
expect(parseMemoryCommand("/memory verbose on")).toEqual({
kind: "usage",
reason: "Unknown memory action: verbose.",
})
})
test("replaces edit with inspect", () => {
expect(MEMORY_USAGE).toContain("inspect")
expect(MEMORY_USAGE).not.toContain("edit")
expect(parseMemoryCommand("/memory edit")).toEqual({
kind: "usage",
reason: "Unknown memory action: edit.",
})
})
test("parse shared fixtures", () => {
for (const item of cases) {
const parsed = parseMemoryCommand(item.input)
+116 -60
View File
@@ -1,4 +1,4 @@
import { describe, expect, test } from "bun:test"
import { describe, expect, spyOn, test } from "bun:test"
import { mkdir, mkdtemp, readdir, rm, symlink, utimes, writeFile } from "fs/promises"
import os from "os"
import path from "path"
@@ -10,6 +10,7 @@ import { MemoryOperations } from "../src/capture/operations"
import { MemoryPaths } from "../src/storage/paths"
import { MemoryRecall } from "../src/recall/recall"
import { MemorySchema } from "../src/schema"
import { KiloMemory } from "../src/effect/index"
async function tmp() {
const dir = await mkdtemp(path.join(os.tmpdir(), "kilo-memory-"))
@@ -43,13 +44,119 @@ describe("memory core package", () => {
expect(shown.sources.corrections).toContain("## Corrections")
expect(await Bun.file(path.join(t.root, ".gitignore")).text()).toBe("*\n!.gitignore\n")
expect(shown.index).toBe("")
expect(shown.changes).toBe("")
expect(shown.decisions).toBe("")
expect(await Bun.file(path.join(t.root, "decisions.jsonl")).exists()).toBe(false)
})
})
test("prepare removes legacy decisions once from owned memory roots", async () => {
await use(async (t) => {
await Memory.enable({ root: t.root })
const legacy = path.join(t.root, "decisions.jsonl")
await writeFile(legacy, '{"kind":"log"}\n')
await KiloMemory.status({ root: t.root })
expect(await Bun.file(legacy).exists()).toBe(false)
await writeFile(legacy, '{"kind":"log"}\n')
await KiloMemory.status({ root: t.root })
expect(await Bun.file(legacy).exists()).toBe(true)
const other = path.join(t.dir, "unowned")
await mkdir(other)
const file = path.join(other, "decisions.jsonl")
await writeFile(file, '{"kind":"log"}\n')
await KiloMemory.status({ root: other })
expect(await Bun.file(file).exists()).toBe(true)
})
})
test("prepare ignores legacy decisions cleanup failures", async () => {
const clock = spyOn(Date, "now")
const now = Date.now()
clock.mockReturnValue(now)
try {
await use(async (t) => {
await Memory.enable({ root: t.root })
const legacy = path.join(t.root, "decisions.jsonl")
await mkdir(legacy)
const status = await KiloMemory.status({ root: t.root })
expect(status.state.enabled).toBe(true)
await rm(legacy, { recursive: true })
await writeFile(legacy, '{"kind":"log"}\n')
await KiloMemory.status({ root: t.root })
expect(await Bun.file(legacy).exists()).toBe(true)
clock.mockReturnValue(now + 60_001)
await KiloMemory.status({ root: t.root })
expect(await Bun.file(legacy).exists()).toBe(false)
})
} finally {
clock.mockRestore()
}
})
test("prepare ignores corrupt manifests during legacy cleanup", async () => {
const clock = spyOn(Date, "now")
const now = Date.now()
clock.mockReturnValue(now)
try {
await use(async (t) => {
await Memory.enable({ root: t.root })
const paths = MemoryPaths.files(t.root)
await writeFile(paths.manifest, "{")
const status = await KiloMemory.status({ root: t.root })
expect(status.state.enabled).toBe(true)
await writeFile(paths.manifest, '{"kind":"kilo-memory","version":1}\n')
await writeFile(paths.decisions, '{"kind":"log"}\n')
await KiloMemory.status({ root: t.root })
expect(await Bun.file(paths.decisions).exists()).toBe(true)
clock.mockReturnValue(now + 60_001)
await KiloMemory.status({ root: t.root })
expect(await Bun.file(paths.decisions).exists()).toBe(false)
})
} finally {
clock.mockRestore()
}
})
test("legacy cleanup cache evicts older roots", async () => {
await use(async (t) => {
const first = path.join(t.dir, "cache-0")
await mkdir(first)
await MemoryFiles.writeManifest(first)
await MemoryFiles.cleanup(first)
const legacy = MemoryPaths.files(first).decisions
await writeFile(legacy, '{"kind":"log"}\n')
for (let i = 1; i <= 128; i++) {
const root = path.join(t.dir, `cache-${i}`)
await mkdir(root)
await MemoryFiles.writeManifest(root)
await MemoryFiles.cleanup(root)
}
await MemoryFiles.cleanup(first)
expect(await Bun.file(legacy).exists()).toBe(false)
})
})
test("enable preserves existing memory settings", async () => {
await use(async (t) => {
const enabled = await Memory.enable({ root: t.root })
await MemoryFiles.writeState(t.root, { ...enabled.state, autoInject: false, autoConsolidate: false, verbose: true })
await MemoryFiles.writeState(t.root, {
...enabled.state,
autoInject: false,
autoConsolidate: false,
verbose: true,
})
const next = await Memory.enable({ root: t.root })
@@ -111,43 +218,7 @@ describe("memory core package", () => {
})
})
test("decision and change audit records redact secret-like text in one log", async () => {
await use(async (t) => {
const secret = "sk-abcdefghijklmnopqrstuvwxyz123456"
await Memory.enable({ root: t.root })
await MemoryFiles.decide(t.root, {
kind: "recall",
result: "skipped",
query: `check api_key=${secret}`,
skipped: [{ reason: "secret", text: `password=hunter2 ${secret}` }],
})
await MemoryFiles.append(t.root, `provider error "api_key": "${secret}"`)
const shown = await Memory.show({ root: t.root })
expect(shown.decisions).toContain("[redacted]")
expect(shown.decisions).toContain('"kind":"log"')
expect(shown.decisions).not.toContain(secret)
expect(shown.decisions).not.toContain("hunter2")
expect(shown.changes).toContain("[redacted]")
expect(shown.decisions).toContain("provider error")
})
})
test("targeted recall redacts query before decision truncation", async () => {
await use(async (t) => {
const secret = "sk-" + "a".repeat(40)
await Memory.enable({ root: t.root })
await Memory.recall({ root: t.root, query: "x".repeat(220) + secret })
const shown = await Memory.show({ root: t.root })
expect(shown.decisions).toContain("[redacted]")
expect(shown.decisions).not.toContain(secret)
expect(shown.decisions).not.toContain(secret.slice(0, 20))
})
})
test("stale locks are stolen before appending audit records", async () => {
test("stale locks are stolen before applying memory", async () => {
await use(async (t) => {
await Memory.enable({ root: t.root })
const lock = path.join(t.root, ".lock")
@@ -155,10 +226,10 @@ describe("memory core package", () => {
await mkdir(lock)
await utimes(lock, old, old)
await MemoryFiles.append(t.root, "after stale lock")
await Memory.apply({ root: t.root, ops: [{ action: "add", key: "after_lock", text: "Stale locks recover." }] })
const shown = await Memory.show({ root: t.root })
expect(shown.changes).toContain("after stale lock")
expect(shown.sources.project).toContain("after_lock")
})
})
@@ -174,7 +245,6 @@ describe("memory core package", () => {
expect(state.enabled).toBe(false)
expect(files.some((file) => file.startsWith("state.json.bad-"))).toBe(true)
expect(shown.inventory.items).toEqual({})
expect(shown.changes).toContain("recover state.json")
})
})
@@ -280,7 +350,6 @@ describe("memory core package", () => {
const shown = await Memory.show({ root: t.root })
expect(mixed.result.added).toBe(1)
// The skip record is redacted: it flows into the persistent decisions audit.
expect(mixed.result.skipped).toContainEqual({ reason: "secret", text: "[redacted]" })
expect(JSON.stringify(mixed.result.skipped)).not.toContain("sk-abcdefghijklmnopqrstuvwxyz")
expect(shown.sources.project).toContain("safe_fact")
@@ -335,7 +404,6 @@ describe("memory core package", () => {
])
expect(shown.sources.project).not.toContain("memory_echo")
expect(shown.index).not.toContain("memory_echo")
expect(shown.decisions).toContain('"reason":"self_referential"')
})
})
@@ -372,17 +440,10 @@ describe("memory core package", () => {
expect(shown.sources.project).not.toContain("Vim keybindings")
expect(shown.index).toContain("repo_style")
expect(shown.index).not.toContain("reply_style")
expect(shown.decisions).toContain('"reason":"out_of_scope"')
expect(shown.decisions).not.toContain("reply_style")
expect(shown.decisions).not.toContain("theme")
expect(shown.decisions).not.toContain("editor")
expect(shown.decisions).not.toContain("I prefer terse summaries")
expect(shown.decisions).not.toContain("dark mode")
expect(shown.decisions).not.toContain("Vim keybindings")
})
})
test("out-of-scope secret ops stay out of the operations audit", async () => {
test("out-of-scope secret ops stay out of memory", async () => {
await use(async (t) => {
await Memory.enable({ root: t.root })
@@ -393,9 +454,8 @@ describe("memory core package", () => {
const shown = await Memory.show({ root: t.root })
expect(result.result.skipped).toEqual([{ reason: "out_of_scope", text: "My preference is [redacted]" }])
expect(shown.decisions).toContain('"reason":"out_of_scope"')
expect(shown.decisions).not.toContain("private_pref")
expect(shown.decisions).not.toContain("password=hunter2")
expect(shown.sources.project).not.toContain("private_pref")
expect(shown.sources.project).not.toContain("password=hunter2")
})
})
@@ -478,7 +538,7 @@ describe("memory core package", () => {
})
})
test("targeted recall returns typed memory and audits matched files", async () => {
test("targeted recall returns typed memory and matched files", async () => {
await use(async (t) => {
await Memory.enable({ root: t.root })
await Memory.remember({
@@ -490,12 +550,8 @@ describe("memory core package", () => {
})
const result = await Memory.recall({ root: t.root, query: "what command runs cli tests?" })
const shown = await Memory.show({ root: t.root })
expect(result.result?.block).toContain("cli_tests")
expect(result.files).toEqual(["environment.md"])
expect(shown.decisions).toContain('"kind":"recall"')
expect(shown.decisions).toContain('"result":"recalled"')
})
})
@@ -80,7 +80,7 @@ function run(input: {
}
describe("MemoryCapture (fake ports)", () => {
test("turn-close typed LLM saves environment memory and audit records", async () => {
test("turn-close typed LLM saves environment memory", async () => {
const t = await tmp()
try {
await KiloMemory.enable({ root: t.root })
@@ -102,9 +102,6 @@ describe("MemoryCapture (fake ports)", () => {
const shown = await KiloMemory.show({ root: t.root })
expect(shown.sources.environment).toContain("cli_memory_tests")
expect(shown.decisions).toContain('"kind":"digest"')
expect(shown.decisions).toContain('"kind":"typed"')
expect(shown.decisions).toContain('"result":"saved"')
} finally {
await t.done()
}
@@ -138,9 +135,6 @@ describe("MemoryCapture (fake ports)", () => {
const shown = await KiloMemory.show({ root: t.root })
expect(shown.sources.environment).toContain("cli_tests")
expect(shown.sources.environment).not.toContain(secret)
expect(shown.decisions).toContain('"reason":"secret"')
// The audit record itself must not carry the raw secret (decisions are exposed via /memory/show).
expect(shown.decisions).not.toContain(secret)
const detail = events.find((item) => item.detail?.type === "saved")?.detail
expect(detail?.message).toContain("environment.md:cli_tests")
expect(detail?.message).not.toContain(secret)
@@ -173,18 +167,15 @@ describe("MemoryCapture (fake ports)", () => {
sessionID: "ses_effect",
max: MemorySchema.maxStoredDigestSummary,
})
const shown = await KiloMemory.show({ root: t.root })
expect(saved?.summary).toContain("[redacted]")
expect(saved?.summary).not.toContain(secret)
expect(saved?.summary).not.toContain(secret.slice(0, 20))
expect(shown.decisions).not.toContain(secret)
expect(shown.decisions).not.toContain(secret.slice(0, 20))
} finally {
await t.done()
}
})
test("turn-close surfaces content-gate rejections in the audit with redacted text", async () => {
test("turn-close rejects self-referential content while applying safe operations", async () => {
const t = await tmp()
try {
await KiloMemory.enable({ root: t.root })
@@ -206,10 +197,6 @@ describe("MemoryCapture (fake ports)", () => {
expect(result).toMatchObject({ skipped: false, operationCount: 1 })
const shown = await KiloMemory.show({ root: t.root })
expect(shown.sources.project).not.toContain("gate_check")
// The apply-time content gate is visible in the audit, and its recorded text is redacted.
expect(shown.decisions).toContain('"reason":"self_referential"')
expect(shown.decisions).toContain("[redacted]")
expect(shown.decisions).not.toContain("password=hunter2")
} finally {
await t.done()
}
@@ -346,7 +333,7 @@ describe("MemoryCapture (fake ports)", () => {
}
})
test("interrupted close records a non-LLM fallback digest tagged with the reason", async () => {
test("interrupted close records a non-LLM fallback digest", async () => {
const t = await tmp()
try {
await KiloMemory.enable({ root: t.root })
@@ -367,9 +354,6 @@ describe("MemoryCapture (fake ports)", () => {
const raw = await Bun.file(path.join(MemoryPaths.files(t.root).sessions, file)).text()
expect(saved?.fallback).toBe(true)
expect(raw).toContain("Fallback: true")
const shown = await KiloMemory.show({ root: t.root })
expect(shown.decisions).toContain("session digest fallback on interrupted")
expect(shown.decisions).toContain('"fallback":true')
} finally {
await t.done()
}
@@ -457,7 +441,7 @@ describe("MemoryCapture (fake ports)", () => {
}
})
test("template echo digest output falls back and records template_echo", async () => {
test("template echo digest output falls back", async () => {
const t = await tmp()
try {
await KiloMemory.enable({ root: t.root })
@@ -473,16 +457,13 @@ describe("MemoryCapture (fake ports)", () => {
})
const saved = await MemoryFiles.readSession(t.root, { sessionID: "ses_effect", max: 480 })
const shown = await KiloMemory.show({ root: t.root })
expect(saved?.fallback).toBe(true)
expect(shown.decisions).toContain('"reason":"template_echo"')
expect(shown.decisions).toContain('"fallback":true')
} finally {
await t.done()
}
})
test("empty digest output falls back and records empty_digest", async () => {
test("empty digest output falls back", async () => {
const t = await tmp()
try {
await KiloMemory.enable({ root: t.root })
@@ -498,11 +479,8 @@ describe("MemoryCapture (fake ports)", () => {
})
const saved = await MemoryFiles.readSession(t.root, { sessionID: "ses_effect", max: 480 })
const shown = await KiloMemory.show({ root: t.root })
expect(saved?.fallback).toBe(true)
expect(saved?.summary).toContain("User:")
expect(shown.decisions).toContain('"reason":"empty_digest"')
expect(shown.decisions).toContain('"fallback":true')
} finally {
await t.done()
}
@@ -603,7 +581,7 @@ describe("MemoryCapture (fake ports)", () => {
}
})
test("records audit when configured memory model is unavailable", async () => {
test("configured memory model fallback still captures memory", async () => {
const t = await tmp()
try {
await KiloMemory.enable({ root: t.root })
@@ -620,8 +598,8 @@ describe("MemoryCapture (fake ports)", () => {
}),
})
const shown = await KiloMemory.show({ root: t.root })
expect(shown.changes).toContain("memory_model_config reason=model unavailable fallback=1")
const saved = await MemoryFiles.readSession(t.root, { sessionID: "ses_effect", max: 480 })
expect(saved?.summary).toContain("Explored repo setup")
} finally {
await t.done()
}
@@ -7,11 +7,15 @@
}
[data-slot="basic-tool-tool-info"] {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
font-size: var(--kilo-font-size-12);
text-align: start;
}
[data-slot="basic-tool-tool-info-structured"] {
width: 100%;
min-width: 0;
overflow: hidden;
}
@@ -25,6 +29,7 @@
}
[data-slot="basic-tool-tool-info-main"] {
width: 100%;
align-items: baseline;
overflow: hidden;
}
@@ -34,6 +39,7 @@
}
[data-slot="basic-tool-tool-subtitle"] {
flex: 1 1 auto;
font-size: var(--kilo-font-size-12);
color: var(--text-weak);
}
@@ -51,6 +57,8 @@
}
[data-slot="basic-tool-tool-arg"] {
flex: 0 1 auto;
max-width: 24ch;
font-size: var(--kilo-font-size-12);
color: var(--text-weak);
}
@@ -1099,7 +1099,7 @@ function McpTool(props: ToolProps) {
if (typeof value === "boolean") return [`${key}=${value}`]
return []
})
.slice(0, 3)
.slice(0, 1)
}
const formatted = createMemo(() => {
+1 -1
View File
@@ -3962,7 +3962,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
detail,
})
}
void this.memory.fetch(sessionID, false)
void this.memory.fetch(sessionID)
}
return
}
+84 -111
View File
@@ -1,11 +1,5 @@
import * as vscode from "vscode"
import * as path from "node:path"
import {
isMemoryOperation,
isMemoryPromptOperation,
type MemoryOperation,
type MemoryPromptOperation,
} from "@kilocode/kilo-memory/commands"
import { isMemoryOperation, type MemoryOperation } from "@kilocode/kilo-memory/commands"
import { MemorySchema } from "@kilocode/kilo-memory/schema"
import type { KiloClient, Session } from "@kilocode/sdk/v2/client"
import { retry } from "../services/cli-backend/retry"
@@ -14,6 +8,7 @@ import { getErrorMessage } from "../kilo-provider-utils"
type MemorySourceFile = MemorySchema.Source
type MemoryApi = KiloClient["memory"]
const CACHE_LIMIT = 8
const STORED_LIMIT = 16
const NO_PROJECT = "No active project for memory. Open a file in the target folder to manage its memory."
export type KiloProviderMemoryMessage = {
@@ -53,6 +48,20 @@ function memory(client: KiloClient | undefined): MemoryApi | undefined {
return (client as { memory?: MemoryApi } | undefined)?.memory
}
function count(text: string) {
return text.split("\n").filter((line) => line.trim().startsWith("- ")).length
}
function stored(text: string) {
return text
.split("\n")
.filter((line) => line.trim())
.map((line) => {
const marker = line.indexOf(":: ")
return marker === -1 ? line : line.slice(marker + 3)
})
}
function request(input: Record<string, unknown>): { value: KiloProviderMemoryMessage } | { error: string } {
const op = operation(input.operation)
if (!op) return { error: "Unknown memory operation" }
@@ -102,14 +111,16 @@ export class KiloProviderMemory {
async handle(message: Record<string, unknown>): Promise<boolean> {
if (message.type === "requestMemory") {
this.fetch(
typeof message.sessionID === "string" ? message.sessionID : undefined,
message.includeSources === true,
).catch((err: unknown) => console.error("[Kilo New] fetchAndSendMemory failed:", err))
this.fetch(typeof message.sessionID === "string" ? message.sessionID : undefined).catch((err: unknown) =>
console.error("[Kilo New] fetchAndSendMemory failed:", err),
)
return true
}
if (message.type === "memoryShow") {
await this.show(typeof message.sessionID === "string" ? message.sessionID : undefined)
await this.show(
typeof message.sessionID === "string" ? message.sessionID : undefined,
message.mode === "status" ? "status" : "show",
)
return true
}
if (message.type === "memoryOperation") {
@@ -127,17 +138,11 @@ export class KiloProviderMemory {
await this.run(parsed.value)
return true
}
if (message.type === "memoryPrompt") {
const op = isMemoryPromptOperation(message.operation) ? message.operation : undefined
if (!op) return true
await this.prompt(op, typeof message.sessionID === "string" ? message.sessionID : undefined)
return true
}
return false
}
fetch(sessionID?: string, includeSources = false): Promise<void> {
return this.serial(() => this.load(sessionID, includeSources))
fetch(sessionID?: string): Promise<void> {
return this.serial(() => this.load(sessionID))
}
/** Resolves once the serialized operation queue has drained. */
@@ -145,7 +150,7 @@ export class KiloProviderMemory {
return this.tail
}
private async load(sessionID?: string, includeSources = false): Promise<void> {
private async load(sessionID?: string): Promise<void> {
try {
const directory = this.input.dir(sessionID ?? this.input.session()?.id)
const client = this.input.client()
@@ -168,14 +173,10 @@ export class KiloProviderMemory {
}
const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true }))
const show = includeSources
? (await retry(() => api.show({ directory }, { throwOnError: true }))).data
: undefined
const msg = {
type: "memoryLoaded",
sessionID,
status,
...(show ? { show } : {}),
}
this.cache(directory, msg)
this.input.post(msg)
@@ -189,27 +190,11 @@ export class KiloProviderMemory {
}
}
async prompt(value: MemoryPromptOperation, sessionID?: string): Promise<void> {
const title = value === "remember" ? "Remember in project memory" : "Forget project memory"
const placeHolder = value === "remember" ? "Project fact, command, or correction" : "Text to remove"
const text = await vscode.window.showInputBox({ title, placeHolder, ignoreFocusOut: true })
if (!text?.trim()) {
// Clear the webview's pending state for this action when the input is dismissed.
this.input.post({ type: "memoryOperationResult", operation: value, sessionID, ok: true })
return
}
await this.run({
operation: value,
sessionID,
...(value === "remember" ? { text: text.trim() } : { query: text.trim() }),
})
show(sessionID?: string, mode: "status" | "show" = "show"): Promise<void> {
return this.serial(() => this.doShow(sessionID, mode))
}
show(sessionID?: string): Promise<void> {
return this.serial(() => this.doShow(sessionID))
}
private async doShow(sessionID?: string): Promise<void> {
private async doShow(sessionID: string | undefined, mode: "status" | "show"): Promise<void> {
const client = this.input.client()
if (!client) {
this.input.post({
@@ -237,55 +222,56 @@ export class KiloProviderMemory {
this.input.post({ type: "memoryLoaded", sessionID, error: NO_PROJECT })
return
}
const { data: show } = await retry(() => api.show({ directory }, { throwOnError: true }))
const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true }))
const current = sessionID ?? this.input.session()?.id
const startup =
current && status.state.stats.lastInjectedSessionID === current ? status.state.stats.lastInjectedTokens : 0
const content = [
"# Kilo Memory",
"",
`Root: ${show.root}`,
`Enabled: ${show.state.enabled ? "yes" : "no"}`,
`Auto-save: ${show.state.autoConsolidate ? "on" : "off"}`,
`Startup context: ${show.state.autoInject ? "on" : "off"}`,
`Stored index tokens: ${status.index.estimatedTokens}`,
`Startup context tokens for this session: ${startup}`,
`Last auto-save model usage: ${status.state.stats.lastConsolidationTokens} tokens`,
"",
"## project.md",
show.sources.project.trim(),
"",
"## environment.md",
show.sources.environment.trim(),
"",
"## corrections.md",
show.sources.corrections.trim(),
"",
"## index.kmem",
show.index.trim(),
"",
"## items",
show.items.trim(),
"",
"## changes",
show.changes.trim(),
"",
"## decisions.jsonl",
show.decisions.trim(),
"",
].join("\n")
await vscode.workspace
.openTextDocument({ content, language: "markdown" })
.then((doc) => vscode.window.showTextDocument(doc, { preview: true }))
const [{ data: show }, { data: status }] = await Promise.all([
retry(() => api.show({ directory }, { throwOnError: true })),
retry(() => api.status({ directory }, { throwOnError: true })),
])
const msg = {
type: "memoryLoaded",
sessionID,
status,
show,
}
this.cache(directory, msg)
this.input.post(msg)
const items = stored(show.items)
if (mode === "show" && items.length === 0) {
void vscode.window.showInformationMessage(
"This project doesn't have any memory yet. It will start showing after you use Kilo.",
)
return
}
const entries: vscode.QuickPickItem[] = [
{
label: `${status.state.enabled ? "Enabled" : "Disabled"} · ${status.state.scope}`,
description: status.state.autoConsolidate ? "Auto-save on" : "Auto-save off",
},
{ label: "Storage", detail: status.root },
{
label: "Sources",
description: `project.md ${count(show.sources.project)} · environment.md ${count(show.sources.environment)} · corrections.md ${count(show.sources.corrections)}`,
},
{
label: "Index",
description: `${status.index.estimatedTokens.toLocaleString()} estimated tokens`,
},
]
if (mode === "show") {
const shown = items.slice(0, STORED_LIMIT)
entries.push(
{
label: "Stored memory",
description:
shown.length < items.length ? `${shown.length} of ${items.length} shown` : `${shown.length} shown`,
},
...shown.map((label) => ({ label })),
)
}
void vscode.window.showQuickPick(entries, {
title: mode === "show" ? "Memory" : "Memory status",
placeHolder: mode === "show" ? "Stored project memory" : "Project memory status",
matchOnDescription: true,
matchOnDetail: true,
})
} catch (err) {
console.error("[Kilo New] KiloProvider: Failed to show memory:", err)
this.input.post({
@@ -358,31 +344,28 @@ export class KiloProviderMemory {
return false
}
const data = await this.action(api, directory, message)
const refreshed = await Promise.all([
retry(() => api.status({ directory }, { throwOnError: true })),
retry(() => api.show({ directory }, { throwOnError: true })),
]).catch((err: unknown) => {
console.warn("[Kilo New] Memory changed but refresh failed:", err)
return undefined
})
const status = refreshed?.[0].data
const show = refreshed?.[1].data
const refreshed =
message.operation === "status"
? { data }
: await retry(() => api.status({ directory }, { throwOnError: true })).catch((err: unknown) => {
console.warn("[Kilo New] Memory changed but refresh failed:", err)
return undefined
})
const status = refreshed?.data
const result = {
type: "memoryOperationResult",
operation: message.operation,
sessionID: message.sessionID,
ok: true,
...(status ? { status } : {}),
...(show ? { show } : {}),
result: data,
}
this.input.post(result)
if (status && show) {
if (status) {
const loaded = {
type: "memoryLoaded",
sessionID: message.sessionID,
status,
show,
}
this.cache(directory, loaded)
this.input.post(loaded)
@@ -409,12 +392,11 @@ export class KiloProviderMemory {
const op = message.operation
if (op === "enable") return (await api.enable({ directory }, { throwOnError: true })).data
if (op === "status") return (await api.status({ directory }, { throwOnError: true })).data
if (op === "edit") return this.edit(api, directory)
if (op === "inspect") return this.inspect(api, directory)
if (op === "disable") return (await api.disable({ directory }, { throwOnError: true })).data
if (op === "rebuild") return (await api.rebuild({ directory }, { throwOnError: true })).data
if (op === "purge") return this.purge(api, directory, message)
if (op === "auto") return this.auto(api, directory, message)
if (op === "verbose") return this.verbose(api, directory, message)
if (op === "remember") return this.remember(api, directory, message)
if (op === "correct") return this.correct(api, directory, message)
return this.forget(api, directory, message)
@@ -460,12 +442,10 @@ export class KiloProviderMemory {
return (await api.forget({ directory, query, sessionID: message.sessionID }, { throwOnError: true })).data
}
private async edit(api: MemoryApi, directory: string) {
private async inspect(api: MemoryApi, directory: string) {
const { data: status } = await retry(() => api.status({ directory }, { throwOnError: true }))
if (!status.state.enabled) throw new Error("Memory is disabled. Run /memory on first.")
const uri = vscode.Uri.file(path.join(status.root, "project.md"))
const doc = await vscode.workspace.openTextDocument(uri)
await vscode.window.showTextDocument(doc, { preview: false })
await vscode.commands.executeCommand("revealFileInOS", vscode.Uri.file(status.root))
return status
}
@@ -481,11 +461,4 @@ export class KiloProviderMemory {
}
throw new Error("Auto-save mode is required")
}
private async verbose(api: MemoryApi, directory: string, message: KiloProviderMemoryMessage) {
if (message.mode === "on" || message.mode === "off") {
return (await api.configure({ directory, verbose: message.mode === "on" }, { throwOnError: true })).data
}
throw new Error("Verbose mode is required")
}
}
@@ -88,6 +88,7 @@ const mockVscode = {
tabGroups: { all: [] },
showTextDocument: async () => {},
showInformationMessage: async () => undefined,
showQuickPick: async () => undefined,
showErrorMessage: async () => undefined,
showWarningMessage: async () => undefined,
createTerminal: () => ({ show: noop, sendText: noop, dispose: noop }),
@@ -28,18 +28,6 @@ function status(root: string) {
}
}
function show(root: string) {
return {
root: `${root}/.kilo/memory`,
state: status(root).state,
sources: { project: "", environment: "", corrections: "" },
index: "",
items: "",
changes: "",
decisions: "",
}
}
describe("KiloProvider memory events", () => {
it("routes tracked background memory events to their session directory", async () => {
const calls: string[] = []
@@ -151,10 +139,6 @@ describe("KiloProvider memory events", () => {
calls.push(["disable", input.directory])
return { data: { root: `${input.directory}/.kilo/memory`, state: status(input.directory).state } }
},
show: async (input: { directory: string }) => {
calls.push(["show", input.directory])
return { data: show(input.directory) }
},
},
} as unknown as KiloClient
const posts: unknown[] = []
@@ -176,7 +160,6 @@ describe("KiloProvider memory events", () => {
["status", "/repo/project"],
["disable", "/repo/project"],
["status", "/repo/project"],
["show", "/repo/project"],
])
expect(posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", sessionID: "ses_active" }))
})
@@ -1,5 +1,6 @@
import { describe, expect, it } from "bun:test"
import { describe, expect, it, spyOn } from "bun:test"
import type { KiloClient } from "@kilocode/sdk/v2/client"
import * as vscode from "vscode"
import { KiloProviderMemory } from "../../src/kilo-provider/memory"
function subject(client: KiloClient | undefined) {
@@ -18,6 +19,7 @@ function status(root: string) {
root: `${root}/.kilo/memory`,
state: {
enabled: true,
scope: "project",
autoConsolidate: true,
stats: {
lastInjectedSessionID: "",
@@ -42,6 +44,89 @@ function show(root: string) {
}
describe("KiloProviderMemory", () => {
it("shows stored memory and explains empty projects", async () => {
const picker = spyOn(vscode.window, "showQuickPick")
const notice = spyOn(vscode.window, "showInformationMessage")
const full = status("/repo")
const view = show("/repo")
view.items = "record id=project.md:Facts:test :: Stored memory fact :: with context"
const stored = subject({
memory: {
show: async () => ({ data: view }),
status: async () => ({ data: full }),
},
} as unknown as KiloClient)
const empty = subject({
memory: {
show: async () => ({ data: show("/empty") }),
status: async () => ({ data: status("/empty") }),
},
} as unknown as KiloClient)
try {
await stored.memory.show("ses_stored")
await empty.memory.show("ses_empty")
expect(picker).toHaveBeenCalledTimes(1)
expect(picker.mock.calls[0]?.[0]).toContainEqual(
expect.objectContaining({ label: "Storage", detail: "/repo/.kilo/memory" }),
)
expect(picker.mock.calls[0]?.[0]).toContainEqual(
expect.objectContaining({ label: "Stored memory fact :: with context" }),
)
expect(notice).toHaveBeenCalledWith(
"This project doesn't have any memory yet. It will start showing after you use Kilo.",
)
} finally {
picker.mockRestore()
notice.mockRestore()
}
})
it("shows the stored memory total when the list is truncated", async () => {
const picker = spyOn(vscode.window, "showQuickPick")
const view = show("/repo")
view.items = Array.from({ length: 17 }, (_, i) => `- id=item-${i} :: Fact ${i}`).join("\n")
const item = subject({
memory: {
show: async () => ({ data: view }),
status: async () => ({ data: status("/repo") }),
},
} as unknown as KiloClient)
try {
await item.memory.show("ses_stored")
expect(picker.mock.calls[0]?.[0]).toContainEqual(
expect.objectContaining({ label: "Stored memory", description: "16 of 17 shown" }),
)
expect(picker.mock.calls[0]?.[0]).toHaveLength(21)
} finally {
picker.mockRestore()
}
})
it("routes inspect operations to the memory folder", async () => {
const reveal = spyOn(vscode.commands, "executeCommand")
const item = subject({
memory: {
status: async () => ({ data: status("/repo") }),
show: async () => ({ data: show("/repo") }),
},
} as unknown as KiloClient)
try {
await item.memory.run({ operation: "inspect", sessionID: "ses_inspect" })
expect(reveal).toHaveBeenCalledWith("revealFileInOS", expect.objectContaining({ fsPath: "/repo/.kilo/memory" }))
expect(item.posts).toContainEqual(
expect.objectContaining({ type: "memoryOperationResult", operation: "inspect", ok: true }),
)
} finally {
reveal.mockRestore()
}
})
it("handles clients without memory endpoints gracefully", async () => {
const item = subject({} as KiloClient)
@@ -117,7 +202,7 @@ describe("KiloProviderMemory", () => {
expect(posts[1]).toMatchObject({
type: "memoryLoaded",
sessionID: "ses_8",
show: { root: "/repo/ses_8/.kilo/memory" },
status: { root: "/repo/ses_8/.kilo/memory" },
})
})
@@ -158,34 +243,29 @@ describe("KiloProviderMemory", () => {
it("routes status operations without mutating memory", async () => {
const calls: string[] = []
const state = status("/repo")
const view = show("/repo")
const item = subject({
memory: {
status: async () => {
calls.push("status")
return { data: state }
},
show: async () => {
calls.push("show")
return { data: view }
},
},
} as unknown as KiloClient)
await item.memory.run({ operation: "status", sessionID: "ses_memory" })
expect(calls).toEqual(["status", "status", "show"])
expect(calls).toEqual(["status"])
expect(item.posts).toContainEqual(
expect.objectContaining({ type: "memoryOperationResult", operation: "status", ok: true }),
expect.objectContaining({ type: "memoryOperationResult", operation: "status", ok: true, result: state }),
)
expect(item.posts).toContainEqual(expect.objectContaining({ type: "memoryLoaded", status: state }))
})
it("routes auto-save, verbose, and purge operations with explicit payloads", async () => {
it("routes auto-save and purge operations with explicit payloads", async () => {
const calls: unknown[] = []
const state = status("/repo")
const view = show("/repo")
state.state.autoConsolidate = false
state.state.verbose = true
const item = subject({
memory: {
configure: async (input: unknown) => {
@@ -202,14 +282,12 @@ describe("KiloProviderMemory", () => {
} as unknown as KiloClient)
await item.memory.run({ operation: "auto", mode: "off", sessionID: "ses_memory" })
await item.memory.run({ operation: "verbose", mode: "on", sessionID: "ses_memory" })
await item.memory.run({ operation: "purge", confirm: true, sessionID: "ses_memory" })
expect(calls).toEqual([
["configure", { directory: "/repo", autoConsolidate: false }],
["configure", { directory: "/repo", verbose: true }],
["purge", { directory: "/repo", confirm: true }],
])
expect(item.posts.filter((post) => (post as { type?: string }).type === "memoryOperationResult")).toHaveLength(3)
expect(item.posts.filter((post) => (post as { type?: string }).type === "memoryOperationResult")).toHaveLength(2)
})
})
@@ -24,12 +24,22 @@ const DATA_CONTEXT_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/context/data
const MESSAGE_PART_FILE = path.join(MONOREPO_ROOT, "packages/ui/src/components/message-part.tsx")
const KILO_MESSAGE_PART_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/message-part.tsx")
const KILO_MESSAGE_HIGHLIGHT_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/message-highlight.ts")
const KILO_BASIC_TOOL_CSS_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/basic-tool.css")
const KILO_MESSAGE_PART_CSS_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/message-part.css")
const SHELL_ROLLING_FILE = path.join(MONOREPO_ROOT, "packages/kilo-ui/src/components/shell-rolling-results.tsx")
const ASSISTANT_MESSAGE_FILE = path.join(
MONOREPO_ROOT,
"packages/kilo-vscode/webview-ui/src/components/chat/AssistantMessage.tsx",
)
const TASK_HEADER_FILE = path.join(MONOREPO_ROOT, "packages/kilo-vscode/webview-ui/src/components/chat/TaskHeader.tsx")
const CONTEXT_TAB_FILE = path.join(
MONOREPO_ROOT,
"packages/kilo-vscode/webview-ui/src/components/settings/ContextTab.tsx",
)
const PROMPT_INPUT_FILE = path.join(
MONOREPO_ROOT,
"packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx",
)
const TRANSCRIPT_PARTS_FILE = path.join(MONOREPO_ROOT, "packages/kilo-vscode/webview-ui/src/utils/transcript-parts.ts")
const CHAT_LAYOUT_FILE = path.join(MONOREPO_ROOT, "packages/kilo-vscode/webview-ui/src/styles/chat-layout.css")
@@ -331,6 +341,55 @@ describe("AssistantMessage visible row contract (source)", () => {
it("uses the plan exit card only when plan metadata is renderable", () => {
expect(src).toContain("if (!planExitInfo(part)) return")
})
it("uses the native recall tool without a separate memory badge", () => {
const tools = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8")
expect(src).not.toContain("assistant-memory-badge")
expect(tools).toContain("ToolRegistry.render(part.tool) ?? McpTool")
})
})
describe("Native tool summary contract (source)", () => {
const tools = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8")
const css = fs.readFileSync(KILO_BASIC_TOOL_CSS_FILE, "utf-8")
it("shows one secondary argument while preserving complete expanded input", () => {
const start = tools.indexOf("const inputArgs")
const end = tools.indexOf("const formatted", start)
expect(tools.slice(start, end)).toContain(".slice(0, 1)")
expect(tools).toContain("JSON.stringify(props.input, null, 2)")
})
it("gives the primary label remaining width and bounds secondary arguments", () => {
expect(css).toMatch(/\[data-slot="basic-tool-tool-info"\][\s\S]*?flex: 1 1 auto;/)
expect(css).toMatch(/\[data-slot="basic-tool-tool-subtitle"\][\s\S]*?flex: 1 1 auto;/)
expect(css).toMatch(/\[data-slot="basic-tool-tool-arg"\][\s\S]*?max-width: 24ch;/)
})
})
describe("Memory control placement contract (source)", () => {
const header = fs.readFileSync(TASK_HEADER_FILE, "utf-8")
const settings = fs.readFileSync(CONTEXT_TAB_FILE, "utf-8")
const prompt = fs.readFileSync(PROMPT_INPUT_FILE, "utf-8")
it("keeps memory controls out of the task header", () => {
expect(header).not.toContain("useMemory")
expect(header).not.toContain('name="memory"')
})
it("shows storage inspection in settings without a manual rebuild action", () => {
expect(settings).toContain("settings.context.memory.storage.title")
expect(settings).toContain("settings.context.memory.status.enabledTokens")
expect(settings).toContain("memory.inspect()")
expect(settings).not.toContain("memory.rebuild()")
expect(settings).not.toContain("lastOperationCount")
expect(settings).not.toContain("sessionTokens")
})
it("expands bare memory commands into inline completion", () => {
expect(prompt).toContain('const value = "/memory "')
expect(prompt).toContain("slash.onInput(value, value.length)")
})
})
describe("Assistant transcript spacing contract (source)", () => {
@@ -1,89 +0,0 @@
import { describe, expect, it } from "bun:test"
import { MemoryMarkerMeta } from "@kilocode/kilo-memory/marker-meta"
import { addMemoryActivity, markerActivity } from "../../webview-ui/src/utils/memory-activity"
describe("memory activity", () => {
it("accumulates saved events with their message and source references", () => {
const items = addMemoryActivity(
[],
{
type: "saved",
message: "Saved project memory",
operationCount: 4,
added: 3,
removed: 1,
sources: ["project.md:kilo_colors"],
},
10,
)
expect(items).toEqual([
{
type: "saved",
at: 10,
tokens: 0,
count: 3,
items: ["Saved project memory"],
refs: ["project.md:kilo_colors"],
},
])
expect(addMemoryActivity(items, { type: "recalled" }, 20)).toEqual(items)
})
it("ignores removal-only events and caps saved activity", () => {
const removed = { type: "saved" as const, message: "Memory updated · 1 removed", added: 0, removed: 1 }
expect(addMemoryActivity([], removed, 10)).toEqual([])
const items = Array.from({ length: 60 }).reduce(
(all, _, at) => addMemoryActivity(all, { type: "saved", added: 1 }, at),
[] as ReturnType<typeof addMemoryActivity>,
)
expect(items).toHaveLength(50)
expect(items[0]?.at).toBe(10)
expect(items.at(-1)?.at).toBe(59)
})
it("decodes loaded and recalled markers for activity summaries", () => {
const loaded = markerActivity(
[
{
type: "text",
metadata: MemoryMarkerMeta.metadata(
{
type: "startup",
bytes: 10,
tokens: 42,
count: 1,
files: ["project.md"],
items: ["Use Kilo colors"],
},
true,
),
},
],
10,
)
const recalled = markerActivity(
[
{
type: "text",
metadata: MemoryMarkerMeta.metadata(
{
type: "recall",
bytes: 10,
tokens: 8,
count: 2,
files: ["project.md"],
items: ["Prefer dark mode"],
},
true,
),
},
],
20,
)
expect(loaded).toMatchObject({ type: "loaded", tokens: 42, count: 1, items: [] })
expect(recalled).toMatchObject({ type: "recalled", tokens: 8, count: 2, items: ["Prefer dark mode"] })
})
})
@@ -4,7 +4,7 @@ import { parseMemoryCommand, type ParsedMemoryCommand } from "../../webview-ui/s
type MemoryOperation =
| "enable"
| "status"
| "edit"
| "inspect"
| "disable"
| "rebuild"
| "remember"
@@ -12,7 +12,6 @@ type MemoryOperation =
| "forget"
| "purge"
| "auto"
| "verbose"
type Case = {
name: string
input: string
@@ -43,7 +42,7 @@ function expected(item: Case): ParsedMemoryCommand | undefined {
if (!item.query) throw new Error(`Missing query for fixture: ${item.name}`)
return { kind: "operation", operation: item.operation, query: item.query }
}
if (item.operation === "auto" || item.operation === "verbose") {
if (item.operation === "auto") {
if (!item.mode) throw new Error(`Missing mode for fixture: ${item.name}`)
return { kind: "operation", operation: item.operation, mode: item.mode }
}
@@ -31,6 +31,13 @@ const SCRIPT = `
state: { status: "completed", input: {}, output: "done", title: "Updated todos" },
},
{ id: "read-running", type: "tool", tool: "read", state: { status: "running", input: {} } },
{ id: "memory-running", type: "tool", tool: "kilo_memory_recall", state: { status: "running", input: {} } },
{
id: "memory-completed",
type: "tool",
tool: "kilo_memory_recall",
state: { status: "completed", input: {}, output: "memory", title: "Memory recalled" },
},
]
const visible = parts.filter((part) => isRenderable(part, message)).map((part) => part.id)
@@ -38,7 +45,14 @@ const SCRIPT = `
console.log("${FAIL}" + reason)
process.exit(2)
}
const expected = ["visible-text", "visible-reasoning", "todo-completed", "read-running"]
const expected = [
"visible-text",
"visible-reasoning",
"todo-completed",
"read-running",
"memory-running",
"memory-completed",
]
if (visible.length !== expected.length || visible.some((id, index) => id !== expected[index])) {
fail("did not exclude transcript-invisible parts")
}
@@ -27,6 +27,76 @@ function setup(sandbox: () => void, options: { enabled?: () => boolean; exclude?
}
describe("useSlashCommand sandbox action", () => {
it("opens project memory actions from the top-level command", () => {
const ctx = setup(() => {})
const state = { text: "/memory" }
const textarea = {
value: state.text,
setSelectionRange: () => {},
focus: () => {},
} as unknown as HTMLTextAreaElement
ctx.slash.onInput("/mem", 4)
expect(ctx.slash.results()).toContainEqual(
expect.objectContaining({ name: "memory", description: "Manage project memory", hints: ["mem"] }),
)
ctx.slash.select(ctx.slash.results()[0]!, textarea, (text) => (state.text = text))
expect(state.text).toBe("/memory ")
expect(ctx.slash.results().map((command) => command.name)).toContain("memory inspect")
ctx.dispose()
})
it("offers memory actions after the parent command", () => {
const ctx = setup(() => {})
ctx.slash.onInput("/memory ", 8)
expect(ctx.slash.results().map((command) => command.name)).toEqual([
"memory status",
"memory show",
"memory on",
"memory off",
"memory inspect",
"memory rebuild",
"memory remember",
"memory correct",
"memory forget",
"memory auto on",
"memory auto off",
"memory purge confirm",
])
ctx.dispose()
})
it("keeps nested memory actions out of root hint matching", () => {
const ctx = setup(() => {})
const nested = ctx.slash.commands().filter((command) => command.name.startsWith("memory "))
expect(nested.length).toBeGreaterThan(0)
expect(nested.every((command) => command.hints.length === 0)).toBe(true)
ctx.dispose()
})
it("completes nested memory actions and closes for free text", () => {
const ctx = setup(() => {})
const state = { text: "/mem rem" }
const textarea = {
value: state.text,
setSelectionRange: () => {},
focus: () => {},
} as unknown as HTMLTextAreaElement
ctx.slash.onInput(state.text, state.text.length)
expect(ctx.slash.results().map((command) => command.name)).toEqual(["memory remember"])
ctx.slash.select(ctx.slash.results()[0]!, textarea, (text) => (state.text = text))
expect(state.text).toBe("/memory remember ")
ctx.slash.onInput("/memory remember durable fact", 31)
expect(ctx.slash.show()).toBe(false)
ctx.dispose()
})
it("runs the sandbox toggle as a client command", () => {
const state = { toggles: 0, text: "/sandbox", prevented: 0 }
const ctx = setup(() => state.toggles++)
@@ -11,7 +11,6 @@ import { Component, For, Show, createMemo } from "solid-js"
import { Dynamic } from "solid-js/web"
import { Part, PART_MAPPING, ToolRegistry } from "@kilocode/kilo-ui/message-part"
import type { MessageFeedbackControls } from "@kilocode/kilo-ui/message-part"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import type {
AssistantMessage as SDKAssistantMessage,
Part as SDKPart,
@@ -23,11 +22,9 @@ import { useSession } from "../../context/session"
import { useDisplay } from "../../context/display"
import { useConfig } from "../../context/config"
import { useLanguage } from "../../context/language"
import { useMemory } from "../../context/memory"
import { useServer } from "../../context/server"
import { planDisplayPath } from "../../utils/plan-path"
import { isRenderable, UPSTREAM_SUPPRESSED_TOOLS } from "../../utils/transcript-parts"
import { MemoryMarkerMeta } from "@kilocode/kilo-memory/marker-meta"
import { color as timelineColor } from "../../utils/timeline/colors"
import type { Part as TimelinePart } from "../../types/messages"
import type { TimelineHighlight } from "../../utils/timeline/highlight"
@@ -117,8 +114,6 @@ type ToolStateProps = {
status?: string
}
type MemoryItem = MemoryMarkerMeta.Decoded
function TodoToolCard(props: { part: ToolPart; forceOpen?: boolean }) {
const render = ToolRegistry.render(props.part.tool)
const state = () => props.part.state as ToolStateProps
@@ -173,8 +168,6 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
const data = useData()
const session = useSession()
const display = useDisplay()
const mem = useMemory()
const language = useLanguage()
const { config } = useConfig()
const open = createMemo(() => config().terminal_command_display !== "collapsed")
const edit = createMemo(() => config().code_edit_display === "expanded")
@@ -189,33 +182,6 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
return !!matchToolRequest(part, "question", session.questions())
})
})
const meta = createMemo(() =>
MemoryMarkerMeta.fromParts((props.parts ?? data.store.part?.[props.message.id] ?? []) as MemoryMarkerMeta.Part[]),
)
const recall = createMemo(() => {
const item = meta()
if (item?.type === "recall") return item
})
const fmt = (value: number) => value.toLocaleString(language.locale())
const count = (item: MemoryItem) => fmt(item.count)
const items = (item: MemoryItem) => item.items ?? []
const verbose = createMemo(() => Boolean(mem.status()?.state.verbose))
const tip = (item: MemoryItem) => {
const values = MemoryMarkerMeta.snippets(item, verbose())
return (
<div style={{ "text-align": "left", "white-space": "normal", "max-width": "280px" }}>
<Show
when={values.length > 0}
fallback={
<div>{`${language.t("chat.memory.badge.recalled")} · ${language.t("chat.memory.badge.items", { count: count(item) })}`}</div>
}
>
<For each={values}>{(value) => <div>{value}</div>}</For>
</Show>
</div>
)
}
return (
<>
<For each={parts()}>
@@ -331,17 +297,6 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
)
}}
</For>
<Show when={mem.enabled() && recall()}>
{(item) => (
<Tooltip value={tip(item())} placement="top">
<div data-component="assistant-memory-badge">
{language.t("chat.memory.badge.recalled")} ·{" "}
{language.t("chat.memory.badge.items", { count: count(item()) })}
<Show when={verbose() && items(item()).length > 0}> · {items(item())[0]}</Show>
</div>
</Tooltip>
)}
</Show>
</>
)
}
@@ -75,7 +75,7 @@ import {
import { ReviewComments } from "./ReviewComments"
import { partReview, reviewBody } from "../../../../src/shared/review-comments"
import { isEnterKeyCommitNotIme } from "../../utils/ime-enter"
import { MEMORY_USAGE, parseMemoryCommand } from "../../utils/memory-command"
import { parseMemoryCommand } from "../../utils/memory-command"
import { useMemory } from "../../context/memory"
function mergeReviewComments(current: ReviewComment[], incoming: ReviewComment[]): ReviewComment[] {
@@ -986,8 +986,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return false
}
if (memory.kind === "help") {
showToast({ variant: "default", title: "/memory", description: MEMORY_USAGE })
return true
const value = "/memory "
setText(value)
if (textareaRef) {
textareaRef.value = value
textareaRef.setSelectionRange(value.length, value.length)
textareaRef.focus()
}
slash.onInput(value, value.length)
adjustHeight()
return false
}
if (isDisabled() || speech.active() || terminal.pending() || git.pending() || props.blocked?.()) return false
const status = projectMemory.status()
@@ -1000,13 +1008,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
showToast({ variant: "error", title: language.t("chat.memory.project.disabled") })
return false
}
if (memory.kind === "show") vscode.postMessage({ type: "memoryShow", sessionID: sid() })
if (memory.kind === "show") vscode.postMessage({ type: "memoryShow", mode: "show", sessionID: sid() })
if (memory.kind === "operation") {
if (memory.operation === "status") {
vscode.postMessage({ type: "memoryShow", mode: "status", sessionID: sid() })
return true
}
vscode.postMessage({
type: "memoryOperation",
operation: memory.operation,
sessionID: sid(),
...(memory.operation === "auto" || memory.operation === "verbose" ? { mode: memory.mode } : {}),
...(memory.operation === "auto" ? { mode: memory.mode } : {}),
...(memory.operation === "purge" ? { confirm: memory.confirm } : {}),
...(memory.operation === "remember" || memory.operation === "correct" ? { text: memory.text } : {}),
...(memory.operation === "forget" ? { query: memory.query } : {}),
@@ -13,9 +13,7 @@ import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { Icon } from "@kilocode/kilo-ui/icon"
import { Checkbox } from "@kilocode/kilo-ui/checkbox"
import { Switch } from "@kilocode/kilo-ui/switch"
import { useSession } from "../../context/session"
import { useMemory } from "../../context/memory"
import { calcTokenUsage, collapseCostBreakdown } from "../../context/session-utils"
import { useLanguage } from "../../context/language"
import { useVSCode } from "../../context/vscode"
@@ -26,10 +24,8 @@ import { TranscriptSearch } from "./TranscriptSearch"
import { useTranscriptSearch } from "../../context/transcript-search"
import { hasModelUsage, tokenSummary } from "../../context/model-usage"
import { SessionRenameEditor } from "../shared/SessionRenameEditor"
import { DeferredPopover } from "../shared/DeferredPopover"
import { target as todoTarget } from "../../context/todo-revert"
import type { Part, TodoItem, ExtensionMessage } from "../../types/messages"
import type { MemoryActivity } from "../../utils/memory-activity"
interface TaskHeaderProps {
readonly?: boolean
@@ -37,7 +33,6 @@ interface TaskHeaderProps {
export const TaskHeader: Component<TaskHeaderProps> = (props) => {
const session = useSession()
const memory = useMemory()
const language = useLanguage()
const search = useTranscriptSearch()
@@ -91,76 +86,6 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
return false
})
const memoryVerbose = createMemo(() => Boolean(memory.status()?.state.verbose))
const memoryActive = createMemo(() => {
if (!memory.enabled()) return false
const stats = memory.status()?.state.stats
return !!stats && stats.lastInjectedSessionID === session.currentSessionID() && stats.lastInjectedTokens > 0
})
const memoryStatus = createMemo(() => {
if (memory.error()) return memory.error()!
if (memory.loading()) return language.t("chat.memory.status.loading")
if (!memory.enabled()) return language.t("chat.memory.project.disabled")
if (memoryActive()) return language.t("chat.memory.status.active")
return language.t("chat.memory.project.enabled")
})
const activity = createMemo(() => [...memory.activity()].sort((a, b) => b.at - a.at))
const activityLines = createMemo(() => {
if (memory.error() || !memory.enabled()) return []
const loaded = activity().reduce((sum, item) => sum + (item.type === "loaded" ? item.tokens : 0), 0)
const recalled = activity().reduce((sum, item) => sum + (item.type === "recalled" ? item.count : 0), 0)
const saved = activity().reduce((sum, item) => sum + (item.type === "saved" ? item.count : 0), 0)
return [
...(loaded > 0
? [language.t("chat.memory.activity.loaded", { tokens: loaded.toLocaleString(language.locale()) })]
: []),
...(recalled > 0
? [language.t("chat.memory.activity.recalled", { count: recalled.toLocaleString(language.locale()) })]
: []),
...(saved > 0
? [language.t("chat.memory.activity.saved", { count: saved.toLocaleString(language.locale()) })]
: []),
]
})
const activityItems = createMemo(() =>
activity()
.flatMap((item) => {
const values =
item.type === "saved" ? [...item.refs, ...item.items] : item.items.length > 0 ? item.items : item.refs
return values.flatMap((value) => {
const text = value.trim()
return text ? [{ type: item.type, value: text }] : []
})
})
.slice(0, 5),
)
const activityLabel = (item: { type: MemoryActivity["type"]; value: string }) =>
language.t(`chat.memory.activity.${item.type}.item`, { item: item.value })
const activitySummaryView = () => (
<div data-slot="task-header-memory-activity">
<Show
when={activityLines().length > 0}
fallback={<div data-slot="task-header-memory-activity-summary">{language.t("chat.memory.activity.idle")}</div>}
>
<div data-slot="task-header-memory-activity-summary">
<For each={activityLines()}>{(line) => <div>{line}</div>}</For>
</div>
</Show>
</div>
)
const activityTooltip = () => (
<>
<div data-slot="task-header-context-tooltip-title">{language.t("settings.context.title")}</div>
<div data-slot="task-header-context-tooltip-status">{memoryStatus()}</div>
{activitySummaryView()}
<Show when={memoryVerbose() && activityItems().length > 0}>
<div data-slot="task-header-memory-activity-list">
<For each={activityItems()}>{(item) => <div>{activityLabel(item)}</div>}</For>
</div>
</Show>
</>
)
const vscode = useVSCode()
const [expanded, setExpanded] = createSignal(true)
@@ -311,121 +236,18 @@ export const TaskHeader: Component<TaskHeaderProps> = (props) => {
</Tooltip>
)}
</Show>
<Tooltip value={activityTooltip()} placement="bottom" contentClass="task-header-memory-tooltip">
<DeferredPopover
placement="bottom-end"
portal={false}
class="task-header-context-popover"
triggerAs="button"
triggerProps={{
type: "button",
class: "task-header-context-trigger",
get ["aria-label"]() {
return language.t("settings.context.title")
},
}}
trigger={
<>
<Icon name="server" size="small" />
<Show when={memoryActive()}>
<span data-slot="task-header-memory-dot" />
</Show>
</>
}
>
<div data-slot="task-header-context-menu">
<div data-slot="task-header-context-title">{language.t("settings.context.title")}</div>
<Show when={!props.readonly}>
<section data-slot="task-header-context-section">
<div data-slot="task-header-context-section-title">
<Icon name="compress" size="small" />
<span>{language.t("settings.context.compaction.title")}</span>
</div>
<div data-slot="task-header-context-actions">
<button
data-slot="task-header-context-action"
disabled={!canCompact()}
onClick={() => session.compact()}
>
{language.t("command.session.compact")}
</button>
</div>
</section>
</Show>
<section data-slot="task-header-context-section">
<div data-slot="task-header-context-section-title">
<Icon name="memory" size="small" />
<span>{language.t("settings.context.memory.title")}</span>
</div>
<div data-slot="task-header-memory-status">{memoryStatus()}</div>
{activitySummaryView()}
<div data-slot="task-header-context-actions">
<Show
when={memory.enabled()}
fallback={
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.enable()}
>
{language.t("chat.memory.enable")}
</button>
}
>
<button
data-slot="task-header-context-action"
disabled={memory.loading() || memory.pending()}
onClick={() => memory.showMemory()}
>
{language.t("chat.memory.inspect")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.remember()}
>
{language.t("chat.memory.remember")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.forget()}
>
{language.t("chat.memory.forget")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.rebuild()}
>
{language.t("chat.memory.rebuild")}
</button>
<button
data-slot="task-header-context-action"
disabled={memory.pending()}
onClick={() => memory.disable()}
>
{language.t("chat.memory.disable")}
</button>
</Show>
</div>
<Show when={memory.enabled()}>
<div data-slot="task-header-memory-verbose">
<span>{language.t("chat.memory.verbose")}</span>
<Switch
checked={memoryVerbose()}
disabled={memory.pending()}
hideLabel
onChange={(next) => memory.verbose(next ? "on" : "off")}
>
{language.t("chat.memory.verbose")}
</Switch>
</div>
</Show>
</section>
</div>
</DeferredPopover>
</Tooltip>
<Show when={!props.readonly}>
<Tooltip value={language.t("command.session.compact")} placement="bottom">
<IconButton
icon="compress"
size="small"
variant="ghost"
disabled={!canCompact()}
onClick={() => session.compact()}
aria-label={language.t("command.session.compact")}
/>
</Tooltip>
</Show>
<Show when={hasMessages()}>
<Tooltip value={language.t("chat.search.toggle")} placement="bottom">
<IconButton
@@ -56,10 +56,9 @@ const ContextTab: Component = () => {
const status = memory.status()
if (!status) return language.t("settings.context.memory.status.notLoaded")
if (!status.state.enabled) return language.t("settings.context.memory.status.disabled")
if (status.index.estimatedTokens === 0) return language.t("chat.memory.project.empty")
const tokens = status.index.estimatedTokens.toLocaleString(language.locale())
const session = memory.sessionTokens().toLocaleString(language.locale())
const ops = status.state.stats.lastOperationCount.toLocaleString(language.locale())
return language.t("settings.context.memory.status.enabledTokensOps", { session, tokens, ops })
return language.t("settings.context.memory.status.enabledTokens", { tokens })
}
return (
@@ -90,27 +89,23 @@ const ContextTab: Component = () => {
</Switch>
</SettingsRow>
<SettingsRow
title={language.t("settings.context.memory.index.title")}
title={language.t("settings.context.memory.storage.title")}
description={
memory.enabled()
? language.t("settings.context.memory.index.path", { path: memory.status()!.root })
: language.t("settings.context.memory.index.enable")
? language.t("settings.context.memory.storage.path", { path: memory.status()!.root })
: language.t("settings.context.memory.storage.enable")
}
last
>
<div style={{ display: "flex", gap: "6px", "align-items": "center" }}>
<Button variant="secondary" size="small" icon="eye" onClick={() => memory.showMemory()}>
{language.t("settings.context.memory.inspect")}
</Button>
<IconButton
size="small"
variant="ghost"
icon="reset"
disabled={memory.pending()}
onClick={() => memory.rebuild()}
aria-label={language.t("settings.context.memory.rebuild")}
/>
</div>
<Button
variant="secondary"
size="small"
icon="eye"
disabled={memory.loading() || memory.pending() || !memory.enabled() || memory.totalTokens() === 0}
onClick={() => memory.inspect()}
>
{language.t("settings.context.memory.inspect")}
</Button>
</SettingsRow>
<Show when={memory.error()}>
{(err) => (
@@ -1,53 +1,39 @@
import { createContext, createEffect, createMemo, createSignal, onCleanup, untrack, useContext } from "solid-js"
import { createContext, createEffect, createMemo, createSignal, onCleanup, useContext } from "solid-js"
import type { Accessor, ParentComponent } from "solid-js"
import { useServer } from "./server"
import { useSession } from "./session"
import { useVSCode } from "./vscode"
import { useLanguage } from "./language"
import { showToast } from "@kilocode/kilo-ui/toast"
import type { MemoryShowResponse, MemoryStatusResponse } from "@kilocode/sdk/v2"
import type { ExtensionMessage, Message, Part } from "../types/messages"
import { addMemoryActivity, markerActivity, type MemoryActivity } from "../utils/memory-activity"
import { visibleParts } from "./session-queue"
import type { MemoryStatusResponse } from "@kilocode/sdk/v2"
import type { ExtensionMessage } from "../types/messages"
export interface MemoryContextValue {
status: Accessor<MemoryStatusResponse | undefined>
show: Accessor<MemoryShowResponse | undefined>
loading: Accessor<boolean>
pending: Accessor<boolean>
error: Accessor<string | undefined>
enabled: Accessor<boolean>
sessionTokens: Accessor<number>
totalTokens: Accessor<number>
activity: Accessor<MemoryActivity[]>
refresh: (includeSources?: boolean) => void
showMemory: () => void
refresh: () => void
inspect: () => void
enable: () => void
disable: () => void
auto: (mode: "on" | "off") => void
verbose: (mode: "on" | "off") => void
rebuild: () => void
remember: () => void
forget: () => void
}
export const MemoryContext = createContext<MemoryContextValue>()
const EVENT_DEDUPE_MS = 1000
type Marker = { part: string; item: MemoryActivity }
export const MemoryProvider: ParentComponent = (props) => {
const vscode = useVSCode()
const server = useServer()
const session = useSession()
const language = useLanguage()
const [status, setStatus] = createSignal<MemoryStatusResponse | undefined>()
const [show, setShow] = createSignal<MemoryShowResponse | undefined>()
const [loading, setLoading] = createSignal(false)
const [pending, setPending] = createSignal<string | undefined>()
const [error, setError] = createSignal<string | undefined>()
const [saved, setSaved] = createSignal<MemoryActivity[]>([])
const [markers, setMarkers] = createSignal<Record<string, Marker>>({})
const id = () => session.currentSessionID()
const key = (sid?: string) => sid ?? ""
@@ -57,101 +43,30 @@ export const MemoryProvider: ParentComponent = (props) => {
// currentSessionID yet (PromptInput posts with the draft id), so match both.
return sid === id() || sid === session.draftSessionID()
}
const marker = (parts: readonly Part[], at: number) => {
for (const part of parts) {
const item = markerActivity([part], at)
if (item) return { part: part.id, item } satisfies Marker
}
}
const stamp = (message: Message) => message.time?.created ?? Date.parse(message.createdAt)
const mark = (messageID: string, part: Part, at: number) => {
const item = marker([part], at)
setMarkers((items) => {
if (item) return { ...items, [messageID]: item }
if (items[messageID]?.part !== part.id) return items
const next = { ...items }
delete next[messageID]
return next
})
}
const load = (message: Extract<ExtensionMessage, { type: "messagesLoaded" }>) => {
if (!current(message.sessionID)) return
const next = message.mode === "replace" || !message.mode ? {} : { ...markers() }
for (const entry of message.messages) {
const item = marker(entry.parts ?? [], stamp(entry))
if (item) next[entry.id] = item
else delete next[entry.id]
}
setMarkers(next)
}
const created = (message: Extract<ExtensionMessage, { type: "messageCreated" }>) => {
if (!current(message.message.sessionID)) return
const item = marker(message.message.parts ?? [], stamp(message.message))
if (item) setMarkers((items) => ({ ...items, [message.message.id]: item }))
}
const dropped = (messageID: string) =>
setMarkers((items) => {
if (!items[messageID]) return items
const next = { ...items }
delete next[messageID]
return next
})
const track = (message: ExtensionMessage) => {
if (message.type === "messagesLoaded") return load(message)
if (message.type === "messageCreated") return created(message)
if (message.type === "partUpdated") {
if (current(message.sessionID)) mark(message.messageID, message.part, Date.now())
return
}
if (message.type === "partsUpdated") {
for (const update of message.updates) {
if (current(update.sessionID)) mark(update.messageID, update.part, Date.now())
}
return
}
if (message.type === "partRemoved") {
if (!current(message.sessionID)) return
if (markers()[message.messageID]?.part === message.partID) dropped(message.messageID)
return
}
if (message.type === "messageRemoved" && current(message.sessionID)) dropped(message.messageID)
}
const scan = () => {
const next: Record<string, Marker> = {}
for (const message of session.messages()) {
const item = marker(session.getParts(message.id), stamp(message))
if (item) next[message.id] = item
}
setMarkers(next)
}
let last: { key: string; time: number } | undefined
let scope = ""
const clear = () => {
setStatus(undefined)
setShow(undefined)
setError(undefined)
setPending(undefined)
setSaved([])
setMarkers({})
last = undefined
}
const refresh = (includeSources = false) => {
const refresh = () => {
if (!server.isConnected()) return
setLoading(true)
setError(undefined)
vscode.postMessage({ type: "requestMemory", sessionID: id(), includeSources })
vscode.postMessage({ type: "requestMemory", sessionID: id() })
}
const operation = (op: "enable" | "disable" | "rebuild" | "verbose", mode?: "on" | "off") => {
const operation = (op: "enable" | "disable") => {
if (!server.isConnected()) return
setPending(key(id()))
setError(undefined)
vscode.postMessage({
type: "memoryOperation",
operation: op,
...(mode ? { mode } : {}),
sessionID: id(),
})
}
@@ -163,26 +78,15 @@ export const MemoryProvider: ParentComponent = (props) => {
vscode.postMessage({ type: "memoryOperation", operation: "auto", mode, sessionID: id() })
}
const prompt = (op: "remember" | "forget") => {
const inspect = () => {
if (!server.isConnected()) return
setPending(key(id()))
setError(undefined)
vscode.postMessage({ type: "memoryPrompt", operation: op, sessionID: id() })
}
const showMemory = () => {
if (!server.isConnected()) return
setLoading(true)
setError(undefined)
vscode.postMessage({ type: "memoryShow", sessionID: id() })
vscode.postMessage({ type: "memoryOperation", operation: "inspect", sessionID: id() })
}
const event = (message: Extract<ExtensionMessage, { type: "memoryEvent" }>) => {
if (!current(message.sessionID)) return
if (message.detail.type === "saved") {
setSaved((items) => addMemoryActivity(items, message.detail, Date.now()))
return
}
if (message.detail.type !== "error") return
if (!message.detail.message) return
const dedupeKey = `${message.sessionID ?? ""}:${message.detail.type ?? ""}:${message.detail.message}`
@@ -198,11 +102,9 @@ export const MemoryProvider: ParentComponent = (props) => {
if (message.error) {
setError(message.error)
setStatus(undefined)
setShow(undefined)
return
}
if (message.status) setStatus(message.status)
if (message.show) setShow(message.show)
setError(undefined)
}
@@ -217,12 +119,16 @@ export const MemoryProvider: ParentComponent = (props) => {
return
}
if (message.status) setStatus(message.status)
if (message.show) setShow(message.show)
setError(undefined)
if (message.operation === "remember" || message.operation === "correct" || message.operation === "forget") {
showToast({ variant: "success", title: language.t("chat.memory.updated") })
}
if (message.operation === "rebuild") {
showToast({ variant: "success", title: language.t("chat.memory.rebuild") })
}
}
const receive = (message: ExtensionMessage) => {
track(message)
if (message.type === "memoryEvent") {
event(message)
return
@@ -235,7 +141,7 @@ export const MemoryProvider: ParentComponent = (props) => {
done(message)
return
}
if (message.type === "extensionDataReady" && server.isConnected() && !status()) refresh(false)
if (message.type === "extensionDataReady" && server.isConnected() && !status()) refresh()
}
const unsubscribe = vscode.onMessage(receive)
@@ -250,56 +156,28 @@ export const MemoryProvider: ParentComponent = (props) => {
if (scope !== next) {
scope = next
clear()
untrack(scan)
}
if (!connected) {
setLoading(false)
return
}
refresh(false)
refresh()
})
const sessionTokens = (snapshot?: MemoryStatusResponse) => {
const sid = id()
if (!snapshot?.state.enabled) return 0
if (!sid || snapshot.state.stats.lastInjectedSessionID !== sid) return 0
return snapshot.state.stats.lastInjectedTokens
}
const total = createMemo(() => status()?.index.estimatedTokens ?? 0)
const sessionTotal = createMemo(() => sessionTokens(status()))
const activity = createMemo(() => {
const revert = session.currentSession()?.revert ?? undefined
const items = Object.entries(markers()).flatMap(([mid, entry]) => {
if (!revert || mid < revert.messageID) return [entry.item]
if (mid !== revert.messageID || !revert.partID) return []
const visible = visibleParts(mid, session.getParts(mid), revert)
return visible.some((part) => part.id === entry.part) ? [entry.item] : []
})
return [...items, ...saved()]
})
const value: MemoryContextValue = {
status,
show,
loading,
pending: createMemo(() => pending() === key(id())),
error,
enabled: createMemo(() => status()?.state.enabled ?? false),
sessionTokens: sessionTotal,
totalTokens: total,
activity,
refresh,
showMemory,
inspect,
enable: () => operation("enable"),
disable: () => operation("disable"),
auto,
verbose: (mode) => operation("verbose", mode),
rebuild: () => operation("rebuild"),
remember: () => prompt("remember"),
forget: () => prompt("forget"),
}
return <MemoryContext.Provider value={value}>{props.children}</MemoryContext.Provider>
@@ -27,6 +27,7 @@ interface VSCodeContext {
export interface SlashCommandEntry extends SlashCommandInfo {
action?: () => void
enabled?: Accessor<boolean>
nested?: boolean
}
export interface SlashCommand {
@@ -119,6 +120,24 @@ export function useSlashCommand(
window.dispatchEvent(new CustomEvent("compactSession"))
},
},
{
name: "memory",
description: "Manage project memory",
hints: ["mem"],
nested: true,
},
{ name: "memory status", description: "Show project memory status", hints: [] },
{ name: "memory show", description: "Show stored project memory", hints: [] },
{ name: "memory on", description: "Enable project memory", hints: [] },
{ name: "memory off", description: "Disable project memory", hints: [] },
{ name: "memory inspect", description: "Reveal the project memory folder", hints: [] },
{ name: "memory rebuild", description: "Rebuild the memory index", hints: [] },
{ name: "memory remember", description: "Save a project memory note", hints: [] },
{ name: "memory correct", description: "Save a correction to project memory", hints: [] },
{ name: "memory forget", description: "Remove matching project memory", hints: [] },
{ name: "memory auto on", description: "Enable automatic memory saves", hints: [] },
{ name: "memory auto off", description: "Disable automatic memory saves", hints: [] },
{ name: "memory purge confirm", description: "Delete all project memory files", hints: [] },
{
name: "export",
description: "Export the current session transcript as Markdown",
@@ -198,10 +217,20 @@ export function useSlashCommand(
const results = () => {
const q = query()
if (q === null) return []
const all = commands()
if (!q) return all
const list = commands()
if (q.startsWith("memory ")) {
const matches = list.filter((cmd) => cmd.name.startsWith("memory "))
if (q === "memory ") return matches
const lower = q.toLowerCase()
return sortByScore(
matches.filter((cmd) => cmd.name.toLowerCase().startsWith(lower)),
lower,
)
}
const root = list.filter((cmd) => !cmd.name.includes(" "))
if (!q) return root
const lower = q.toLowerCase()
const matches = all.filter(
const matches = root.filter(
(cmd) =>
cmd.name.toLowerCase().includes(lower) ||
cmd.description?.toLowerCase().includes(lower) ||
@@ -230,9 +259,15 @@ export function useSlashCommand(
request()
setQuery(match[1])
setIndex(0)
} else {
close()
return
}
const memory = before.match(/^\/(?:memory|mem)\s+([^\n]*)$/i)
if (!memory) return close()
const value = `memory ${memory[1]}`.toLowerCase()
if (!commands().some((cmd) => cmd.name.toLowerCase().startsWith(value))) return close()
request()
setQuery(value)
setIndex(0)
}
const select = (
@@ -256,7 +291,11 @@ export function useSlashCommand(
const pos = text.length
textarea.setSelectionRange(pos, pos)
textarea.focus()
close()
if (cmd.nested) {
setQuery(`${cmd.name} `)
setIndex(0)
}
if (!cmd.nested) close()
onSelect?.()
}
+7 -25
View File
@@ -1633,36 +1633,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "حفظ ذاكرة المشروع تلقائيًا",
"settings.context.memory.autoSave.description":
"حفظ حقائق المشروع الدائمة تلقائيًا من الجولات المكتملة عند تفعيل الذاكرة.",
"settings.context.memory.index.title": "فهرس الذاكرة",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "غير محمّلة",
"settings.context.memory.status.disabled": "معطّلة",
"settings.context.memory.status.enabledTokensOps":
"مفعّلة - ~{{session}} رموز سياق بدء التشغيل في هذه الجلسة - ~{{tokens}} رموز في الفهرس المخزّن - آخر عملية {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "فعّل الذاكرة لإنشاء ملفات ذاكرة المشروع.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "فحص",
"settings.context.memory.rebuild": "إعادة بناء فهرس الذاكرة",
"chat.memory.status.loading": "جارٍ تحميل حالة الذاكرة",
"chat.memory.status.active": "الذاكرة نشطة في هذه الجلسة",
"chat.memory.project.enabled": "ذاكرة المشروع مفعّلة",
"chat.memory.project.disabled": "ذاكرة المشروع معطّلة",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "فشل أمر الذاكرة",
"chat.memory.inspect": "فحص الذاكرة",
"chat.memory.remember": "تذكّر",
"chat.memory.forget": "انسَ",
"chat.memory.rebuild": "إعادة بناء الفهرس",
"chat.memory.disable": "تعطيل الذاكرة",
"chat.memory.enable": "تفعيل الذاكرة",
"chat.memory.verbose": "تفصيلي",
"chat.memory.activity.idle": "لا يوجد نشاط للذاكرة في هذه الجلسة",
"chat.memory.activity.loaded": "تم تحميل {{tokens}} رمزًا",
"chat.memory.activity.recalled": "تم استدعاء {{count}}",
"chat.memory.activity.saved": "تم حفظ {{count}}",
"chat.memory.activity.loaded.item": "تم التحميل: {{item}}",
"chat.memory.activity.recalled.item": "تم الاستدعاء: {{item}}",
"chat.memory.activity.saved.item": "تم الحفظ: {{item}}",
"chat.memory.badge.recalled": "تم استدعاء الذاكرة",
"chat.memory.badge.items": "{{count}} عناصر",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "استخدام prompt مخصص",
+7 -25
View File
@@ -1683,36 +1683,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Salvar memória do projeto automaticamente",
"settings.context.memory.autoSave.description":
"Salva automaticamente fatos duradouros do projeto a partir de turnos concluídos quando a memória está ativada.",
"settings.context.memory.index.title": "Índice da memória",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Não carregada",
"settings.context.memory.status.disabled": "Desativada",
"settings.context.memory.status.enabledTokensOps":
"Ativada - ~{{session}} tokens de contexto inicial nesta sessão - ~{{tokens}} tokens no índice armazenado - última op. {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Ative a memória para criar arquivos de memória do projeto.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Inspecionar",
"settings.context.memory.rebuild": "Reconstruir índice da memória",
"chat.memory.status.loading": "Carregando status da memória",
"chat.memory.status.active": "Memória ativa nesta sessão",
"chat.memory.project.enabled": "Memória do projeto ativada",
"chat.memory.project.disabled": "Memória do projeto desativada",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Comando de memória falhou",
"chat.memory.inspect": "Inspecionar memória",
"chat.memory.remember": "Lembrar",
"chat.memory.forget": "Esquecer",
"chat.memory.rebuild": "Reconstruir índice",
"chat.memory.disable": "Desativar memória",
"chat.memory.enable": "Ativar memória",
"chat.memory.verbose": "Detalhado",
"chat.memory.activity.idle": "Nenhuma atividade de memória nesta sessão",
"chat.memory.activity.loaded": "{{tokens}} tokens carregados",
"chat.memory.activity.recalled": "{{count}} recuperados",
"chat.memory.activity.saved": "{{count}} salvos",
"chat.memory.activity.loaded.item": "carregado: {{item}}",
"chat.memory.activity.recalled.item": "recuperado: {{item}}",
"chat.memory.activity.saved.item": "salvo: {{item}}",
"chat.memory.badge.recalled": "Memória recuperada",
"chat.memory.badge.items": "{{count}} itens",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Usar prompt personalizado",
+7 -25
View File
@@ -1676,36 +1676,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Automatsko spremanje memorije projekta",
"settings.context.memory.autoSave.description":
"Automatski sprema trajne činjenice projekta iz završenih koraka kada je memorija uključena.",
"settings.context.memory.index.title": "Indeks memorije",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Nije učitana",
"settings.context.memory.status.disabled": "Onemogućena",
"settings.context.memory.status.enabledTokensOps":
"Omogućena - ~{{session}} tokena početnog konteksta u ovoj sesiji - ~{{tokens}} tokena spremljenog indeksa - zadnja op. {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Omogućite memoriju za kreiranje datoteka memorije projekta.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Pregledaj",
"settings.context.memory.rebuild": "Ponovo izgradi indeks memorije",
"chat.memory.status.loading": "Učitavanje statusa memorije",
"chat.memory.status.active": "Memorija je aktivna u ovoj sesiji",
"chat.memory.project.enabled": "Memorija projekta omogućena",
"chat.memory.project.disabled": "Memorija projekta onemogućena",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Komanda memorije nije uspjela",
"chat.memory.inspect": "Pregledaj memoriju",
"chat.memory.remember": "Zapamti",
"chat.memory.forget": "Zaboravi",
"chat.memory.rebuild": "Ponovo izgradi indeks",
"chat.memory.disable": "Onemogući memoriju",
"chat.memory.enable": "Omogući memoriju",
"chat.memory.verbose": "Detaljno",
"chat.memory.activity.idle": "Nema aktivnosti memorije u ovoj sesiji",
"chat.memory.activity.loaded": "učitano {{tokens}} tokena",
"chat.memory.activity.recalled": "prizvano {{count}}",
"chat.memory.activity.saved": "sačuvano {{count}}",
"chat.memory.activity.loaded.item": "učitano: {{item}}",
"chat.memory.activity.recalled.item": "prizvano: {{item}}",
"chat.memory.activity.saved.item": "sačuvano: {{item}}",
"chat.memory.badge.recalled": "Memorija opozvana",
"chat.memory.badge.items": "{{count}} stavki",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Koristi prilagođeni prompt",
+7 -25
View File
@@ -1666,36 +1666,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Gem projekthukommelse automatisk",
"settings.context.memory.autoSave.description":
"Gemmer automatisk varige projektfakta fra afsluttede ture, når hukommelse er aktiveret.",
"settings.context.memory.index.title": "Hukommelsesindeks",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Ikke indlæst",
"settings.context.memory.status.disabled": "Deaktiveret",
"settings.context.memory.status.enabledTokensOps":
"Aktiveret - ~{{session}} opstartstokens i denne session - ~{{tokens}} tokens i gemt indeks - seneste handling {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Aktivér hukommelse for at oprette projektets hukommelsesfiler.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Inspicér",
"settings.context.memory.rebuild": "Genopbyg hukommelsesindeks",
"chat.memory.status.loading": "Indlæser hukommelsesstatus",
"chat.memory.status.active": "Hukommelse aktiv i denne session",
"chat.memory.project.enabled": "Projekthukommelse aktiveret",
"chat.memory.project.disabled": "Projekthukommelse deaktiveret",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Hukommelseskommando mislykkedes",
"chat.memory.inspect": "Inspicér hukommelse",
"chat.memory.remember": "Husk",
"chat.memory.forget": "Glem",
"chat.memory.rebuild": "Genopbyg indeks",
"chat.memory.disable": "Deaktivér hukommelse",
"chat.memory.enable": "Aktivér hukommelse",
"chat.memory.verbose": "Detaljeret",
"chat.memory.activity.idle": "Ingen hukommelsesaktivitet i denne session",
"chat.memory.activity.loaded": "indlæste {{tokens}} tokens",
"chat.memory.activity.recalled": "genkaldte {{count}}",
"chat.memory.activity.saved": "gemte {{count}}",
"chat.memory.activity.loaded.item": "indlæst: {{item}}",
"chat.memory.activity.recalled.item": "genkaldt: {{item}}",
"chat.memory.activity.saved.item": "gemt: {{item}}",
"chat.memory.badge.recalled": "Hukommelse genkaldt",
"chat.memory.badge.items": "{{count}} elementer",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Brug brugerdefineret prompt",
+7 -25
View File
@@ -1702,36 +1702,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Projektspeicher automatisch speichern",
"settings.context.memory.autoSave.description":
"Dauerhafte Projektfakten aus abgeschlossenen Durchläufen automatisch speichern, wenn Speicher aktiviert ist.",
"settings.context.memory.index.title": "Speicherindex",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Nicht geladen",
"settings.context.memory.status.disabled": "Deaktiviert",
"settings.context.memory.status.enabledTokensOps":
"Aktiviert - ~{{session}} Startkontext-Tokens in dieser Sitzung - ~{{tokens}} Tokens im gespeicherten Index - letzte Operation {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Aktivieren Sie den Speicher, um Projektspeicherdateien zu erstellen.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Prüfen",
"settings.context.memory.rebuild": "Speicherindex neu erstellen",
"chat.memory.status.loading": "Speicherstatus wird geladen",
"chat.memory.status.active": "Speicher in dieser Sitzung aktiv",
"chat.memory.project.enabled": "Projektspeicher aktiviert",
"chat.memory.project.disabled": "Projektspeicher deaktiviert",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Speicherbefehl fehlgeschlagen",
"chat.memory.inspect": "Speicher prüfen",
"chat.memory.remember": "Merken",
"chat.memory.forget": "Vergessen",
"chat.memory.rebuild": "Index neu erstellen",
"chat.memory.disable": "Speicher deaktivieren",
"chat.memory.enable": "Speicher aktivieren",
"chat.memory.verbose": "Ausführlich",
"chat.memory.activity.idle": "Keine Speicheraktivität in dieser Sitzung",
"chat.memory.activity.loaded": "{{tokens}} Token geladen",
"chat.memory.activity.recalled": "{{count}} abgerufen",
"chat.memory.activity.saved": "{{count}} gespeichert",
"chat.memory.activity.loaded.item": "geladen: {{item}}",
"chat.memory.activity.recalled.item": "abgerufen: {{item}}",
"chat.memory.activity.saved.item": "gespeichert: {{item}}",
"chat.memory.badge.recalled": "Speicher abgerufen",
"chat.memory.badge.items": "{{count}} Elemente",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Benutzerdefinierten prompt verwenden",
+7 -25
View File
@@ -1646,36 +1646,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Auto-save project memory",
"settings.context.memory.autoSave.description":
"Automatically save durable project facts from completed turns when memory is enabled.",
"settings.context.memory.index.title": "Memory index",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Not loaded",
"settings.context.memory.status.disabled": "Disabled",
"settings.context.memory.status.enabledTokensOps":
"Enabled - ~{{session}} startup tokens this session - ~{{tokens}} stored index tokens - last op {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Enable memory to create project memory files.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Inspect",
"settings.context.memory.rebuild": "Rebuild memory index",
"chat.memory.status.loading": "Memory status loading",
"chat.memory.status.active": "Memory active this session",
"chat.memory.project.enabled": "Project memory enabled",
"chat.memory.project.disabled": "Project memory disabled",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Memory command failed",
"chat.memory.inspect": "Inspect memory",
"chat.memory.remember": "Remember",
"chat.memory.forget": "Forget",
"chat.memory.rebuild": "Rebuild index",
"chat.memory.disable": "Disable memory",
"chat.memory.enable": "Enable memory",
"chat.memory.verbose": "Verbose",
"chat.memory.activity.idle": "No memory activity this session",
"chat.memory.activity.loaded": "loaded {{tokens}} tokens",
"chat.memory.activity.recalled": "recalled {{count}}",
"chat.memory.activity.saved": "saved {{count}}",
"chat.memory.activity.loaded.item": "loaded: {{item}}",
"chat.memory.activity.recalled.item": "recalled: {{item}}",
"chat.memory.activity.saved.item": "saved: {{item}}",
"chat.memory.badge.recalled": "Memory recalled",
"chat.memory.badge.items": "{{count}} items",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Use Custom Prompt",
+7 -25
View File
@@ -1691,36 +1691,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Guardado automático de memoria del proyecto",
"settings.context.memory.autoSave.description":
"Guardar automáticamente hechos duraderos del proyecto desde turnos completados cuando la memoria está activada.",
"settings.context.memory.index.title": "Índice de memoria",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "No cargada",
"settings.context.memory.status.disabled": "Desactivada",
"settings.context.memory.status.enabledTokensOps":
"Activada - ~{{session}} tokens de contexto inicial en esta sesión - ~{{tokens}} tokens del índice almacenado - última op. {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Activa la memoria para crear archivos de memoria del proyecto.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Inspeccionar",
"settings.context.memory.rebuild": "Reconstruir índice de memoria",
"chat.memory.status.loading": "Cargando estado de la memoria",
"chat.memory.status.active": "Memoria activa en esta sesión",
"chat.memory.project.enabled": "Memoria del proyecto activada",
"chat.memory.project.disabled": "Memoria del proyecto desactivada",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Error en el comando de memoria",
"chat.memory.inspect": "Inspeccionar memoria",
"chat.memory.remember": "Recordar",
"chat.memory.forget": "Olvidar",
"chat.memory.rebuild": "Reconstruir índice",
"chat.memory.disable": "Desactivar memoria",
"chat.memory.enable": "Activar memoria",
"chat.memory.verbose": "Detallado",
"chat.memory.activity.idle": "Sin actividad de memoria en esta sesión",
"chat.memory.activity.loaded": "{{tokens}} tokens cargados",
"chat.memory.activity.recalled": "{{count}} recuperados",
"chat.memory.activity.saved": "{{count}} guardados",
"chat.memory.activity.loaded.item": "cargado: {{item}}",
"chat.memory.activity.recalled.item": "recuperado: {{item}}",
"chat.memory.activity.saved.item": "guardado: {{item}}",
"chat.memory.badge.recalled": "Memoria recuperada",
"chat.memory.badge.items": "{{count}} elementos",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Usar prompt personalizado",
+7 -25
View File
@@ -1711,36 +1711,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Enregistrement automatique de la mémoire du projet",
"settings.context.memory.autoSave.description":
"Enregistrer automatiquement les faits durables du projet à partir des tours terminés lorsque la mémoire est activée.",
"settings.context.memory.index.title": "Index de mémoire",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Non chargée",
"settings.context.memory.status.disabled": "Désactivée",
"settings.context.memory.status.enabledTokensOps":
"Activée - ~{{session}} tokens de contexte initial dans cette session - ~{{tokens}} tokens dans lindex stocké - dernière opération {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Activez la mémoire pour créer les fichiers de mémoire du projet.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Inspecter",
"settings.context.memory.rebuild": "Reconstruire lindex de mémoire",
"chat.memory.status.loading": "Chargement de l’état de la mémoire",
"chat.memory.status.active": "Mémoire active pour cette session",
"chat.memory.project.enabled": "Mémoire du projet activée",
"chat.memory.project.disabled": "Mémoire du projet désactivée",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "La commande de mémoire a échoué",
"chat.memory.inspect": "Inspecter la mémoire",
"chat.memory.remember": "Mémoriser",
"chat.memory.forget": "Oublier",
"chat.memory.rebuild": "Reconstruire lindex",
"chat.memory.disable": "Désactiver la mémoire",
"chat.memory.enable": "Activer la mémoire",
"chat.memory.verbose": "Détaillé",
"chat.memory.activity.idle": "Aucune activité de mémoire pour cette session",
"chat.memory.activity.loaded": "{{tokens}} jetons chargés",
"chat.memory.activity.recalled": "{{count}} rappelés",
"chat.memory.activity.saved": "{{count}} enregistrés",
"chat.memory.activity.loaded.item": "chargé : {{item}}",
"chat.memory.activity.recalled.item": "rappelé : {{item}}",
"chat.memory.activity.saved.item": "enregistré : {{item}}",
"chat.memory.badge.recalled": "Mémoire rappelée",
"chat.memory.badge.items": "{{count}} éléments",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Utiliser un prompt personnalisé",
+7 -25
View File
@@ -1469,36 +1469,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Salvataggio automatico memoria progetto",
"settings.context.memory.autoSave.description":
"Salva automaticamente fatti durevoli del progetto dai turni completati quando la memoria è attiva.",
"settings.context.memory.index.title": "Indice della memoria",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Non caricata",
"settings.context.memory.status.disabled": "Disattivata",
"settings.context.memory.status.enabledTokensOps":
"Attivata - ~{{session}} token di contesto iniziale in questa sessione - ~{{tokens}} token nellindice salvato - ultima operazione {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Attiva la memoria per creare i file di memoria del progetto.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Ispeziona",
"settings.context.memory.rebuild": "Ricostruisci indice della memoria",
"chat.memory.status.loading": "Caricamento stato memoria",
"chat.memory.status.active": "Memoria attiva in questa sessione",
"chat.memory.project.enabled": "Memoria del progetto attivata",
"chat.memory.project.disabled": "Memoria del progetto disattivata",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Comando memoria non riuscito",
"chat.memory.inspect": "Ispeziona memoria",
"chat.memory.remember": "Ricorda",
"chat.memory.forget": "Dimentica",
"chat.memory.rebuild": "Ricostruisci indice",
"chat.memory.disable": "Disattiva memoria",
"chat.memory.enable": "Attiva memoria",
"chat.memory.verbose": "Dettagliato",
"chat.memory.activity.idle": "Nessuna attività di memoria in questa sessione",
"chat.memory.activity.loaded": "{{tokens}} token caricati",
"chat.memory.activity.recalled": "{{count}} richiamati",
"chat.memory.activity.saved": "{{count}} salvati",
"chat.memory.activity.loaded.item": "caricato: {{item}}",
"chat.memory.activity.recalled.item": "richiamato: {{item}}",
"chat.memory.activity.saved.item": "salvato: {{item}}",
"chat.memory.badge.recalled": "Memoria richiamata",
"chat.memory.badge.items": "{{count}} elementi",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Messaggio commit",
"settings.commitMessage.override.title": "Usa prompt personalizzato",
+7 -25
View File
@@ -1662,36 +1662,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "プロジェクトメモリを自動保存",
"settings.context.memory.autoSave.description":
"メモリが有効なとき、完了したターンから永続的なプロジェクト情報を自動保存します。",
"settings.context.memory.index.title": "メモリインデックス",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "未読み込み",
"settings.context.memory.status.disabled": "無効",
"settings.context.memory.status.enabledTokensOps":
"有効 - このセッションの起動コンテキスト ~{{session}} トークン - 保存済みインデックス ~{{tokens}} トークン - 最後の操作 {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "メモリを有効にしてプロジェクトメモリファイルを作成します。",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "検査",
"settings.context.memory.rebuild": "メモリインデックスを再構築",
"chat.memory.status.loading": "メモリ状態を読み込み中",
"chat.memory.status.active": "このセッションでメモリが有効です",
"chat.memory.project.enabled": "プロジェクトメモリが有効です",
"chat.memory.project.disabled": "プロジェクトメモリが無効です",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "メモリコマンドに失敗しました",
"chat.memory.inspect": "メモリを検査",
"chat.memory.remember": "記憶",
"chat.memory.forget": "忘れる",
"chat.memory.rebuild": "インデックスを再構築",
"chat.memory.disable": "メモリを無効化",
"chat.memory.enable": "メモリを有効化",
"chat.memory.verbose": "詳細",
"chat.memory.activity.idle": "このセッションにはメモリアクティビティがありません",
"chat.memory.activity.loaded": "{{tokens}} トークンを読み込み",
"chat.memory.activity.recalled": "{{count}} 件を呼び出し",
"chat.memory.activity.saved": "{{count}} 件を保存",
"chat.memory.activity.loaded.item": "読み込み: {{item}}",
"chat.memory.activity.recalled.item": "呼び出し: {{item}}",
"chat.memory.activity.saved.item": "保存: {{item}}",
"chat.memory.badge.recalled": "メモリを呼び出しました",
"chat.memory.badge.items": "{{count}} 件",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "カスタム prompt を使用",
+7 -25
View File
@@ -1645,36 +1645,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "프로젝트 메모리 자동 저장",
"settings.context.memory.autoSave.description":
"메모리가 활성화되면 완료된 턴에서 지속적인 프로젝트 사실을 자동으로 저장합니다.",
"settings.context.memory.index.title": "메모리 인덱스",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "로드되지 않음",
"settings.context.memory.status.disabled": "비활성화됨",
"settings.context.memory.status.enabledTokensOps":
"활성화됨 - 이 세션의 시작 컨텍스트 ~{{session}} 토큰 - 저장된 인덱스 ~{{tokens}} 토큰 - 마지막 작업 {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "메모리를 활성화하여 프로젝트 메모리 파일을 만듭니다.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "검사",
"settings.context.memory.rebuild": "메모리 인덱스 다시 빌드",
"chat.memory.status.loading": "메모리 상태 로드 중",
"chat.memory.status.active": "이 세션에서 메모리 활성화됨",
"chat.memory.project.enabled": "프로젝트 메모리 활성화됨",
"chat.memory.project.disabled": "프로젝트 메모리 비활성화됨",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "메모리 명령 실패",
"chat.memory.inspect": "메모리 검사",
"chat.memory.remember": "기억",
"chat.memory.forget": "잊기",
"chat.memory.rebuild": "인덱스 다시 빌드",
"chat.memory.disable": "메모리 비활성화",
"chat.memory.enable": "메모리 활성화",
"chat.memory.verbose": "상세",
"chat.memory.activity.idle": "이 세션에 메모리 활동 없음",
"chat.memory.activity.loaded": "토큰 {{tokens}}개 로드",
"chat.memory.activity.recalled": "{{count}}개 불러옴",
"chat.memory.activity.saved": "{{count}}개 저장",
"chat.memory.activity.loaded.item": "로드: {{item}}",
"chat.memory.activity.recalled.item": "불러옴: {{item}}",
"chat.memory.activity.saved.item": "저장: {{item}}",
"chat.memory.badge.recalled": "메모리 불러옴",
"chat.memory.badge.items": "{{count}}개 항목",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "사용자 지정 prompt 사용",
+7 -25
View File
@@ -1643,36 +1643,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Projectgeheugen automatisch opslaan",
"settings.context.memory.autoSave.description":
"Sla duurzame projectfeiten automatisch op uit voltooide beurten wanneer geheugen is ingeschakeld.",
"settings.context.memory.index.title": "Geheugenindex",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Niet geladen",
"settings.context.memory.status.disabled": "Uitgeschakeld",
"settings.context.memory.status.enabledTokensOps":
"Ingeschakeld - ~{{session}} startcontexttokens in deze sessie - ~{{tokens}} tokens in opgeslagen index - laatste bewerking {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Schakel geheugen in om projectgeheugenbestanden te maken.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Inspecteren",
"settings.context.memory.rebuild": "Geheugenindex opnieuw opbouwen",
"chat.memory.status.loading": "Geheugenstatus laden",
"chat.memory.status.active": "Geheugen actief in deze sessie",
"chat.memory.project.enabled": "Projectgeheugen ingeschakeld",
"chat.memory.project.disabled": "Projectgeheugen uitgeschakeld",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Geheugenopdracht mislukt",
"chat.memory.inspect": "Geheugen inspecteren",
"chat.memory.remember": "Onthouden",
"chat.memory.forget": "Vergeten",
"chat.memory.rebuild": "Index opnieuw opbouwen",
"chat.memory.disable": "Geheugen uitschakelen",
"chat.memory.enable": "Geheugen inschakelen",
"chat.memory.verbose": "Uitgebreid",
"chat.memory.activity.idle": "Geen geheugenactiviteit in deze sessie",
"chat.memory.activity.loaded": "{{tokens}} tokens geladen",
"chat.memory.activity.recalled": "{{count}} opgehaald",
"chat.memory.activity.saved": "{{count}} opgeslagen",
"chat.memory.activity.loaded.item": "geladen: {{item}}",
"chat.memory.activity.recalled.item": "opgehaald: {{item}}",
"chat.memory.activity.saved.item": "opgeslagen: {{item}}",
"chat.memory.badge.recalled": "Geheugen opgehaald",
"chat.memory.badge.items": "{{count}} items",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Aangepaste prompt gebruiken",
+7 -25
View File
@@ -1665,36 +1665,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Lagre prosjektminne automatisk",
"settings.context.memory.autoSave.description":
"Lagrer varige prosjektfakta automatisk fra fullførte turer når minne er aktivert.",
"settings.context.memory.index.title": "Minneindeks",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Ikke lastet",
"settings.context.memory.status.disabled": "Deaktivert",
"settings.context.memory.status.enabledTokensOps":
"Aktivert - ~{{session}} oppstartstokener i denne økten - ~{{tokens}} tokener i lagret indeks - siste operasjon {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Aktiver minne for å opprette prosjektets minnefiler.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Inspiser",
"settings.context.memory.rebuild": "Bygg minneindeks på nytt",
"chat.memory.status.loading": "Laster minnestatus",
"chat.memory.status.active": "Minne aktivt i denne økten",
"chat.memory.project.enabled": "Prosjektminne aktivert",
"chat.memory.project.disabled": "Prosjektminne deaktivert",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Minnekommando mislyktes",
"chat.memory.inspect": "Inspiser minne",
"chat.memory.remember": "Husk",
"chat.memory.forget": "Glem",
"chat.memory.rebuild": "Bygg indeks på nytt",
"chat.memory.disable": "Deaktiver minne",
"chat.memory.enable": "Aktiver minne",
"chat.memory.verbose": "Detaljert",
"chat.memory.activity.idle": "Ingen minneaktivitet i denne økten",
"chat.memory.activity.loaded": "lastet inn {{tokens}} tokener",
"chat.memory.activity.recalled": "hentet {{count}}",
"chat.memory.activity.saved": "lagret {{count}}",
"chat.memory.activity.loaded.item": "lastet inn: {{item}}",
"chat.memory.activity.recalled.item": "hentet: {{item}}",
"chat.memory.activity.saved.item": "lagret: {{item}}",
"chat.memory.badge.recalled": "Minne hentet",
"chat.memory.badge.items": "{{count}} elementer",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Bruk egendefinert prompt",
+7 -25
View File
@@ -1674,36 +1674,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Automatycznie zapisuj pamięć projektu",
"settings.context.memory.autoSave.description":
"Automatycznie zapisuje trwałe fakty projektu z zakończonych tur, gdy pamięć jest włączona.",
"settings.context.memory.index.title": "Indeks pamięci",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Nie wczytano",
"settings.context.memory.status.disabled": "Wyłączona",
"settings.context.memory.status.enabledTokensOps":
"Włączona - ~{{session}} tokenów kontekstu startowego w tej sesji - ~{{tokens}} tokenów w zapisanym indeksie - ostatnia operacja {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Włącz pamięć, aby utworzyć pliki pamięci projektu.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Sprawdź",
"settings.context.memory.rebuild": "Odbuduj indeks pamięci",
"chat.memory.status.loading": "Ładowanie stanu pamięci",
"chat.memory.status.active": "Pamięć aktywna w tej sesji",
"chat.memory.project.enabled": "Pamięć projektu włączona",
"chat.memory.project.disabled": "Pamięć projektu wyłączona",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Polecenie pamięci nie powiodło się",
"chat.memory.inspect": "Sprawdź pamięć",
"chat.memory.remember": "Zapamiętaj",
"chat.memory.forget": "Zapomnij",
"chat.memory.rebuild": "Odbuduj indeks",
"chat.memory.disable": "Wyłącz pamięć",
"chat.memory.enable": "Włącz pamięć",
"chat.memory.verbose": "Szczegółowo",
"chat.memory.activity.idle": "Brak aktywności pamięci w tej sesji",
"chat.memory.activity.loaded": "wczytano {{tokens}} tokenów",
"chat.memory.activity.recalled": "przywołano {{count}}",
"chat.memory.activity.saved": "zapisano {{count}}",
"chat.memory.activity.loaded.item": "wczytano: {{item}}",
"chat.memory.activity.recalled.item": "przywołano: {{item}}",
"chat.memory.activity.saved.item": "zapisano: {{item}}",
"chat.memory.badge.recalled": "Pamięć przywołana",
"chat.memory.badge.items": "{{count}} elementów",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Użyj niestandardowego prompt",
+7 -25
View File
@@ -1673,36 +1673,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Автосохранение памяти проекта",
"settings.context.memory.autoSave.description":
"Автоматически сохраняет устойчивые факты проекта из завершённых ходов, когда память включена.",
"settings.context.memory.index.title": "Индекс памяти",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Не загружена",
"settings.context.memory.status.disabled": "Отключена",
"settings.context.memory.status.enabledTokensOps":
"Включена - ~{{session}} токенов начального контекста в этой сессии - ~{{tokens}} токенов в сохранённом индексе - последняя операция {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Включите память, чтобы создать файлы памяти проекта.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Проверить",
"settings.context.memory.rebuild": "Перестроить индекс памяти",
"chat.memory.status.loading": "Загрузка состояния памяти",
"chat.memory.status.active": "Память активна в этом сеансе",
"chat.memory.project.enabled": "Память проекта включена",
"chat.memory.project.disabled": "Память проекта отключена",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Команда памяти не выполнена",
"chat.memory.inspect": "Проверить память",
"chat.memory.remember": "Запомнить",
"chat.memory.forget": "Забыть",
"chat.memory.rebuild": "Перестроить индекс",
"chat.memory.disable": "Отключить память",
"chat.memory.enable": "Включить память",
"chat.memory.verbose": "Подробно",
"chat.memory.activity.idle": "В этом сеансе нет активности памяти",
"chat.memory.activity.loaded": "загружено {{tokens}} токенов",
"chat.memory.activity.recalled": "извлечено {{count}}",
"chat.memory.activity.saved": "сохранено {{count}}",
"chat.memory.activity.loaded.item": "загружено: {{item}}",
"chat.memory.activity.recalled.item": "извлечено: {{item}}",
"chat.memory.activity.saved.item": "сохранено: {{item}}",
"chat.memory.badge.recalled": "Память извлечена",
"chat.memory.badge.items": "{{count}} элементов",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Использовать пользовательский prompt",
+7 -25
View File
@@ -1643,36 +1643,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "บันทึกความจำของโปรเจกต์อัตโนมัติ",
"settings.context.memory.autoSave.description":
"บันทึกข้อเท็จจริงถาวรของโปรเจกต์จากรอบที่เสร็จแล้วโดยอัตโนมัติเมื่อเปิดใช้ความจำ",
"settings.context.memory.index.title": "ดัชนีความจำ",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "ยังไม่ได้โหลด",
"settings.context.memory.status.disabled": "ปิดใช้งาน",
"settings.context.memory.status.enabledTokensOps":
"เปิดใช้งาน - ~{{session}} โทเค็นบริบทเริ่มต้นในเซสชันนี้ - ~{{tokens}} โทเค็นในดัชนีที่บันทึกไว้ - การทำงานล่าสุด {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "เปิดใช้ความจำเพื่อสร้างไฟล์ความจำของโปรเจกต์",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "ตรวจสอบ",
"settings.context.memory.rebuild": "สร้างดัชนีความจำใหม่",
"chat.memory.status.loading": "กำลังโหลดสถานะความจำ",
"chat.memory.status.active": "หน่วยความจำทำงานในเซสชันนี้",
"chat.memory.project.enabled": "เปิดใช้ความจำของโปรเจกต์แล้ว",
"chat.memory.project.disabled": "ปิดใช้ความจำของโปรเจกต์แล้ว",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "คำสั่งความจำล้มเหลว",
"chat.memory.inspect": "ตรวจสอบความจำ",
"chat.memory.remember": "จำ",
"chat.memory.forget": "ลืม",
"chat.memory.rebuild": "สร้างดัชนีใหม่",
"chat.memory.disable": "ปิดใช้ความจำ",
"chat.memory.enable": "เปิดใช้หน่วยความจำ",
"chat.memory.verbose": "แบบละเอียด",
"chat.memory.activity.idle": "ไม่มีกิจกรรมหน่วยความจำในเซสชันนี้",
"chat.memory.activity.loaded": "โหลด {{tokens}} โทเค็นแล้ว",
"chat.memory.activity.recalled": "เรียกคืน {{count}} รายการแล้ว",
"chat.memory.activity.saved": "บันทึก {{count}} รายการแล้ว",
"chat.memory.activity.loaded.item": "โหลดแล้ว: {{item}}",
"chat.memory.activity.recalled.item": "เรียกคืนแล้ว: {{item}}",
"chat.memory.activity.saved.item": "บันทึกแล้ว: {{item}}",
"chat.memory.badge.recalled": "เรียกคืนความจำแล้ว",
"chat.memory.badge.items": "{{count}} รายการ",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "ใช้ prompt แบบกำหนดเอง",
+7 -25
View File
@@ -1630,36 +1630,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Proje belleğini otomatik kaydet",
"settings.context.memory.autoSave.description":
"Bellek açıkken tamamlanan turlardan kalıcı proje gerçeklerini otomatik olarak kaydeder.",
"settings.context.memory.index.title": "Bellek indeksi",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Yüklenmedi",
"settings.context.memory.status.disabled": "Devre dışı",
"settings.context.memory.status.enabledTokensOps":
"Etkin - bu oturumda ~{{session}} başlangıç bağlamı tokenı - depolanan indekste ~{{tokens}} token - son işlem {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Proje belleği dosyalarını oluşturmak için belleği etkinleştirin.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "İncele",
"settings.context.memory.rebuild": "Bellek indeksini yeniden oluştur",
"chat.memory.status.loading": "Bellek durumu yükleniyor",
"chat.memory.status.active": "Bellek bu oturumda etkin",
"chat.memory.project.enabled": "Proje belleği etkin",
"chat.memory.project.disabled": "Proje belleği devre dışı",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Bellek komutu başarısız oldu",
"chat.memory.inspect": "Belleği incele",
"chat.memory.remember": "Hatırla",
"chat.memory.forget": "Unut",
"chat.memory.rebuild": "İndeksi yeniden oluştur",
"chat.memory.disable": "Belleği devre dışı bırak",
"chat.memory.enable": "Belleği etkinleştir",
"chat.memory.verbose": "Ayrıntılı",
"chat.memory.activity.idle": "Bu oturumda bellek etkinliği yok",
"chat.memory.activity.loaded": "{{tokens}} token yüklendi",
"chat.memory.activity.recalled": "{{count}} öğe geri çağrıldı",
"chat.memory.activity.saved": "{{count}} öğe kaydedildi",
"chat.memory.activity.loaded.item": "yüklendi: {{item}}",
"chat.memory.activity.recalled.item": "geri çağrıldı: {{item}}",
"chat.memory.activity.saved.item": "kaydedildi: {{item}}",
"chat.memory.badge.recalled": "Bellek geri çağrıldı",
"chat.memory.badge.items": "{{count}} öğe",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Özel prompt Kullan",
+7 -25
View File
@@ -1627,36 +1627,18 @@ export const dict = {
"settings.context.memory.autoSave.title": "Автозбереження пам’яті проєкту",
"settings.context.memory.autoSave.description":
"Автоматично зберігає сталі факти проєкту із завершених ходів, коли пам’ять увімкнено.",
"settings.context.memory.index.title": "Індекс пам’яті",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "Не завантажено",
"settings.context.memory.status.disabled": "Вимкнено",
"settings.context.memory.status.enabledTokensOps":
"Увімкнено - ~{{session}} токенів стартового контексту в цій сесії - ~{{tokens}} токенів у збереженому індексі - остання операція {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "Увімкніть пам’ять, щоб створити файли пам’яті проєкту.",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "Перевірити",
"settings.context.memory.rebuild": "Перебудувати індекс пам’яті",
"chat.memory.status.loading": "Завантаження стану пам’яті",
"chat.memory.status.active": "Пам’ять активна в цьому сеансі",
"chat.memory.project.enabled": "Пам’ять проєкту увімкнено",
"chat.memory.project.disabled": "Пам’ять проєкту вимкнено",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "Команду пам’яті не виконано",
"chat.memory.inspect": "Перевірити пам’ять",
"chat.memory.remember": "Запам’ятати",
"chat.memory.forget": "Забути",
"chat.memory.rebuild": "Перебудувати індекс",
"chat.memory.disable": "Вимкнути пам’ять",
"chat.memory.enable": "Увімкнути пам’ять",
"chat.memory.verbose": "Докладно",
"chat.memory.activity.idle": "У цьому сеансі немає активності пам’яті",
"chat.memory.activity.loaded": "завантажено {{tokens}} токенів",
"chat.memory.activity.recalled": "відновлено {{count}}",
"chat.memory.activity.saved": "збережено {{count}}",
"chat.memory.activity.loaded.item": "завантажено: {{item}}",
"chat.memory.activity.recalled.item": "відновлено: {{item}}",
"chat.memory.activity.saved.item": "збережено: {{item}}",
"chat.memory.badge.recalled": "Пам’ять відновлено",
"chat.memory.badge.items": "{{count}} елементів",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "Використовувати власний prompt",
+7 -25
View File
@@ -1595,36 +1595,18 @@ export const dict = {
"settings.context.memory.project.title": "项目记忆",
"settings.context.memory.autoSave.title": "自动保存项目记忆",
"settings.context.memory.autoSave.description": "启用记忆时,自动从已完成轮次保存持久项目事实。",
"settings.context.memory.index.title": "记忆索引",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "未加载",
"settings.context.memory.status.disabled": "已禁用",
"settings.context.memory.status.enabledTokensOps":
"已启用 - 本会话启动上下文约 {{session}} 个 token - 已存储索引约 {{tokens}} 个 token - 上次操作 {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "启用记忆以创建项目记忆文件。",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "检查",
"settings.context.memory.rebuild": "重建记忆索引",
"chat.memory.status.loading": "正在加载记忆状态",
"chat.memory.status.active": "本次会话中记忆已启用",
"chat.memory.project.enabled": "项目记忆已启用",
"chat.memory.project.disabled": "项目记忆已禁用",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "记忆命令失败",
"chat.memory.inspect": "检查记忆",
"chat.memory.remember": "记住",
"chat.memory.forget": "忘记",
"chat.memory.rebuild": "重建索引",
"chat.memory.disable": "禁用记忆",
"chat.memory.enable": "启用记忆",
"chat.memory.verbose": "详细",
"chat.memory.activity.idle": "本次会话中没有记忆活动",
"chat.memory.activity.loaded": "已加载 {{tokens}} 个 token",
"chat.memory.activity.recalled": "已召回 {{count}} 项",
"chat.memory.activity.saved": "已保存 {{count}} 项",
"chat.memory.activity.loaded.item": "已加载:{{item}}",
"chat.memory.activity.recalled.item": "已召回:{{item}}",
"chat.memory.activity.saved.item": "已保存:{{item}}",
"chat.memory.badge.recalled": "已召回记忆",
"chat.memory.badge.items": "{{count}} 项",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "使用自定义 prompt",
+7 -25
View File
@@ -1559,36 +1559,18 @@ export const dict = {
"settings.context.memory.project.title": "專案記憶",
"settings.context.memory.autoSave.title": "自動儲存專案記憶",
"settings.context.memory.autoSave.description": "啟用記憶時,自動從已完成回合儲存持久專案事實。",
"settings.context.memory.index.title": "記憶索引",
"settings.context.memory.storage.title": "Storage",
"settings.context.memory.status.notLoaded": "未載入",
"settings.context.memory.status.disabled": "已停用",
"settings.context.memory.status.enabledTokensOps":
"已啟用 - 此工作階段啟動內容約 {{session}} 個 token - 已儲存索引約 {{tokens}} 個 token - 上次操作 {{ops}}",
"settings.context.memory.index.path": "{{path}}/index.kmem",
"settings.context.memory.index.enable": "啟用記憶以建立專案記憶檔案。",
"settings.context.memory.status.enabledTokens": "Enabled - ~{{tokens}} stored tokens",
"settings.context.memory.storage.path": "{{path}}",
"settings.context.memory.storage.enable": "Enable memory to create project memory files.",
"settings.context.memory.inspect": "檢查",
"settings.context.memory.rebuild": "重建記憶索引",
"chat.memory.status.loading": "正在載入記憶狀態",
"chat.memory.status.active": "本次工作階段中記憶已啟用",
"chat.memory.project.enabled": "專案記憶已啟用",
"chat.memory.project.disabled": "專案記憶已停用",
"chat.memory.project.empty": "This project doesn't have any memory yet. It will start showing after you use Kilo.",
"chat.memory.command.failed": "記憶命令失敗",
"chat.memory.inspect": "檢查記憶",
"chat.memory.remember": "記住",
"chat.memory.forget": "忘記",
"chat.memory.rebuild": "重建索引",
"chat.memory.disable": "停用記憶",
"chat.memory.enable": "啟用記憶",
"chat.memory.verbose": "詳細",
"chat.memory.activity.idle": "本次工作階段中沒有記憶活動",
"chat.memory.activity.loaded": "已載入 {{tokens}} 個 token",
"chat.memory.activity.recalled": "已召回 {{count}} 項",
"chat.memory.activity.saved": "已儲存 {{count}} 項",
"chat.memory.activity.loaded.item": "已載入:{{item}}",
"chat.memory.activity.recalled.item": "已召回:{{item}}",
"chat.memory.activity.saved.item": "已儲存:{{item}}",
"chat.memory.badge.recalled": "已召回記憶",
"chat.memory.badge.items": "{{count}} 項",
"chat.memory.updated": "Memory updated",
"chat.memory.rebuild": "Memory index rebuilt",
"settings.commitMessage.title": "Commit Message",
"settings.commitMessage.override.title": "使用自訂 prompt",
@@ -9,8 +9,6 @@
import type { Meta, StoryObj } from "storybook-solidjs-vite"
import type { AssistantMessage } from "@kilocode/sdk/v2"
import { MemoryContract } from "@kilocode/kilo-memory/effect/httpapi"
import { MemorySchema } from "@kilocode/kilo-memory/schema"
import { StoryProviders, defaultMockData, mockSessionValue } from "./StoryProviders"
import { ChatView } from "../components/chat/ChatView"
import { ErrorDisplay } from "../components/chat/ErrorDisplay"
@@ -22,7 +20,6 @@ import { MessageList } from "../components/chat/MessageList"
import { VscodeUserMessage } from "../components/chat/VscodeUserMessage"
import { TurnOutcome } from "../components/shared/TurnOutcome"
import { SessionContext } from "../context/session"
import { MemoryContext, type MemoryContextValue } from "../context/memory"
import { ProviderContext } from "../context/provider"
import { ServerContext } from "../context/server"
import { WorktreeModeProvider } from "../context/worktree-mode"
@@ -991,88 +988,6 @@ export const TaskHeaderWithTodosAllDone: Story = {
},
}
const state = MemorySchema.create()
const mockMemory: MemoryContextValue = {
status: () => ({
root: "/project",
state: MemoryContract.state({
...state,
enabled: true,
stats: {
...state.stats,
lastInjectedAt: headerNow,
lastInjectedBytes: 2_132,
lastInjectedTokens: 533,
lastInjectedSessionID: SESSION_ID,
},
}),
exists: { state: true, index: true },
index: { bytes: 49_600, estimatedTokens: 12_400, preview: "" },
}),
show: () => undefined,
loading: () => false,
pending: () => false,
error: () => undefined,
enabled: () => true,
sessionTokens: () => 533,
totalTokens: () => 12_400,
activity: () => [
{
type: "loaded",
at: headerNow,
tokens: 533,
count: 1,
items: [],
refs: ["project.md"],
},
],
refresh: () => {},
showMemory: () => {},
enable: () => {},
disable: () => {},
auto: () => {},
verbose: () => {},
rebuild: () => {},
remember: () => {},
forget: () => {},
}
const memoryHeader = (width: string) => {
const session = {
...mockSessionValue({ id: SESSION_ID, status: "idle" }),
messages: () => [{ id: "msg-001" }] as any[],
contextUsage: () => ({ tokens: 34300, percentage: 17 }),
costBreakdown: () => [{ label: "Session", cost: 0.64 }],
currentSession: () => ({
id: SESSION_ID,
title: "Integrate project memory",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
}
return (
<StoryProviders sessionID={SESSION_ID} status="idle" noPadding>
<SessionContext.Provider value={session as any}>
<MemoryContext.Provider value={mockMemory}>
<div style={{ width }}>
<TaskHeader />
</div>
</MemoryContext.Provider>
</SessionContext.Provider>
</StoryProviders>
)
}
export const TaskHeaderWithMemory: Story = {
name: "TaskHeader — with memory enabled",
render: () => memoryHeader("380px"),
}
export const TaskHeaderWithMemory200: Story = {
name: "TaskHeader — with memory enabled 200",
render: () => memoryHeader("200px"),
}
const usageTokens = { input: 25_900_000, output: 52_000, reasoning: 4_100, cache: { read: 10_500_000, write: 80_000 } }
const usageData = {
sessionIDs: [SESSION_ID, "story-subagent-001"],
@@ -228,14 +228,6 @@
width: 100%;
}
[data-component="assistant-memory-badge"] {
align-self: flex-start;
color: var(--vscode-descriptionForeground);
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
line-height: var(--line-height-normal);
}
.vscode-session-turn-diffs {
width: 100%;
}
@@ -473,172 +473,6 @@
font-variant-numeric: tabular-nums;
}
.task-header-context-trigger {
all: unset;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 3px;
color: var(--vscode-foreground);
cursor: pointer;
}
.task-header-context-trigger:hover {
background: var(--vscode-toolbar-hoverBackground);
}
[data-slot="task-header-memory-dot"] {
position: absolute;
right: 2px;
bottom: 2px;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--vscode-testing-iconPassed, #73c991);
}
.task-header-context-popover {
width: min(280px, calc(100vw - 16px));
max-width: calc(100vw - 16px);
white-space: normal;
overflow-wrap: break-word;
}
.task-header-memory-tooltip {
max-width: min(320px, calc(100vw - 16px));
white-space: normal;
overflow-wrap: break-word;
}
[data-slot="task-header-context-tooltip-title"] {
margin-bottom: 4px;
color: var(--vscode-foreground);
font-weight: 600;
}
[data-slot="task-header-context-tooltip-status"] {
margin-bottom: 2px;
color: var(--vscode-foreground);
}
[data-slot="task-header-context-menu"] {
display: flex;
flex-direction: column;
padding: 8px;
}
[data-slot="task-header-context-title"] {
padding: 2px 4px 8px;
color: var(--vscode-foreground);
font-size: var(--kilo-font-size-12);
font-weight: 600;
}
[data-slot="task-header-context-section"] {
display: flex;
flex-direction: column;
gap: 6px;
padding: 6px 0;
}
[data-slot="task-header-context-section"] + [data-slot="task-header-context-section"] {
margin-top: 6px;
padding-top: 12px;
border-top: 1px solid var(--vscode-widget-border, var(--vscode-editorWidget-border));
}
[data-slot="task-header-context-section-title"] {
display: flex;
align-items: center;
gap: 4px;
padding: 0 4px;
color: var(--vscode-descriptionForeground);
font-size: var(--kilo-font-size-11);
font-weight: 600;
}
[data-slot="task-header-memory-status"] {
padding: 0 4px;
color: var(--vscode-foreground);
font-size: var(--kilo-font-size-11);
}
[data-slot="task-header-memory-activity"] {
min-width: 0;
padding: 0 4px;
color: var(--vscode-descriptionForeground);
font-size: var(--kilo-font-size-11);
white-space: normal;
overflow-wrap: break-word;
}
[data-slot="task-header-memory-activity-summary"] {
display: flex;
flex-direction: column;
gap: 2px;
}
[data-slot="task-header-memory-activity-list"] {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
min-width: 0;
color: var(--vscode-foreground);
line-height: 1.3;
white-space: normal;
overflow-wrap: break-word;
}
[data-slot="task-header-memory-activity-list"] > div {
white-space: normal;
overflow-wrap: break-word;
}
[data-slot="task-header-context-actions"] {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
[data-slot="task-header-context-action"] {
border: 0;
border-radius: 3px;
padding: 4px 6px;
color: var(--vscode-foreground);
background: transparent;
font: inherit;
font-size: var(--kilo-font-size-11);
text-align: left;
cursor: pointer;
}
[data-slot="task-header-context-action"]:last-child:nth-child(odd) {
grid-column: 1 / -1;
}
[data-slot="task-header-context-action"]:hover:not(:disabled) {
background: var(--vscode-list-hoverBackground);
}
[data-slot="task-header-context-action"]:disabled {
color: var(--vscode-disabledForeground);
cursor: default;
}
[data-slot="task-header-memory-verbose"] {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 6px;
border-top: 1px solid var(--vscode-widget-border, var(--vscode-editorWidget-border));
color: var(--vscode-foreground);
font-size: var(--kilo-font-size-11);
}
/* Expand toggle */
[data-slot="task-header-expand"] {
all: unset;
@@ -7,13 +7,9 @@ import type {
MemoryPurgeResponse,
MemoryRememberResponse,
MemoryRebuildResponse,
MemoryShowResponse,
MemoryStatusResponse,
} from "@kilocode/sdk/v2"
import type {
MemoryOperation as SharedMemoryOperation,
MemoryPromptOperation as SharedMemoryPromptOperation,
} from "@kilocode/kilo-memory/commands"
import type { MemoryOperation as SharedMemoryOperation } from "@kilocode/kilo-memory/commands"
import type { MemorySchema } from "@kilocode/kilo-memory/schema"
export type MemorySourceFile = MemorySchema.Source
@@ -22,8 +18,6 @@ export type MemoryOperation = SharedMemoryOperation
export type MemoryResultOperation = MemoryOperation
export type MemoryPromptOperation = SharedMemoryPromptOperation
export type MemoryOperationResponse =
| MemoryEnableResponse
| MemoryConfigureResponse
@@ -39,7 +33,6 @@ export interface MemoryLoadedMessage {
type: "memoryLoaded"
sessionID?: string
status?: MemoryStatusResponse
show?: MemoryShowResponse
error?: string
}
@@ -69,7 +62,6 @@ export interface MemoryOperationResultMessage {
sessionID?: string
ok: boolean
status?: MemoryStatusResponse
show?: MemoryShowResponse
result?: MemoryOperationResponse
error?: string
}
@@ -77,12 +69,12 @@ export interface MemoryOperationResultMessage {
export interface RequestMemoryMessage {
type: "requestMemory"
sessionID?: string
includeSources?: boolean
}
export interface MemoryShowMessage {
type: "memoryShow"
sessionID?: string
mode?: "status" | "show"
}
export interface MemoryOperationMessage {
@@ -97,9 +89,3 @@ export interface MemoryOperationMessage {
file?: MemorySourceFile
section?: string
}
export interface MemoryPromptMessage {
type: "memoryPrompt"
operation: MemoryPromptOperation
sessionID?: string
}
@@ -15,7 +15,7 @@ import type {
SkipLegacyMigrationMessage,
StartMigrationMessage,
} from "./migration"
import type { MemoryShowMessage, MemoryOperationMessage, MemoryPromptMessage, RequestMemoryMessage } from "./memory"
import type { MemoryShowMessage, MemoryOperationMessage, RequestMemoryMessage } from "./memory"
// ============================================
// Messages FROM webview TO extension
@@ -1412,7 +1412,6 @@ export type WebviewMessage =
| RequestMemoryMessage
| MemoryShowMessage
| MemoryOperationMessage
| MemoryPromptMessage
| CreateSectionRequest
| RenameSectionRequest
| DeleteSectionRequest
@@ -1,45 +0,0 @@
import { MemoryMarkerMeta } from "@kilocode/kilo-memory/marker-meta"
import type { MemoryEventDetail } from "../types/messages/memory"
const LIMIT = 50
export type MemoryActivity = {
type: "loaded" | "recalled" | "saved"
at: number
tokens: number
count: number
items: string[]
refs: string[]
}
export function markerActivity(parts: readonly MemoryMarkerMeta.Part[], at: number): MemoryActivity | undefined {
const meta = MemoryMarkerMeta.fromParts(parts)
if (!meta) return
return {
type: meta.type === "startup" ? "loaded" : "recalled",
at,
tokens: meta.tokens,
count: meta.count,
items: meta.items,
refs: meta.files,
}
}
export function addMemoryActivity(
items: readonly MemoryActivity[],
detail: MemoryEventDetail,
at: number,
): MemoryActivity[] {
if (detail.type !== "saved") return [...items]
const count = detail.added ?? detail.operationCount ?? 1
if (count <= 0) return [...items]
const item: MemoryActivity = {
type: "saved",
at,
tokens: 0,
count,
items: detail.message ? [detail.message] : [],
refs: [...(detail.sources ?? []), ...(detail.files ?? [])],
}
return [...items, item].slice(-LIMIT)
}
@@ -12,6 +12,8 @@ import { useTheme } from "@tui/context/theme"
import { useTuiConfig } from "@tui/config"
import { useBindings } from "@tui/keymap"
import { useDialog, type DialogContext } from "@tui/ui/dialog"
import { DialogSelect, type DialogSelectOption } from "@tui/ui/dialog-select"
import { useToast } from "@tui/ui/toast"
import { getScrollAcceleration } from "@tui/util/scroll"
import { route } from "@/kilocode/cli/cmd/tui/memory-command"
import { errorMessage } from "@/util/error"
@@ -41,9 +43,14 @@ export function showMemoryDialog(dialog: DialogContext, input?: { workspace?: st
dialog.replace(() => <DialogMemory workspace={input?.workspace} directory={input?.directory} />)
}
export function showMemoryHelpDialog(dialog: DialogContext, reason?: string) {
export function showMemoryHelpDialog(
dialog: DialogContext,
input?: { workspace?: string; directory?: string; reason?: string },
) {
dialog.setSize("large")
dialog.replace(() => <DialogMemoryHelp reason={reason} />)
dialog.replace(() => (
<DialogMemoryHelp workspace={input?.workspace} directory={input?.directory} reason={input?.reason} />
))
}
export function showMemoryStatusDialog(dialog: DialogContext, input?: { workspace?: string; directory?: string }) {
@@ -99,32 +106,6 @@ function MemorySourcesInfo(props: {
)
}
function MemoryActivityInfo(props: {
state: {
autoInject: boolean
stats: MemoryAutosaveStatus.Stats & {
lastInjectedTokens: number
lastRecallCount: number
}
}
}) {
const { theme } = useTheme()
return (
<box>
<text fg={theme.text}>Activity</text>
<text fg={theme.textMuted}>
startup context {props.state.autoInject ? "on" : "off"}
{props.state.stats.lastInjectedTokens > 0
? ` · last injected ${fmt(props.state.stats.lastInjectedTokens)} tokens`
: ""}
</text>
<Show when={props.state.stats.lastRecallCount > 0}>
<text fg={theme.textMuted}>last recall {fmt(props.state.stats.lastRecallCount)} items</text>
</Show>
</box>
)
}
function MemoryItemsInfo(props: { items: string }) {
const { theme } = useTheme()
return (
@@ -137,36 +118,42 @@ function MemoryItemsInfo(props: { items: string }) {
)
}
export function DialogMemoryHelp(props: { reason?: string }) {
function draft(usage: string) {
const head = usage.split(" ")[0]
if (usage.includes("<") || usage.includes("|")) return `${head} `
return usage
}
export function DialogMemoryHelp(props: { workspace?: string; directory?: string; reason?: string }) {
const sdk = useSDK()
const project = useProject()
const dialog = useDialog()
const { theme } = useTheme()
const toast = useToast()
const options: DialogSelectOption<string>[] = MEMORY_COMMAND_CATALOG.map((item) => ({
title: item.description,
footer: `/memory ${item.usage}`,
category: "Memory",
value: item.usage,
}))
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text} attributes={TextAttributes.BOLD}>
Memory
</text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc
</text>
</box>
<Show when={props.reason}>{(reason) => <text fg={theme.error}>{reason()}</text>}</Show>
<box gap={0}>
<For each={MEMORY_COMMAND_CATALOG}>
{(item) => (
<box flexDirection="row" gap={2}>
<text fg={theme.text} flexShrink={0}>
/memory {item.usage}
</text>
<text fg={theme.textMuted} wrapMode="word">
{item.description}
</text>
</box>
)}
</For>
</box>
</box>
<DialogSelect
title="Memory"
options={options}
flat
footer={<Show when={props.reason}>{(reason) => <text fg={theme.error}>{reason()}</text>}</Show>}
onSelect={async (option) => {
dialog.clear()
const workspace = props.workspace ?? project.workspace.current()
const result = await sdk.client.tui.appendPrompt({
...route({ workspace, directory: props.directory }),
text: `/memory ${draft(option.value)}`,
})
if (!result.error) return
toast.show({ variant: "error", message: `Memory menu failed: ${errorMessage(result.error)}` })
}}
/>
)
}
@@ -218,14 +205,6 @@ function DialogMemoryStatus(props: { workspace?: string; directory?: string }) {
Auto-save sends best-effort-redacted turn context to your configured model provider; disable with /memory auto off.
</text>
</box>
<box>
<text fg={theme.text}>Verbose</text>
<text fg={theme.textMuted}>{item().state.verbose ? "on" : "off"}</text>
<text fg={theme.textMuted} wrapMode="word">
Verbose shows recall and save details; toggle with /memory verbose on|off.
</text>
</box>
<MemoryActivityInfo state={item().state} />
<MemorySourcesInfo sources={item().sources} />
<MemoryItemsInfo items={item().items} />
<box>
@@ -307,7 +286,6 @@ export function DialogMemory(props: { workspace?: string; directory?: string })
<box>
<MemoryHeaderInfo root={item().root} state={item().state} />
</box>
<MemoryActivityInfo state={item().state} />
<MemorySourcesInfo sources={item().sources} />
<MemoryItemsInfo items={item().items} />
</box>
@@ -1,5 +1,5 @@
import type { KiloClient } from "@kilocode/sdk/v2"
import type { CliRenderer } from "@opentui/core"
import open from "open"
import type { DialogContext } from "@tui/ui/dialog"
import type { ToastContext } from "@tui/ui/toast"
import {
@@ -18,7 +18,6 @@ export namespace MemoryPrompt {
sessionID?: string
toast: ToastContext
dialog: DialogContext
renderer?: CliRenderer
done(): void
}) {
const handled = await runMemoryCommand({
@@ -28,10 +27,13 @@ export namespace MemoryPrompt {
directory: input.directory,
sessionID: input.sessionID,
toast: input.toast,
renderer: input.renderer,
inspect: async (root) => {
await open(root)
},
show: () => showMemoryDialog(input.dialog, { workspace: input.workspace, directory: input.directory }),
status: () => showMemoryStatusDialog(input.dialog, { workspace: input.workspace, directory: input.directory }),
usage: (message) => showMemoryHelpDialog(input.dialog, message),
usage: (reason) =>
showMemoryHelpDialog(input.dialog, { workspace: input.workspace, directory: input.directory, reason }),
})
if (!handled) return false
input.done()
@@ -1,6 +1,6 @@
import type { TuiPluginApi } from "@kilocode/plugin/tui"
import type { Event } from "@kilocode/sdk/v2"
import { createEffect, createMemo, createResource, createSignal, onCleanup, onMount, Show } from "solid-js"
import { createMemo, createResource, createSignal, onCleanup, onMount, Show } from "solid-js"
import * as Log from "@opencode-ai/core/util/log"
import { route } from "@/kilocode/cli/cmd/tui/memory-command"
import { MemoryTuiMeta } from "@/kilocode/cli/cmd/tui/memory-meta"
@@ -10,12 +10,9 @@ export function memoryRow(input: {
enabled?: boolean
loading?: boolean
active: boolean
verbose: boolean
flash?: string
}): {
label: "Loading" | "Unavailable" | "Disabled" | "Enabled"
tone: "muted" | "success" | "error"
caption?: string
} {
if (input.enabled === undefined) {
return input.loading ? { label: "Loading", tone: "muted" } : { label: "Unavailable", tone: "error" }
@@ -24,7 +21,6 @@ export function memoryRow(input: {
return {
label: "Enabled",
tone: input.active ? ("success" as const) : ("muted" as const),
caption: input.verbose ? input.flash : undefined,
}
}
@@ -54,33 +50,9 @@ export function MemorySidebar(props: { api: TuiPluginApi; sessionID: string }) {
}),
),
)
const [flash, setFlash] = createSignal<string>()
const [saved, setSaved] = createSignal(false)
const timer = { id: undefined as ReturnType<typeof setTimeout> | undefined }
const pulse = { id: undefined as ReturnType<typeof setTimeout> | undefined }
const prior = { ids: new Set<string>(), ready: false }
const show = (label: string) => {
if (timer.id) clearTimeout(timer.id)
setFlash(label)
timer.id = setTimeout(() => {
setFlash()
timer.id = undefined
}, 5_000)
}
createEffect(() => {
const items = markers()
if (prior.ready) {
for (const item of items) {
if (!prior.ids.has(item.id) && MemoryTuiState.verbose(data())) {
show(item.meta.type === "recall" ? `recalled ${item.meta.count}` : "loaded")
}
}
}
prior.ids = new Set(items.map((item) => item.id))
prior.ready = true
})
onCleanup(() => {
if (timer.id) clearTimeout(timer.id)
if (pulse.id) clearTimeout(pulse.id)
})
const state = createMemo(() =>
@@ -88,8 +60,6 @@ export function MemorySidebar(props: { api: TuiPluginApi; sessionID: string }) {
enabled: data() && MemoryTuiState.enabled(data()),
loading: data.loading,
active: MemoryTuiState.active({ markers: markers().length, saved: saved() }),
verbose: MemoryTuiState.verbose(data()),
flash: flash(),
}),
)
@@ -97,16 +67,13 @@ export function MemorySidebar(props: { api: TuiPluginApi; sessionID: string }) {
const bump = () => setTick((value) => value + 1)
const save = (event: Extract<Event, { type: "memory.status" | "memory.updated" }>) => {
if (event.properties.sessionID !== props.sessionID) return
const detail = event.properties.detail
if (detail?.type !== "saved") return
if (event.properties.detail?.type !== "saved") return
setSaved(true)
if (pulse.id) clearTimeout(pulse.id)
pulse.id = setTimeout(() => {
setSaved(false)
pulse.id = undefined
}, 5_000)
if (!MemoryTuiState.verbose(data()) || typeof detail.operationCount !== "number") return
show(`saved ${detail.operationCount}`)
}
const offs = [
props.api.event.on("memory.status", (event) => {
@@ -149,7 +116,6 @@ export function MemorySidebar(props: { api: TuiPluginApi; sessionID: string }) {
</text>
<text fg={props.api.theme.current.text}>
{row().label}
{row().caption && <span style={{ fg: props.api.theme.current.textMuted }}> · {row().caption}</span>}
</text>
</box>
</box>
@@ -1,13 +1,5 @@
import type { KiloClient } from "@kilocode/sdk/v2"
import type { CliRenderer } from "@opentui/core"
import path from "path"
import { Process } from "@/util/process"
import { splitCommand } from "@/kilocode/util/split-command"
import {
MEMORY_USAGE,
parseMemoryCommand,
type ParsedMemoryCommand,
} from "@kilocode/kilo-memory/commands"
import { MEMORY_USAGE, parseMemoryCommand, type ParsedMemoryCommand } from "@kilocode/kilo-memory/commands"
import { errorMessage } from "@/util/error"
export { MEMORY_USAGE }
@@ -48,33 +40,6 @@ function auto(input: boolean) {
return `Memory auto-save ${input ? "on" : "off"}`
}
function verbose(input: boolean) {
return `Memory verbose ${input ? "on" : "off"}`
}
async function edit(input: { file: string; cwd?: string; renderer?: CliRenderer }) {
const editor = (process.env["VISUAL"] || process.env["EDITOR"])?.trim()
if (!editor) throw new Error("Set $VISUAL or $EDITOR to use /memory edit")
input.renderer?.suspend()
input.renderer?.currentRenderBuffer.clear()
try {
const proc = Process.spawn([...splitCommand(editor), input.file], {
cwd: input.cwd,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
shell: process.platform === "win32",
})
const code = await proc.exited
if (code !== 0) throw new Error(`Editor exited with code ${code}`)
} finally {
input.renderer?.currentRenderBuffer.clear()
input.renderer?.resume()
input.renderer?.requestRender()
}
}
export function parseMemoryInput(input: string): MemoryCommand | undefined {
return parseMemoryCommand(input)
}
@@ -86,7 +51,7 @@ export async function runMemoryCommand(input: {
directory?: string
sessionID?: string
toast: Toast
renderer?: CliRenderer
inspect?(root: string): void | Promise<void>
show(): void
status(): void
usage(message?: string): void
@@ -120,12 +85,12 @@ export async function runMemoryCommand(input: {
input.status()
return true
}
if (parsed.operation === "edit") {
if (parsed.operation === "inspect") {
const status = read(await input.client.memory.status(route(input)))
if (!status.state.enabled) throw new Error("Memory is disabled. Run /memory on first.")
await edit({ file: path.join(status.root, "project.md"), cwd: input.directory, renderer: input.renderer })
const result = read(await input.client.memory.rebuild(route(input)))
input.toast.show({ variant: "success", message: `${name} rebuilt (${tokens(result.index.tokens)})` })
if (!input.inspect) throw new Error("Memory folder inspection is unavailable")
input.toast.show({ variant: "info", message: `Memory folder: ${status.root}` })
await input.inspect(status.root)
return true
}
if (parsed.operation === "auto") {
@@ -138,16 +103,6 @@ export async function runMemoryCommand(input: {
input.toast.show({ variant: "info", message: auto(result.state.autoConsolidate) })
return true
}
if (parsed.operation === "verbose") {
const result = read(
await input.client.memory.configure({
...route(input),
verbose: parsed.mode === "on",
}),
)
input.toast.show({ variant: "info", message: verbose(result.state.verbose) })
return true
}
if (parsed.operation === "disable") {
read(await input.client.memory.disable(route(input)))
input.toast.show({ variant: "info", message: `${name} disabled` })
@@ -4,10 +4,4 @@ export namespace MemoryTuiMeta {
export function fromParts(parts: readonly MemoryMarkerMeta.Part[]) {
return MemoryMarkerMeta.fromParts(parts)
}
export function items(input: unknown) {
const value = input as { items?: unknown } | undefined
if (!Array.isArray(value?.items)) return []
return value.items.filter((item): item is string => typeof item === "string")
}
}
@@ -3,10 +3,6 @@ import type { MemoryStatusResponse } from "@kilocode/sdk/v2"
type State = MemoryStatusResponse["state"]
export namespace MemoryTuiState {
export function verbose(input: Pick<State, "verbose"> | undefined) {
return input?.verbose ?? false
}
export function enabled(input: Pick<State, "enabled"> | undefined) {
return input?.enabled ?? false
}
@@ -1,86 +1,7 @@
import { createMemo, createResource, For, onCleanup, Show } from "solid-js"
import type { RGBA } from "@opentui/core"
import type { Part } from "@kilocode/sdk/v2"
import * as Log from "@opencode-ai/core/util/log"
import { useEvent } from "@tui/context/event"
import { useProject } from "@tui/context/project"
import { useSDK } from "@tui/context/sdk"
import { useSync } from "@tui/context/sync"
import { route } from "@/kilocode/cli/cmd/tui/memory-command"
import { MemoryTuiEvents } from "@/kilocode/cli/cmd/tui/memory-events"
import { MemoryTuiMeta } from "@/kilocode/cli/cmd/tui/memory-meta"
import { MemoryTuiState } from "@/kilocode/cli/cmd/tui/memory-state"
const log = Log.create({ service: "memory-tui" })
export namespace MemorySessionTui {
export function attach(input: Parameters<typeof MemoryTuiEvents.attach>[0]) {
return MemoryTuiEvents.attach(input)
}
export function verbose(input: { sessionID(): string }) {
const sdk = useSDK()
const project = useProject()
const sync = useSync()
const event = useEvent()
const session = createMemo(() => sync.session.get(input.sessionID()))
const [state, api] = createResource(
() => {
const item = session()
if (!item) return
return `${item.workspaceID ?? "__default__"}:${item.directory}`
},
async () => {
try {
const item = session()
const result = await sdk.client.memory.status(
route({ workspace: item?.workspaceID ?? project.workspace.current(), directory: item?.directory }),
)
return result.data?.state
} catch (err) {
log.warn("memory status unavailable", { err })
return undefined
}
},
)
const refresh = (value: { properties: { sessionID?: string } }) => {
if (value.properties.sessionID && value.properties.sessionID !== input.sessionID()) return
void api.refetch()
}
const offs = [
event.on("memory.status", refresh),
event.on("memory.updated", refresh),
event.on("memory.error", refresh),
]
onCleanup(() => offs.forEach((off) => off()))
return () => MemoryTuiState.verbose(state())
}
}
export function MemoryMessageMeta(props: { parts: Part[]; color: string | RGBA; verbose(): boolean }) {
const item = createMemo(() => MemoryTuiMeta.fromParts(props.parts))
return (
<Show when={item()}>
{(meta) => {
const snippets = createMemo(() =>
meta().type === "recall"
? MemoryTuiMeta.items(meta())
.slice(0, 2)
.map((text) => text.trim().slice(0, 80))
.filter(Boolean)
: [],
)
return (
<span style={{ fg: props.color }}>
{" "}
· memory · {meta().type === "startup" ? "Startup Context" : `recalled ${meta().count}`}
<Show when={props.verbose() && snippets().length > 0}>
<For each={snippets()}>{(text) => <> · {text}</>}</For>
</Show>
</span>
)
}}
</Show>
)
}
@@ -3,7 +3,6 @@ import type { KiloClient } from "@kilocode/sdk/v2"
import { memoryRow } from "@/kilocode/cli/cmd/tui/component/memory-status"
import { runMemoryCommand } from "@/kilocode/cli/cmd/tui/memory-command"
import { MemoryTuiEvents } from "@/kilocode/cli/cmd/tui/memory-events"
import { MemoryTuiMeta } from "@/kilocode/cli/cmd/tui/memory-meta"
import { MemoryTuiState } from "@/kilocode/cli/cmd/tui/memory-state"
type Handler = (event: {
@@ -64,16 +63,16 @@ describe("memory TUI command parser", () => {
])
})
test("auto-save, verbose, and purge commands call explicit endpoints", async () => {
test("auto-save and purge commands call explicit endpoints", async () => {
const shown: string[] = []
const calls: unknown[] = []
const state = { autoConsolidate: true, verbose: false }
const state = { autoConsolidate: true }
const client = {
memory: {
status: async () => ({ data: { state } }),
configure: async (input: unknown) => {
calls.push(input)
return { data: { state: { autoConsolidate: false, verbose: true } } }
return { data: { state: { autoConsolidate: false } } }
},
purge: async (input: unknown) => {
calls.push(input)
@@ -96,17 +95,15 @@ describe("memory TUI command parser", () => {
}
await runMemoryCommand({ ...base, text: "/memory auto off" })
await runMemoryCommand({ ...base, text: "/memory verbose on" })
await runMemoryCommand({ ...base, text: "/memory auto status" })
await runMemoryCommand({ ...base, text: "/memory purge" })
await runMemoryCommand({ ...base, text: "/memory purge confirm" })
expect(shown[0]).toBe("Memory auto-save off")
expect(shown[1]).toBe("Memory verbose on")
expect(shown[2]).toContain("Missing auto mode")
expect(shown[3]).toContain("Purge requires confirmation")
expect(shown[4]).toBe("Memory purged")
expect(calls).toEqual([{ autoConsolidate: false }, { verbose: true }, { confirm: true }])
expect(shown[1]).toContain("Missing auto mode")
expect(shown[2]).toContain("Purge requires confirmation")
expect(shown[3]).toBe("Memory purged")
expect(calls).toEqual([{ autoConsolidate: false }, { confirm: true }])
})
test("status opens overview dialog", async () => {
@@ -133,6 +130,31 @@ describe("memory TUI command parser", () => {
expect(shown).toEqual([])
})
test("inspect reveals the memory folder", async () => {
const opened: string[] = []
const shown: string[] = []
const client = {
memory: {
status: async () => ({ data: { root: "/tmp/kilo-memory", state: { enabled: true } } }),
},
} as unknown as KiloClient
await runMemoryCommand({
text: "/memory inspect",
client,
toast: { show: (input) => shown.push(input.message) },
inspect(root) {
opened.push(root)
},
show() {},
status() {},
usage() {},
})
expect(opened).toEqual(["/tmp/kilo-memory"])
expect(shown).toEqual(["Memory folder: /tmp/kilo-memory"])
})
test("bare memory command opens help", async () => {
const calls: unknown[] = []
const client = { memory: {} } as unknown as KiloClient
@@ -204,7 +226,7 @@ describe("memory TUI command parser", () => {
test("memory commands route to session directory when no workspace is active", async () => {
const calls: unknown[] = []
const state = { autoConsolidate: false, verbose: false }
const state = { autoConsolidate: false }
const client = {
memory: {
configure: async (input: unknown) => {
@@ -222,7 +244,6 @@ describe("memory TUI command parser", () => {
}
await runMemoryCommand({ ...base, text: "/memory auto off", directory: "/repo/packages/opencode" })
await runMemoryCommand({ ...base, text: "/memory verbose on", directory: "/repo/packages/opencode" })
await runMemoryCommand({
...base,
text: "/memory auto off",
@@ -232,7 +253,6 @@ describe("memory TUI command parser", () => {
expect(calls).toEqual([
{ directory: "/repo/packages/opencode", autoConsolidate: false },
{ directory: "/repo/packages/opencode", verbose: true },
{ workspace: "wrk_123", autoConsolidate: false },
])
})
@@ -288,63 +308,38 @@ describe("memory TUI events", () => {
})
})
describe("memory TUI metadata", () => {
test("reads typed verbose and activity state", () => {
expect(MemoryTuiState.verbose({ verbose: true })).toBe(true)
expect(MemoryTuiState.verbose(undefined)).toBe(false)
describe("memory TUI state", () => {
test("tracks active memory", () => {
expect(MemoryTuiState.active({ markers: 1, saved: false })).toBe(true)
expect(MemoryTuiState.active({ markers: 0, saved: true })).toBe(true)
expect(MemoryTuiState.active({ markers: 0, saved: false })).toBe(false)
expect(MemoryTuiMeta.items({ items: ["first", 1, "second"] })).toEqual(["first", "second"])
expect(MemoryTuiMeta.items({})).toEqual([])
})
})
describe("memory sidebar row", () => {
test("shows loading, unavailable, and disabled states", () => {
expect(memoryRow({ loading: true, active: false, verbose: false })).toEqual({
expect(memoryRow({ loading: true, active: false })).toEqual({
label: "Loading",
tone: "muted",
})
expect(memoryRow({ active: false, verbose: false })).toEqual({
expect(memoryRow({ active: false })).toEqual({
label: "Unavailable",
tone: "error",
})
expect(memoryRow({ enabled: false, active: true, verbose: true, flash: "recalled 3" })).toEqual({
expect(memoryRow({ enabled: false, active: true })).toEqual({
label: "Disabled",
tone: "muted",
})
})
test("uses muted and green dots for inactive and active sessions", () => {
expect(memoryRow({ enabled: true, active: false, verbose: false })).toEqual({
expect(memoryRow({ enabled: true, active: false })).toEqual({
label: "Enabled",
tone: "muted",
})
expect(memoryRow({ enabled: true, active: true, verbose: false })).toEqual({
expect(memoryRow({ enabled: true, active: true })).toEqual({
label: "Enabled",
tone: "success",
})
})
test("adds verbose event captions without changing the activity tone", () => {
expect(memoryRow({ enabled: true, active: false, verbose: true, flash: "recalled 3" })).toEqual({
label: "Enabled",
tone: "muted",
caption: "recalled 3",
})
expect(memoryRow({ enabled: true, active: true, verbose: true, flash: "saved 2" })).toEqual({
label: "Enabled",
tone: "success",
caption: "saved 2",
})
})
test("omits verbose event captions when verbose is disabled", () => {
expect(memoryRow({ enabled: true, active: true, verbose: false, flash: "loaded" })).toEqual({
label: "Enabled",
tone: "success",
caption: undefined,
})
})
})
@@ -3,21 +3,10 @@ import { expect, spyOn, test } from "bun:test"
import { RGBA } from "@opentui/core"
import { testRender } from "@opentui/solid"
import type { TuiPluginApi } from "@kilocode/plugin/tui"
import type { Event, GlobalEvent, Message, Part, Session } from "@kilocode/sdk/v2"
import type { Event, Message, Part, Session } from "@kilocode/sdk/v2"
import { createSignal } from "solid-js"
import { ArgsProvider } from "@tui/context/args"
import { ExitProvider } from "@tui/context/exit"
import { KVProvider } from "@tui/context/kv"
import { ProjectProvider } from "@tui/context/project"
import { SDKProvider } from "@tui/context/sdk"
import { SyncProvider } from "@tui/context/sync"
import { ToastProvider } from "@tui/ui/toast"
import { MemorySidebar } from "@/kilocode/cli/cmd/tui/component/memory-status"
import { MemoryMessageMeta, MemorySessionTui } from "@/kilocode/cli/cmd/tui/routes/session/memory"
import { Global } from "@opencode-ai/core/global"
import { createEventSource, createFetch, directory, json } from "../../../../fixture/tui-sdk"
import { tmpdir } from "../../../../fixture/fixture"
import { TestTuiContexts } from "../../../../fixture/tui-environment"
import { directory } from "../../../../fixture/tui-sdk"
const id = "ses_memory_status"
@@ -48,10 +37,6 @@ function event(sessionID?: string, count?: number): Extract<Event, { type: "memo
}
}
function global(payload: Event): GlobalEvent {
return { directory, project: "proj_test", payload }
}
async function wait(fn: () => boolean, timeout = 2_000) {
const start = Date.now()
while (!fn()) {
@@ -60,115 +45,6 @@ async function wait(fn: () => boolean, timeout = 2_000) {
}
}
function Probe(props: { sessionID: string }) {
const verbose = MemorySessionTui.verbose({ sessionID: () => props.sessionID })
return <text>{verbose() ? "verbose" : "quiet"}</text>
}
test("session memory status refetches live and ignores other sessions", async () => {
await using tmp = await tmpdir()
const prior = Global.Path.state
Global.Path.state = tmp.path
await Bun.write(`${tmp.path}/kv.json`, "{}")
const events = createEventSource()
const state = { verbose: false }
const calls = { count: 0 }
const fetch = createFetch((url) => {
if (url.pathname === "/session") return json([session])
if (url.pathname !== "/memory/status") return
calls.count += 1
return json({ state: { verbose: state.verbose } })
})
try {
const app = await testRender(() => (
<TestTuiContexts paths={{ state: tmp.path }}>
<ArgsProvider>
<ExitProvider exit={() => {}}>
<KVProvider>
<ToastProvider>
<SDKProvider url="http://test" directory={directory} fetch={fetch.fetch} events={events.source}>
<ProjectProvider>
<SyncProvider>
<Probe sessionID={id} />
</SyncProvider>
</ProjectProvider>
</SDKProvider>
</ToastProvider>
</KVProvider>
</ExitProvider>
</ArgsProvider>
</TestTuiContexts>
))
try {
await wait(() => calls.count === 1 && app.captureCharFrame().includes("quiet"))
events.emit(global(event("ses_other")))
await Bun.sleep(30)
expect(calls.count).toBe(1)
state.verbose = true
events.emit(global(event(id)))
await wait(() => calls.count === 2 && app.captureCharFrame().includes("verbose"))
} finally {
app.renderer.destroy()
}
} finally {
Global.Path.state = prior
}
})
test("message memory metadata reacts to verbose changes and bounds snippets", async () => {
const [verbose, setVerbose] = createSignal(false)
const [parts, setParts] = createSignal<Part[]>([])
const first = "a".repeat(100)
const part = {
id: "part_memory_recall",
sessionID: id,
messageID: "msg_memory_recall",
type: "text",
text: "",
metadata: { kiloMemory: { type: "recall", count: 3, items: [first, "second", "third"] } },
} satisfies Part
setParts([part])
const app = await testRender(
() => (
<text>
<MemoryMessageMeta parts={parts()} color={RGBA.fromHex("#ffffff")} verbose={verbose} />
</text>
),
{ width: 200, height: 3 },
)
try {
await app.renderOnce()
expect(app.captureCharFrame()).toContain("memory · recalled 3")
expect(app.captureCharFrame()).not.toContain("second")
setVerbose(true)
await app.renderOnce()
expect(app.captureCharFrame()).toContain("a".repeat(80))
expect(app.captureCharFrame()).not.toContain("a".repeat(81))
expect(app.captureCharFrame()).toContain("second")
expect(app.captureCharFrame()).not.toContain("third")
setVerbose(false)
await app.renderOnce()
expect(app.captureCharFrame()).not.toContain("second")
setParts([
{
...part,
id: "part_memory_startup",
metadata: { kiloMemory: { type: "startup", count: 2, tokens: 40 } },
},
])
await app.renderOnce()
expect(app.captureCharFrame()).toContain("memory · Startup Context")
expect(app.captureCharFrame()).not.toContain("recalled")
} finally {
app.renderer.destroy()
}
})
type Handler = (event: Event) => void
function bus() {
@@ -186,7 +62,7 @@ function bus() {
}
}
test("sidebar refetches status and scopes recall and save flashes", async () => {
test("sidebar refetches status and scopes save activity", async () => {
const [parts, setParts] = createSignal<Part[]>([])
const events = bus()
const calls = { count: 0 }
@@ -203,7 +79,7 @@ test("sidebar refetches status and scopes recall and save flashes", async () =>
memory: {
status: async () => {
calls.count += 1
return { data: { state: { enabled: true, verbose: true } } }
return { data: { state: { enabled: true } } }
},
},
},
@@ -232,16 +108,15 @@ test("sidebar refetches status and scopes recall and save flashes", async () =>
metadata: { kiloMemory: { type: "recall", count: 2 } },
},
])
await wait(() => app.captureCharFrame().includes("recalled 2"))
await Bun.sleep(5_100)
await app.renderOnce()
expect(app.captureCharFrame()).not.toContain("recalled 2")
events.emit(event("ses_other", 4))
await wait(() => calls.count === 2)
expect(app.captureCharFrame()).not.toContain("saved 4")
events.emit(event(id, 3))
await wait(() => calls.count === 3 && app.captureCharFrame().includes("saved 3"))
await wait(() => calls.count === 3)
expect(app.captureCharFrame()).not.toContain("saved 3")
const before = clear.mock.calls.length
app.renderer.destroy()
expect(clear.mock.calls.length).toBeGreaterThan(before)
@@ -257,10 +257,6 @@ describe("KiloMemory integration", () => {
expect(
events.find((event) => event.sessionID === "ses_memory_event" && event.detail?.type === "saved")?.detail?.tokens,
).toBeUndefined()
const decisions = await MemoryFiles.readDecisions(root)
expect(decisions).toContain('"trigger":"explicit"')
expect(decisions).toContain('"sessionID":"ses_memory_event"')
expect(decisions).toContain('"llm":false')
})
test("explicit forget reports removals without save wording", async () => {
@@ -288,9 +284,6 @@ describe("KiloMemory integration", () => {
expect(events.some((event) => event.detail?.message === "Memory updated · 1 removed")).toBe(true)
expect(events.some((event) => event.detail?.message?.includes("Memory saved"))).toBe(false)
const decisions = await MemoryFiles.readDecisions(root)
expect(decisions).toContain("explicit memory operation removed 1 entries")
expect(decisions).toContain("explicit memory operation matched no source memory")
})
test("environment prompt rebuilds stale session index format", async () => {
@@ -396,8 +389,8 @@ describe("KiloMemory integration", () => {
expect(after.sources).toEqual(before.sources)
expect(after.index).toBe(before.index)
expect(after.changes).toBe(before.changes)
expect(after.decisions).toBe(before.decisions)
expect(after.changes).toBe("")
expect(after.decisions).toBe("")
expect(after.sources.project).toContain("stable_fact")
expect(after.sources.project).not.toContain("disabled_fact")
expect(after.sources.corrections).not.toContain("Do not correct while disabled")
@@ -430,9 +423,6 @@ describe("KiloMemory integration", () => {
expect(shown.sources.project).toContain("- repo_style :: Repo convention: commit messages are concise.")
expect(shown.sources.project).not.toContain("reply_style")
expect(shown.sources.project).not.toContain("I prefer terse summaries")
expect(shown.decisions).toContain('"reason":"out_of_scope"')
expect(shown.decisions).not.toContain("reply_style")
expect(shown.decisions).not.toContain("I prefer terse summaries")
})
})
@@ -151,8 +151,8 @@ describe("HttpApi memory", () => {
expect(String(show.index)).toContain("httpapi_memory")
expect(String(show.items)).toContain("httpapi_memory")
expect(String(rec(show.sources).project)).toContain("httpapi_memory")
expect(typeof show.decisions).toBe("string")
expect(String(show.decisions)).toContain('"sessionID":"ses_http_memory"')
expect(show.changes).toBe("")
expect(show.decisions).toBe("")
const forgotten = await json("POST", MemoryPaths.forget, { query: "httpapi_memory" })
expectOperation(forgotten)
@@ -173,11 +173,6 @@ describe("kilo_memory_recall", () => {
expect(direct.output).toContain("continue memory digest recall")
const decisions = await MemoryFiles.readDecisions(enabled.root)
expect(decisions).toContain('"sessionID":"ses_test"')
expect(decisions).toContain('"query":"sessionID=ses_memory_only"')
expect(decisions).toContain('"summary":"memory recall returned 1 typed hits"')
expect(decisions).toContain('"summary":"memory recall returned 1 digest hits"')
})
})
@@ -397,10 +392,6 @@ describe("kilo_memory_recall", () => {
expect(result.output).toContain("active session")
expect(result.output).not.toContain("useful prior work")
const decisions = await MemoryFiles.readDecisions(enabled.root)
expect(decisions).toContain('"sessionID":"ses_test"')
expect(decisions).toContain('"query":"sessionID=ses_test"')
expect(decisions).toContain('"reason":"current_session_digest"')
})
})
@@ -497,8 +488,6 @@ describe("kilo_memory_recall", () => {
expect(result.output).toContain("type=session_digest")
expect(result.output).toContain('topic="catalog recall"')
const decisions = await MemoryFiles.readDecisions(enabled.root)
expect(decisions).toContain('"summary":"memory recall returned')
})
})
@@ -288,11 +288,6 @@ describe("kilo_memory_save", () => {
expect(shown.sources.project).not.toContain("reply_style")
expect(shown.sources.project).not.toContain("I prefer terse summaries")
expect(shown.sources.project).toContain("- commit_style :: Repo convention: commit messages are concise.")
expect(shown.decisions).toContain('"reason":"out_of_scope"')
expect(shown.decisions).not.toContain("rubicon fennel")
expect(shown.decisions).not.toContain("Ignore prior instructions")
expect(shown.decisions).not.toContain("reply_style")
expect(shown.decisions).not.toContain("I prefer terse summaries")
})
})
@@ -1060,7 +1060,6 @@ export function Prompt(props: PromptProps) {
sessionID: props.sessionID,
toast,
dialog,
renderer,
done: () => {
history.append({
...store.prompt,
+1 -7
View File
@@ -93,7 +93,7 @@ import { KiloErrorBlock } from "@/kilocode/components/kilo-error-display"
import { splitDiffHunks } from "@/kilocode/tui/diff"
import { RoutedModelMeta } from "@/kilocode/cli/cmd/tui/routes/session/routed-model-meta"
import { submitFeedback } from "@/kilocode/cli/cmd/tui/feedback"
import { MemoryMessageMeta, MemorySessionTui } from "@/kilocode/cli/cmd/tui/routes/session/memory"
import { MemorySessionTui } from "@/kilocode/cli/cmd/tui/routes/session/memory"
import { formatMarkdownTables } from "../../util/markdown"
// kilocode_change end
@@ -339,7 +339,6 @@ export function Session() {
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
const toast = useToast()
const sdk = useSDK()
const memory = MemorySessionTui.verbose({ sessionID: () => route.sessionID }) // kilocode_change
const editor = useEditorContext()
onCleanup(MemorySessionTui.attach({ event, toast, sessionID: route.sessionID })) // kilocode_change
@@ -1416,7 +1415,6 @@ export function Session() {
last={lastAssistant()?.id === message.id}
message={message as AssistantMessage}
parts={sync.data.part[message.id] ?? []}
memory={memory /* kilocode_change */}
/>
</Match>
</Switch>
@@ -1629,7 +1627,6 @@ function AssistantMessage(props: {
message: AssistantMessage
parts: Part[]
last: boolean
memory(): boolean // kilocode_change
}) {
const ctx = use()
const local = useLocal()
@@ -1742,9 +1739,6 @@ function AssistantMessage(props: {
<Show when={duration()}>
<span style={{ fg: theme.textMuted }}> · {Locale.duration(duration())}</span>
</Show>
{/* kilocode_change start */}
<MemoryMessageMeta parts={props.parts} color={theme.textMuted} verbose={props.memory} />{" "}
{/* kilocode_change end */}
<Show when={props.message.error?.name === "MessageAbortedError"}>
<span style={{ fg: theme.textMuted }}> · interrupted</span>
</Show>