mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-10-30 19:32:39 +08:00
Merge branch 'main' into fix/question-reply-locks
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Keep large chat transcripts responsive by mounting only viewport-visible conversation rows.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
"@kilocode/cli": patch
|
||||
"@kilocode/kilo-gateway": patch
|
||||
---
|
||||
|
||||
Show model free and prompt-training indicators only when their explicit catalog metadata is enabled.
|
||||
@@ -74,6 +74,7 @@ export const Model = Schema.Struct({
|
||||
recommendedIndex: Schema.optional(Schema.Finite),
|
||||
prompt: Schema.optional(Schema.String),
|
||||
isFree: Schema.optional(Schema.Boolean),
|
||||
mayTrainOnYourPrompts: Schema.optional(Schema.Boolean),
|
||||
ai_sdk_provider: Schema.optional(Schema.String),
|
||||
// kilocode_change end
|
||||
experimental: Schema.optional(
|
||||
|
||||
@@ -36,6 +36,7 @@ const openRouterModelSchema = z.object({
|
||||
supported_parameters: z.array(z.string()).optional(),
|
||||
preferredIndex: z.number().optional(),
|
||||
isFree: z.boolean().optional(),
|
||||
mayTrainOnYourPrompts: z.boolean().optional(),
|
||||
terminalBench: z
|
||||
.object({
|
||||
overallScore: z.number(),
|
||||
@@ -190,6 +191,7 @@ function transformToModelDevFormat(model: OpenRouterModel): any {
|
||||
ai_sdk_provider: model.opencode?.ai_sdk_provider,
|
||||
tool_call: supportsTools,
|
||||
isFree: model.isFree,
|
||||
mayTrainOnYourPrompts: model.mayTrainOnYourPrompts,
|
||||
...(model.terminalBench && { terminalBench: model.terminalBench }),
|
||||
...(inputPrice !== undefined &&
|
||||
outputPrice !== undefined && {
|
||||
|
||||
@@ -15,6 +15,8 @@ const VALID_RESPONSE = JSON.stringify({
|
||||
output_modalities: ["text"],
|
||||
},
|
||||
supported_parameters: ["tools", "temperature"],
|
||||
isFree: false,
|
||||
mayTrainOnYourPrompts: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -143,7 +145,10 @@ test("returns models without error on success", async () => {
|
||||
;(globalThis as any).fetch = orig
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(Object.keys(result.models).length).toBeGreaterThan(0)
|
||||
expect(result.models["test/model-a"]).toMatchObject({
|
||||
isFree: false,
|
||||
mayTrainOnYourPrompts: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("preserves Terminal Bench metadata as a dedicated model field", async () => {
|
||||
|
||||
@@ -22,6 +22,15 @@ describe("model preview data collection line", () => {
|
||||
expect(styles).toContain(".model-preview-data-line")
|
||||
})
|
||||
|
||||
it("renders prompt training independently from the free badge", () => {
|
||||
expect(selector).toMatch(
|
||||
/<Show when=\{isFree\(model\)\}>[\s\S]*?<\/Show>\s*<Show when=\{isDataCollectedModel\(model\)\}>/,
|
||||
)
|
||||
expect(preview).toMatch(
|
||||
/<Show when=\{model\(\)\.isFree\}>[\s\S]*?<\/Show>\s*<Show when=\{isDataCollectedModel\(model\(\)\)\}>/,
|
||||
)
|
||||
})
|
||||
|
||||
it("uses the book open check icon for all webview model data disclosures", () => {
|
||||
expect(selector).toContain('Icon name="book-open-check"')
|
||||
expect(selector).not.toContain('Icon name="warning"')
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
PROVIDER_ORDER,
|
||||
freeDataLabel,
|
||||
isDataCollectedModel,
|
||||
isFree,
|
||||
} from "../../webview-ui/src/components/shared/model-selector-utils"
|
||||
|
||||
const labels = { select: "Select model", noProviders: "No providers", notSet: "Not set" }
|
||||
@@ -102,11 +103,19 @@ describe("freeDataLabel", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("isFree", () => {
|
||||
it("uses only explicit free metadata", () => {
|
||||
expect(isFree({ isFree: true })).toBe(true)
|
||||
expect(isFree({ isFree: false })).toBe(false)
|
||||
expect(isFree({})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isDataCollectedModel", () => {
|
||||
it("only marks free Kilo Gateway models with the training disclosure", () => {
|
||||
expect(isDataCollectedModel({ providerID: KILO_GATEWAY_ID, isFree: true })).toBe(true)
|
||||
expect(isDataCollectedModel({ providerID: "openrouter", isFree: true })).toBe(false)
|
||||
expect(isDataCollectedModel({ providerID: KILO_GATEWAY_ID, isFree: false })).toBe(false)
|
||||
it("uses only explicit prompt training metadata", () => {
|
||||
expect(isDataCollectedModel({ mayTrainOnYourPrompts: true })).toBe(true)
|
||||
expect(isDataCollectedModel({ mayTrainOnYourPrompts: false })).toBe(false)
|
||||
expect(isDataCollectedModel({})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -19,12 +19,20 @@ describe("TaskTimeline delegated tooltip contract", () => {
|
||||
expect(src).not.toMatch(/<Tooltip\b/)
|
||||
})
|
||||
|
||||
it("keeps bar labels and renders one delegated portal tooltip", () => {
|
||||
expect(src).toMatch(/data-tip=\{bar\(\)\.tip\}/)
|
||||
expect(src).toMatch(/role="img"/)
|
||||
expect(src).toMatch(/aria-label=\{bar\(\)\.tip\}/)
|
||||
expect(src).toMatch(/if \(!bar \|\| !ref\?\.contains\(bar\)\) return hideTip\(\)/)
|
||||
it("delegates SVG hit testing to one portal tooltip", () => {
|
||||
expect(src).toMatch(/hit\(layout\(\)\.items, e\.clientX - rect\.left \+ ref\.scrollLeft\)/)
|
||||
expect(src).toMatch(/const bar = bars\(\)\[idx\]/)
|
||||
expect(src).toMatch(/text: bar\.tip/)
|
||||
expect(src).toMatch(/<Portal>/)
|
||||
expect(src).toMatch(/class="task-timeline-tooltip"/)
|
||||
})
|
||||
|
||||
it("keeps accessibility and bar overlays bounded", () => {
|
||||
expect(src).toMatch(/data-timeline-count=\{bars\(\)\.length\}/)
|
||||
expect(src).toMatch(/tabIndex=\{0\}/)
|
||||
expect(src).toMatch(/aria-label=\{aria\(\)\}/)
|
||||
expect(src).toMatch(/<For each=\{layout\(\)\.paths\}>/)
|
||||
expect(src).not.toMatch(/<Index\b/)
|
||||
expect(src).not.toMatch(/data-tip=/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { geometry, hit, navigate } from "../../webview-ui/src/utils/timeline/geometry"
|
||||
|
||||
describe("timeline geometry", () => {
|
||||
const bars = [
|
||||
{ bg: "blue", width: 3, height: 4 },
|
||||
{ bg: "red", width: 5, height: 8 },
|
||||
{ bg: "blue", width: 2, height: 6 },
|
||||
]
|
||||
|
||||
it("preserves visual positions while grouping paths by color", () => {
|
||||
const result = geometry(bars, 10)
|
||||
|
||||
expect(result.width).toBe(13)
|
||||
expect(result.items.map((item) => [item.idx, item.x])).toEqual([
|
||||
[0, 0],
|
||||
[1, 4],
|
||||
[2, 10],
|
||||
])
|
||||
expect(result.paths).toHaveLength(2)
|
||||
expect(result.paths.map((path) => path.bg)).toEqual(["blue", "red"])
|
||||
expect(result.paths[0]!.d).toContain("M0,10")
|
||||
expect(result.paths[0]!.d).toContain("M10,10")
|
||||
expect(result.paths[1]!.d).toContain("M4,10")
|
||||
})
|
||||
|
||||
it("hit tests bars but not their gaps", () => {
|
||||
const items = geometry(bars, 10).items
|
||||
|
||||
expect(hit(items, 0)).toBe(0)
|
||||
expect(hit(items, 2.99)).toBe(0)
|
||||
expect(hit(items, 3)).toBe(-1)
|
||||
expect(hit(items, 4)).toBe(1)
|
||||
expect(hit(items, 12)).toBe(-1)
|
||||
})
|
||||
|
||||
it("navigates in visual order with bounded endpoints", () => {
|
||||
expect(navigate(-1, 3, "ArrowRight")).toBe(0)
|
||||
expect(navigate(1, 3, "ArrowLeft")).toBe(0)
|
||||
expect(navigate(2, 3, "ArrowRight")).toBe(2)
|
||||
expect(navigate(1, 3, "Home")).toBe(0)
|
||||
expect(navigate(1, 3, "End")).toBe(2)
|
||||
expect(navigate(1, 3, "Escape")).toBe(1)
|
||||
expect(navigate(0, 0, "ArrowRight")).toBe(-1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,86 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test"
|
||||
import type { CacheSnapshot } from "virtua"
|
||||
import {
|
||||
getMeasurement,
|
||||
getScroll,
|
||||
layoutFingerprint,
|
||||
resetTranscriptCaches,
|
||||
resolveAnchor,
|
||||
rowFingerprint,
|
||||
setMeasurement,
|
||||
setScroll,
|
||||
} from "../../webview-ui/src/components/chat/transcript-cache"
|
||||
|
||||
const snapshot = (id: number) => ({ id }) as unknown as CacheSnapshot
|
||||
const layout = layoutFingerprint({ width: 800, ratio: 2, font: "Kilo Sans", size: "13px", line: "20px" })
|
||||
|
||||
describe("transcript measurement cache", () => {
|
||||
beforeEach(resetTranscriptCaches)
|
||||
|
||||
it("returns a cache only for the exact row and layout fingerprints", () => {
|
||||
const keys = rowFingerprint(["a", "bc"])
|
||||
const cache = snapshot(1)
|
||||
setMeasurement("session", keys, layout, cache)
|
||||
|
||||
expect(getMeasurement("session", keys, layout)).toBe(cache)
|
||||
expect(getMeasurement("session", rowFingerprint(["a", "bd"]), layout)).toBeUndefined()
|
||||
expect(getMeasurement("session", keys, layout)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("invalidates a measurement when layout changes", () => {
|
||||
const keys = rowFingerprint(["row"])
|
||||
setMeasurement("session", keys, layout, snapshot(1))
|
||||
const changed = layoutFingerprint({ width: 801, ratio: 2, font: "Kilo Sans", size: "13px", line: "20px" })
|
||||
|
||||
expect(getMeasurement("session", keys, changed)).toBeUndefined()
|
||||
expect(getMeasurement("session", keys, layout)).toBeUndefined()
|
||||
})
|
||||
|
||||
it("uses collision-safe row and layout fingerprints", () => {
|
||||
expect(rowFingerprint(["a|b", "c"])).not.toBe(rowFingerprint(["a", "b|c"]))
|
||||
expect(layoutFingerprint({ width: 80, ratio: 1, font: "a|b", size: "c", line: "d" })).not.toBe(
|
||||
layoutFingerprint({ width: 80, ratio: 1, font: "a", size: "b|c", line: "d" }),
|
||||
)
|
||||
})
|
||||
|
||||
it("evicts the least recently used measurement after 16 sessions", () => {
|
||||
const keys = rowFingerprint(["row"])
|
||||
for (let i = 0; i < 16; i += 1) setMeasurement(`s${i}`, keys, layout, snapshot(i))
|
||||
expect(getMeasurement("s0", keys, layout)).toBeDefined()
|
||||
setMeasurement("s16", keys, layout, snapshot(16))
|
||||
|
||||
expect(getMeasurement("s1", keys, layout)).toBeUndefined()
|
||||
expect(getMeasurement("s0", keys, layout)).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("transcript scroll cache", () => {
|
||||
beforeEach(resetTranscriptCaches)
|
||||
|
||||
it("stores bottom-follow and anchored positions independently from measurements", () => {
|
||||
setMeasurement("a", rowFingerprint(["row"]), layout, snapshot(1))
|
||||
setScroll("a", { type: "bottom" })
|
||||
setScroll("b", { type: "anchor", key: "row-2", offset: 37 })
|
||||
|
||||
expect(getScroll("a")).toEqual({ type: "bottom" })
|
||||
expect(getScroll("b")).toEqual({ type: "anchor", key: "row-2", offset: 37 })
|
||||
})
|
||||
|
||||
it("resolves an anchor after prepended rows shift its index", () => {
|
||||
const state = { type: "anchor", key: "stable", offset: 19 } as const
|
||||
expect(resolveAnchor(state, ["stable", "tail"])).toEqual({ index: 0, offset: 19 })
|
||||
expect(resolveAnchor(state, ["older-1", "older-2", "stable", "tail"])).toEqual({ index: 2, offset: 19 })
|
||||
expect(resolveAnchor(state, ["other"])).toBeUndefined()
|
||||
expect(resolveAnchor({ type: "bottom" }, ["stable"])).toBeUndefined()
|
||||
})
|
||||
|
||||
it("evicts the least recently used scroll state after 50 sessions", () => {
|
||||
for (let i = 0; i < 50; i += 1) setScroll(`s${i}`, { type: "bottom" })
|
||||
expect(getScroll("s0")).toEqual({ type: "bottom" })
|
||||
setScroll("s50", { type: "anchor", key: "row", offset: 4 })
|
||||
|
||||
expect(getScroll("s1")).toBeUndefined()
|
||||
expect(getScroll("s0")).toEqual({ type: "bottom" })
|
||||
expect(getScroll("s50")).toEqual({ type: "anchor", key: "row", offset: 4 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,219 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { messageTurns } from "../../webview-ui/src/context/session-queue"
|
||||
import { partitionRows, transcriptRows } from "../../webview-ui/src/context/transcript-rows"
|
||||
import type { Message, Part } from "../../webview-ui/src/types/messages"
|
||||
|
||||
const base = {
|
||||
sessionID: "session",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
time: { created: 1 },
|
||||
}
|
||||
|
||||
const user = (id: string, opts: Partial<Message> = {}): Message => ({ ...base, id, role: "user", ...opts })
|
||||
const assistant = (id: string, parentID: string, opts: Partial<Message> = {}): Message => ({
|
||||
...base,
|
||||
id,
|
||||
parentID,
|
||||
role: "assistant",
|
||||
...opts,
|
||||
})
|
||||
const part = (id: string, messageID: string): Part => ({ id, messageID, type: "text", text: id })
|
||||
const lookup = (values: Record<string, Part[]>) => (id: string) => values[id] ?? []
|
||||
|
||||
describe("transcriptRows", () => {
|
||||
it("preserves turn order across user, bounded assistant, diff, and error rows", () => {
|
||||
const u1 = user("u1", { summary: { diffs: [{ file: "a.ts" }] } })
|
||||
const a1 = assistant("a1", "u1")
|
||||
const a2 = assistant("a2", "u1", { error: { name: "ProviderError" } })
|
||||
const u2 = user("u2")
|
||||
const a3 = assistant("a3", "u2")
|
||||
const parts = {
|
||||
u1: [part("up1", "u1")],
|
||||
a1: Array.from({ length: 10 }, (_, i) => part(`p${i}`, "a1")),
|
||||
a2: [part("p10", "a2")],
|
||||
a3: [part("p11", "a3")],
|
||||
}
|
||||
|
||||
const rows = transcriptRows(messageTurns([u1, a1, a2, u2, a3]), lookup(parts))
|
||||
|
||||
expect(rows.map((row) => `${row.turn}:${row.type}`)).toEqual([
|
||||
"u1:user",
|
||||
"u1:assistant",
|
||||
"u1:assistant",
|
||||
"u1:assistant",
|
||||
"u1:diff",
|
||||
"u1:error",
|
||||
"u2:user",
|
||||
"u2:assistant",
|
||||
])
|
||||
expect(rows.filter((row) => row.type === "assistant").map((row) => row.parts.length)).toEqual([8, 2, 1, 1])
|
||||
})
|
||||
|
||||
it("uses the configured bound and keeps an empty assistant renderable", () => {
|
||||
const u1 = user("u1")
|
||||
const a1 = assistant("a1", "u1")
|
||||
const a2 = assistant("a2", "u1")
|
||||
const rows = transcriptRows(
|
||||
messageTurns([u1, a1, a2]),
|
||||
lookup({ a1: Array.from({ length: 7 }, (_, i) => part(`p${i}`, "a1")) }),
|
||||
{ size: 3 },
|
||||
)
|
||||
|
||||
expect(rows.filter((row) => row.type === "assistant").map((row) => row.parts.length)).toEqual([3, 3, 1, 0])
|
||||
})
|
||||
|
||||
it("omits synthetic users for partial turns and carries row metadata", () => {
|
||||
const a1 = assistant("a1", "u1")
|
||||
const rows = transcriptRows(messageTurns([a1]), lookup({ a1: [part("p1", "a1")] }), {
|
||||
queued: new Set(["u1"]),
|
||||
live: new Set(["u1"]),
|
||||
})
|
||||
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0]).toMatchObject({ type: "assistant", turn: "u1", partial: true, queued: true, live: true })
|
||||
})
|
||||
|
||||
it("places only the first visible non-abort error after diffs", () => {
|
||||
const u1 = user("u1", { summary: { diffs: [{ file: "a.ts" }] } })
|
||||
const a1 = assistant("a1", "u1", { error: { name: "MessageAbortedError" } })
|
||||
const a2 = assistant("a2", "u1", { error: { name: "HiddenError" } })
|
||||
const a3 = assistant("a3", "u1", { error: { name: "ShownError" } })
|
||||
const rows = transcriptRows(messageTurns([u1, a1, a2, a3]), lookup({}), { hidden: (id) => id === "a2" })
|
||||
|
||||
expect(rows.slice(-2).map((row) => row.type)).toEqual(["diff", "error"])
|
||||
expect(rows.at(-1)).toMatchObject({ type: "error", message: a3, error: a3.error })
|
||||
})
|
||||
|
||||
it("keeps keys stable when older turns are prepended and parts are appended", () => {
|
||||
const u1 = user("u1")
|
||||
const a1 = assistant("a1", "u1")
|
||||
const parts = Array.from({ length: 8 }, (_, i) => part(`p${i}`, "a1"))
|
||||
const current = transcriptRows(messageTurns([u1, a1]), lookup({ a1: parts }))
|
||||
const older = user("u0")
|
||||
const next = transcriptRows(messageTurns([older, u1, a1]), lookup({ a1: [...parts, part("p8", "a1")] }))
|
||||
|
||||
expect(next.find((row) => row.type === "user" && row.turn === "u1")?.key).toBe(current[0]?.key)
|
||||
expect(next.find((row) => row.type === "assistant" && row.parts[0]?.id === "p0")?.key).toBe(current[1]?.key)
|
||||
})
|
||||
|
||||
it("reuses unchanged rows across prepend and append updates", () => {
|
||||
const u1 = user("u1")
|
||||
const a1 = assistant("a1", "u1")
|
||||
const u2 = user("u2")
|
||||
const p1 = part("p1", "a1")
|
||||
const first = transcriptRows(messageTurns([u1, a1, u2]), lookup({ a1: [p1] }))
|
||||
const u0 = user("u0")
|
||||
const a2 = assistant("a2", "u2")
|
||||
const second = transcriptRows(messageTurns([u0, u1, a1, u2, a2]), lookup({ a1: [p1] }), {}, first)
|
||||
|
||||
expect(second[1]).toBe(first[0])
|
||||
expect(second[2]).toBe(first[1])
|
||||
expect(second[3]).not.toBe(first[2])
|
||||
expect(second[4]).not.toBe(first[2])
|
||||
})
|
||||
|
||||
it("selects the last real assistant text part as the copy target", () => {
|
||||
const u1 = user("u1")
|
||||
const a1 = assistant("a1", "u1")
|
||||
const a2 = assistant("a2", "u1")
|
||||
const synthetic: Part = { ...part("p2", "a2"), synthetic: true }
|
||||
const blank: Part = { ...part("p3", "a2"), text: " " }
|
||||
const rows = transcriptRows(messageTurns([u1, a1, a2]), lookup({ a1: [part("p1", "a1")], a2: [synthetic, blank] }))
|
||||
|
||||
expect(rows.filter((row) => row.type === "assistant").map((row) => row.copy)).toEqual(["p1", "p1"])
|
||||
})
|
||||
|
||||
it("keeps compaction replies ordered under the compacted turn and respects revert turns", () => {
|
||||
const u1 = user("u1")
|
||||
const a1 = assistant("a1", "u1")
|
||||
const u2 = user("u2", {
|
||||
parts: [{ id: "compact", messageID: "u2", type: "compaction", auto: false }],
|
||||
})
|
||||
const a2 = assistant("a2", "u1")
|
||||
const u3 = user("u3")
|
||||
const turns = messageTurns([u1, a1, u2, a2, u3], "u3")
|
||||
const rows = transcriptRows(turns, (id) => (id === "u2" ? (u2.parts ?? []) : []))
|
||||
|
||||
expect(rows.map((row) => `${row.turn}:${row.message.id}`)).toEqual(["u1:u1", "u1:a1", "u2:u2", "u2:a2"])
|
||||
})
|
||||
|
||||
it("replaces only rows whose data or metadata changed", () => {
|
||||
const u1 = user("u1")
|
||||
const a1 = assistant("a1", "u1")
|
||||
const p1 = part("p1", "a1")
|
||||
const first = transcriptRows(messageTurns([u1, a1]), lookup({ a1: [p1] }))
|
||||
const changed = { ...p1, text: "changed" }
|
||||
const second = transcriptRows(messageTurns([u1, a1]), lookup({ a1: [changed] }), {}, first)
|
||||
|
||||
expect(second[0]).toBe(first[0])
|
||||
expect(second[1]).not.toBe(first[1])
|
||||
|
||||
const live = transcriptRows(messageTurns([u1, a1]), lookup({ a1: [changed] }), { live: new Set(["u1"]) }, second)
|
||||
expect(live[0]).not.toBe(second[0])
|
||||
expect(live[1]).not.toBe(second[1])
|
||||
})
|
||||
})
|
||||
|
||||
describe("partitionRows", () => {
|
||||
it("pins only a bounded live suffix and virtualizes completed history", () => {
|
||||
const u1 = user("u1")
|
||||
const a1 = assistant("a1", "u1")
|
||||
const u2 = user("u2")
|
||||
const a2 = assistant("a2", "u2")
|
||||
const parts = Array.from({ length: 18 }, (_, i) => part(`p${i}`, "a2"))
|
||||
const rows = transcriptRows(messageTurns([u1, a1, u2, a2]), lookup({ a1: [part("old", "a1")], a2: parts }), {
|
||||
live: new Set(["u2"]),
|
||||
})
|
||||
const result = partitionRows(rows)
|
||||
|
||||
expect(result.keep).toEqual([result.virtual.length - 2, result.virtual.length - 1])
|
||||
expect(result.keep.map((idx) => result.virtual[idx]).every((row) => row?.live && row.turn === "u2")).toBe(true)
|
||||
expect(
|
||||
result.keep
|
||||
.flatMap((idx) => {
|
||||
const row = result.virtual[idx]
|
||||
return row?.type === "assistant" ? row.parts : []
|
||||
})
|
||||
.map((item) => item.id),
|
||||
).toEqual(["p8", "p9", "p10", "p11", "p12", "p13", "p14", "p15", "p16", "p17"])
|
||||
expect(result.virtual.some((row) => row.turn === "u1")).toBe(true)
|
||||
expect(result.virtual.some((row) => row.turn === "u2" && row.type === "user")).toBe(true)
|
||||
})
|
||||
|
||||
it("returns completed live metadata to virtual history after queue handoff", () => {
|
||||
const u1 = user("u1")
|
||||
const a1 = assistant("a1", "u1")
|
||||
const u2 = user("u2")
|
||||
const first = transcriptRows(messageTurns([u1, a1, u2]), lookup({ a1: [part("p1", "a1")] }), {
|
||||
live: new Set(["u1"]),
|
||||
queued: new Set(["u2"]),
|
||||
})
|
||||
const active = partitionRows(first)
|
||||
expect(active.keep).toEqual([0, 1])
|
||||
expect(active.keep.map((idx) => active.virtual[idx]?.turn)).toEqual(["u1", "u1"])
|
||||
expect(active.queued.map((row) => row.turn)).toEqual(["u2"])
|
||||
|
||||
const second = transcriptRows(messageTurns([u1, a1, u2]), lookup({ a1: [part("p1", "a1")] }), {
|
||||
live: new Set(["u2"]),
|
||||
})
|
||||
const handed = partitionRows(second)
|
||||
|
||||
expect(handed.keep.map((idx) => handed.virtual[idx]?.turn)).toEqual(["u2"])
|
||||
expect(handed.virtual.filter((row) => row.turn === "u1")).toHaveLength(2)
|
||||
})
|
||||
|
||||
it("keeps queued rows in visual order inside virtual data", () => {
|
||||
const u1 = user("u1")
|
||||
const u2 = user("u2")
|
||||
const rows = transcriptRows(messageTurns([u1, u2]), lookup({}), {
|
||||
live: new Set(["u1"]),
|
||||
queued: new Set(["u2"]),
|
||||
})
|
||||
const result = partitionRows(rows)
|
||||
|
||||
expect(result.virtual.map((row) => row.turn)).toEqual(["u1"])
|
||||
expect(result.keep).toEqual([0])
|
||||
expect(result.queued.map((row) => row.turn)).toEqual(["u2"])
|
||||
expect(result.queued[0]).toMatchObject({ type: "user", queued: true })
|
||||
})
|
||||
})
|
||||
@@ -110,6 +110,7 @@ function matchToolRequest<T extends { tool?: { callID: string; messageID: string
|
||||
|
||||
interface AssistantMessageProps {
|
||||
message: SDKAssistantMessage
|
||||
parts?: SDKPart[]
|
||||
showAssistantCopyPartID?: string | null
|
||||
feedback?: MessageFeedbackControls
|
||||
}
|
||||
@@ -177,7 +178,7 @@ export const AssistantMessage: Component<AssistantMessageProps> = (props) => {
|
||||
const open = createMemo(() => config().terminal_command_display !== "collapsed")
|
||||
|
||||
const parts = createMemo(() => {
|
||||
const stored = data.store.part?.[props.message.id]
|
||||
const stored = props.parts ?? data.store.part?.[props.message.id]
|
||||
if (!stored) return []
|
||||
return (stored as SDKPart[]).filter((part) => isRenderable(part))
|
||||
})
|
||||
|
||||
@@ -20,23 +20,32 @@ import { useLanguage } from "../../context/language"
|
||||
import { recentSessions } from "../../context/session-utils"
|
||||
import { formatRelativeDate } from "../../utils/date"
|
||||
import { FeedbackDialog } from "./FeedbackDialog"
|
||||
import { VscodeSessionTurn } from "./VscodeSessionTurn"
|
||||
import { TranscriptRowView } from "./TranscriptRow"
|
||||
import { RevertBanner } from "./RevertBanner"
|
||||
import { AccountSwitcher } from "../shared/AccountSwitcher"
|
||||
import { KiloNotifications } from "./KiloNotifications"
|
||||
import { WorkingIndicator } from "../shared/WorkingIndicator"
|
||||
import { TurnOutcome } from "../shared/TurnOutcome"
|
||||
import { QuestionDock } from "./QuestionDock"
|
||||
import { Virtualizer } from "virtua/solid"
|
||||
import { Virtualizer, type VirtualizerHandle } from "virtua/solid"
|
||||
import { SuggestBar } from "./SuggestBar"
|
||||
import {
|
||||
getMeasurement,
|
||||
getScroll,
|
||||
layoutFingerprint,
|
||||
resolveAnchor,
|
||||
rowFingerprint,
|
||||
setMeasurement,
|
||||
setScroll,
|
||||
} from "./transcript-cache"
|
||||
import {
|
||||
activeUserMessageID as getActiveUserMessageID,
|
||||
messageTurns,
|
||||
partitionTurns,
|
||||
queuedUserMessageIDs,
|
||||
stableMessageTurns,
|
||||
type MessageTurn,
|
||||
} from "../../context/session-queue"
|
||||
import { partitionRows, transcriptRows, type TranscriptRow } from "../../context/transcript-rows"
|
||||
import type { QuestionRequest, SuggestionRequest } from "../../types/messages"
|
||||
|
||||
const KiloLogo = (): JSX.Element => {
|
||||
@@ -88,7 +97,8 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
})
|
||||
|
||||
const [scrollEl, setScrollEl] = createSignal<HTMLElement>()
|
||||
const positions = new Map<string, { top: number; userScrolled: boolean }>()
|
||||
const [virtualizer, setVirtualizer] = createSignal<VirtualizerHandle>()
|
||||
const [layout, setLayout] = createSignal("")
|
||||
|
||||
const boundary = () => session.revert()?.messageID
|
||||
const turns = createMemo((prev: MessageTurn[] | undefined) =>
|
||||
@@ -107,45 +117,55 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
const queuedIDs = createMemo(
|
||||
() => new Set(queuedUserMessageIDs(session.messages(), session.statusInfo(), (msg) => session.getParts(msg.id))),
|
||||
)
|
||||
const [held, setHeld] = createSignal<{ sid: string; ids: Set<string> }>()
|
||||
createEffect(() => {
|
||||
const id = activeUserID()
|
||||
const sid = session.currentSessionID()
|
||||
const paused = autoScroll.userScrolled()
|
||||
if (!sid || (!id && !paused)) {
|
||||
setHeld(undefined)
|
||||
return
|
||||
}
|
||||
if (!id) return
|
||||
if (!paused) {
|
||||
setHeld({ sid, ids: new Set([id]) })
|
||||
return
|
||||
}
|
||||
setHeld((prev) => {
|
||||
if (prev?.sid === sid && prev.ids.has(id)) return prev
|
||||
const ids = prev?.sid === sid ? new Set(prev.ids) : new Set<string>()
|
||||
ids.add(id)
|
||||
return { sid, ids }
|
||||
})
|
||||
})
|
||||
const directIDs = createMemo(() => {
|
||||
const item = held()
|
||||
const ids = item && item.sid === session.currentSessionID() ? new Set(item.ids) : new Set<string>()
|
||||
const rows = createMemo((prev: TranscriptRow[] | undefined) => {
|
||||
const active = activeUserID()
|
||||
if (active) ids.add(active)
|
||||
return ids
|
||||
return transcriptRows(
|
||||
turns(),
|
||||
(msg) => session.getParts(msg),
|
||||
{
|
||||
queued: queuedIDs(),
|
||||
live: new Set(active ? [active] : []),
|
||||
hidden: session.isErrorHidden,
|
||||
},
|
||||
prev,
|
||||
)
|
||||
})
|
||||
const partition = createMemo(() => partitionRows(rows()))
|
||||
const keys = createMemo(() => partition().virtual.map((row) => row.key))
|
||||
const fingerprint = createMemo(() => rowFingerprint(keys()))
|
||||
const measurement = createMemo(() => {
|
||||
const id = session.currentSessionID()
|
||||
const token = layout()
|
||||
if (!id || !token || session.loading() || keys().length === 0) return undefined
|
||||
return getMeasurement(id, fingerprint(), token)
|
||||
})
|
||||
// Keep the growing live turn out of Virtua. Resizing a tall virtual item while
|
||||
// the user reads within it makes Virtua compensate scrollTop as if earlier
|
||||
// content moved, dragging the viewport downward during streaming. Preserve
|
||||
// direct-rendered tail turns while paused so completion and queue handoffs do
|
||||
// not move a turn being read back into the virtualized history.
|
||||
const partition = createMemo(() => partitionTurns(turns(), directIDs(), queuedIDs()))
|
||||
|
||||
const save = (id: string | undefined) => {
|
||||
let active = { id: session.currentSessionID(), keys: keys(), fingerprint: fingerprint() }
|
||||
createEffect(() => {
|
||||
const id = session.currentSessionID()
|
||||
const current = keys()
|
||||
const value = fingerprint()
|
||||
if (!id || session.loading() || active.id !== id) return
|
||||
active = { id, keys: current, fingerprint: value }
|
||||
})
|
||||
|
||||
const save = (id: string | undefined, saved = active) => {
|
||||
const el = scrollEl()
|
||||
if (!id || !el) return
|
||||
positions.set(id, { top: el.scrollTop, userScrolled: autoScroll.userScrolled() })
|
||||
if (!id || !el || saved.id !== id) return
|
||||
const handle = virtualizer()
|
||||
const token = layout()
|
||||
if (handle && token && saved.keys.length > 0) {
|
||||
setMeasurement(id, saved.fingerprint, token, handle.cache)
|
||||
}
|
||||
if (!autoScroll.userScrolled()) {
|
||||
setScroll(id, { type: "bottom" })
|
||||
return
|
||||
}
|
||||
if (!handle || saved.keys.length === 0) return
|
||||
const index = handle.findStartIndex()
|
||||
const key = saved.keys[index]
|
||||
if (!key) return
|
||||
setScroll(id, { type: "anchor", key, offset: handle.scrollOffset - handle.getItemOffset(index) })
|
||||
}
|
||||
|
||||
const maybeLoadOlder = () => {
|
||||
@@ -159,16 +179,44 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
maybeLoadOlder()
|
||||
}
|
||||
|
||||
let resize: ResizeObserver | undefined
|
||||
const refreshLayout = () => {
|
||||
const el = scrollEl()
|
||||
if (!el) return
|
||||
const style = getComputedStyle(el)
|
||||
setLayout(
|
||||
layoutFingerprint({
|
||||
width: Math.round(el.clientWidth),
|
||||
ratio: window.devicePixelRatio,
|
||||
font: style.fontFamily,
|
||||
size: style.fontSize,
|
||||
line: style.lineHeight,
|
||||
}),
|
||||
)
|
||||
}
|
||||
const setScrollRef = (el: HTMLElement | undefined) => {
|
||||
resize?.disconnect()
|
||||
setScrollEl(el)
|
||||
autoScroll.scrollRef(el)
|
||||
if (!el) return
|
||||
refreshLayout()
|
||||
resize = new ResizeObserver(refreshLayout)
|
||||
resize.observe(el)
|
||||
}
|
||||
window.addEventListener("resize", refreshLayout)
|
||||
document.fonts?.addEventListener("loadingdone", refreshLayout)
|
||||
onCleanup(() => {
|
||||
resize?.disconnect()
|
||||
window.removeEventListener("resize", refreshLayout)
|
||||
document.fonts?.removeEventListener("loadingdone", refreshLayout)
|
||||
})
|
||||
|
||||
const [pendingRestore, setPendingRestore] = createSignal<string>()
|
||||
|
||||
createEffect(
|
||||
on(session.currentSessionID, (id, prev) => {
|
||||
save(prev)
|
||||
active = { id, keys: [], fingerprint: rowFingerprint([]) }
|
||||
setPendingRestore(id)
|
||||
}),
|
||||
)
|
||||
@@ -185,9 +233,11 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
if (pendingRestore() !== id) return
|
||||
const el = scrollEl()
|
||||
if (!el) return
|
||||
const pos = positions.get(id)
|
||||
if (pos?.userScrolled) {
|
||||
el.scrollTop = pos.top
|
||||
const state = getScroll(id)
|
||||
const anchor = resolveAnchor(state, keys())
|
||||
const handle = virtualizer()
|
||||
if (state?.type === "anchor" && anchor && handle) {
|
||||
handle.scrollToIndex(anchor.index, { offset: anchor.offset })
|
||||
autoScroll.pause()
|
||||
maybeLoadOlder()
|
||||
} else {
|
||||
@@ -262,28 +312,36 @@ export const MessageList: Component<MessageListProps> = (props) => {
|
||||
{language.t("session.messages.loadEarlier")}
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={partition().virtual.length > 0 || partition().direct.length > 0}>
|
||||
<div class="message-list-turns">
|
||||
<Show when={scrollEl() && partition().virtual.length > 0}>
|
||||
<Show when={partition().virtual.length > 0}>
|
||||
<div
|
||||
class="message-list-turns"
|
||||
data-loaded-messages={session.messages().length}
|
||||
data-row-count={partition().virtual.length}
|
||||
data-queued-count={partition().queued.length}
|
||||
data-kept-count={partition().keep.length}
|
||||
>
|
||||
<Show when={scrollEl()}>
|
||||
<Virtualizer
|
||||
ref={setVirtualizer}
|
||||
data={partition().virtual}
|
||||
scrollRef={scrollEl()}
|
||||
shift={session.messageMutation() === "prepend"}
|
||||
overscan={6}
|
||||
cache={measurement()}
|
||||
keepMounted={partition().keep}
|
||||
overscan={2}
|
||||
itemSize={260}
|
||||
>
|
||||
{(turn) => <VscodeSessionTurn turn={turn} onForkMessage={props.onForkMessage} />}
|
||||
{(row, index) => (
|
||||
<TranscriptRowView row={row} index={index()} onForkMessage={props.onForkMessage} />
|
||||
)}
|
||||
</Virtualizer>
|
||||
</Show>
|
||||
<For each={partition().direct}>
|
||||
{(turn) => <VscodeSessionTurn turn={turn} onForkMessage={props.onForkMessage} />}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={boundary()}>
|
||||
<RevertBanner />
|
||||
</Show>
|
||||
<For each={partition().queued}>{(turn) => <VscodeSessionTurn turn={turn} queued />}</For>
|
||||
<For each={partition().queued}>{(row) => <TranscriptRowView row={row} />}</For>
|
||||
<WorkingIndicator />
|
||||
<TurnOutcome />
|
||||
<For each={props.questions?.()}>{(req) => <QuestionDock request={req} />}</For>
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
/**
|
||||
* TaskTimeline — horizontal strip of colored bars representing session activity.
|
||||
*
|
||||
* Each bar = one Part from assistant messages.
|
||||
* Color = part type (read=blue, write=dark blue, tool=indigo, error=red, text=gray).
|
||||
* Width = proportional to time between parts.
|
||||
* Height = proportional to content length.
|
||||
*
|
||||
* Interactions: drag scroll, mouse wheel, auto-scroll to latest.
|
||||
*
|
||||
* No virtualization needed: SolidJS <Index> creates each element once and
|
||||
* updates bindings in place (unlike React). Even 1000+ bars are fine.
|
||||
* Horizontal session activity timeline rendered as color-grouped SVG paths.
|
||||
* Pointer and keyboard interaction use the same pure bar geometry.
|
||||
*/
|
||||
|
||||
import { Component, Index, Show, createMemo, createEffect, createSignal, on, onCleanup } from "solid-js"
|
||||
import { Component, For, Show, createMemo, createEffect, createSignal, on, onCleanup } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { useSession } from "../../context/session"
|
||||
import { color, label } from "../../utils/timeline/colors"
|
||||
import { geometry, hit, navigate } from "../../utils/timeline/geometry"
|
||||
import { sizes, pinned, MAX_HEIGHT } from "../../utils/timeline/sizes"
|
||||
import type { Part, Message } from "../../types/messages"
|
||||
|
||||
@@ -56,7 +48,8 @@ export const TaskTimeline: Component = () => {
|
||||
let dragging = false
|
||||
let startX = 0
|
||||
let startScroll = 0
|
||||
let tipBar: HTMLElement | undefined
|
||||
const [hover, setHover] = createSignal(-1)
|
||||
const [active, setActive] = createSignal(-1)
|
||||
const [tip, setTip] = createSignal<{ text: string; x: number; y: number }>()
|
||||
|
||||
const messages = () => session.visibleMessages()
|
||||
@@ -71,12 +64,20 @@ export const TaskTimeline: Component = () => {
|
||||
}
|
||||
|
||||
const bars = createMemo(() => collect(messages(), allParts()))
|
||||
const layout = createMemo(() => geometry(bars(), MAX_HEIGHT))
|
||||
const busy = () => session.status() === "busy"
|
||||
const selected = () => {
|
||||
const idx = active()
|
||||
if (idx >= 0 && idx < bars().length) return idx
|
||||
return bars().length - 1
|
||||
}
|
||||
const aria = () => {
|
||||
const idx = selected()
|
||||
const bar = bars()[idx]
|
||||
if (!bar) return "Session activity timeline, no activity"
|
||||
return `Session activity timeline, bar ${idx + 1} of ${bars().length}: ${bar.tip}`
|
||||
}
|
||||
|
||||
// Reading scrollWidth and writing scrollLeft synchronously for every appended bar can
|
||||
// force repeated layout during streamed part updates. Batch those appends behind one
|
||||
// animation frame, after Solid has applied the current DOM updates. Only follow while
|
||||
// pinned so inspecting earlier activity is not interrupted by incoming bars.
|
||||
let prev = 0
|
||||
let frame: number | undefined
|
||||
let follow = true
|
||||
@@ -87,6 +88,7 @@ export const TaskTimeline: Component = () => {
|
||||
on(
|
||||
() => bars().length,
|
||||
(len) => {
|
||||
if (active() >= len) setActive(len - 1)
|
||||
if (len > prev && ref && follow && frame === undefined) {
|
||||
frame = requestAnimationFrame(() => {
|
||||
frame = undefined
|
||||
@@ -103,28 +105,32 @@ export const TaskTimeline: Component = () => {
|
||||
})
|
||||
|
||||
const hideTip = () => {
|
||||
tipBar = undefined
|
||||
setHover(-1)
|
||||
setTip(undefined)
|
||||
}
|
||||
|
||||
createEffect(on(bars, hideTip, { defer: true }))
|
||||
|
||||
const showTip = (e: PointerEvent) => {
|
||||
if (dragging || !(e.target instanceof Element)) return
|
||||
const bar = e.target.closest<HTMLElement>(".task-timeline-bar")
|
||||
if (!bar || !ref?.contains(bar)) return hideTip()
|
||||
if (bar === tipBar) return
|
||||
const rect = bar.getBoundingClientRect()
|
||||
tipBar = bar
|
||||
const showTip = (idx: number) => {
|
||||
const item = layout().items[idx]
|
||||
const bar = bars()[idx]
|
||||
if (!ref || !item || !bar) return hideTip()
|
||||
const rect = ref.getBoundingClientRect()
|
||||
const margin = Math.min(160, window.innerWidth / 2)
|
||||
setHover(idx)
|
||||
setTip({
|
||||
text: bar.dataset.tip ?? "",
|
||||
x: Math.max(margin, Math.min(window.innerWidth - margin, rect.left + rect.width / 2)),
|
||||
y: rect.top,
|
||||
text: bar.tip,
|
||||
x: Math.max(margin, Math.min(window.innerWidth - margin, rect.left + item.x - ref.scrollLeft + item.width / 2)),
|
||||
y: rect.top + MAX_HEIGHT - item.height,
|
||||
})
|
||||
}
|
||||
|
||||
// ── Drag scroll ──────────────────────────────────────────────────
|
||||
const pointerIndex = (e: PointerEvent) => {
|
||||
if (!ref) return -1
|
||||
const rect = ref.getBoundingClientRect()
|
||||
return hit(layout().items, e.clientX - rect.left + ref.scrollLeft)
|
||||
}
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
hideTip()
|
||||
if (!ref) return
|
||||
@@ -137,19 +143,24 @@ export const TaskTimeline: Component = () => {
|
||||
}
|
||||
|
||||
const onPointerMove = (e: PointerEvent) => {
|
||||
if (!dragging || !ref) return showTip(e)
|
||||
if (!ref) return
|
||||
if (!dragging) {
|
||||
const idx = pointerIndex(e)
|
||||
if (idx === hover()) return
|
||||
if (idx < 0) return hideTip()
|
||||
return showTip(idx)
|
||||
}
|
||||
ref.scrollLeft = startScroll - (e.clientX - startX)
|
||||
}
|
||||
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
if (!ref) return
|
||||
dragging = false
|
||||
ref.releasePointerCapture(e.pointerId)
|
||||
if (ref.hasPointerCapture(e.pointerId)) ref.releasePointerCapture(e.pointerId)
|
||||
ref.style.cursor = "grab"
|
||||
ref.style.userSelect = ""
|
||||
}
|
||||
|
||||
// ── Wheel → horizontal scroll ────────────────────────────────────
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
hideTip()
|
||||
if (!ref) return
|
||||
@@ -157,6 +168,20 @@ export const TaskTimeline: Component = () => {
|
||||
ref.scrollLeft += e.deltaY || e.deltaX
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (!ref || !["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return
|
||||
e.preventDefault()
|
||||
const idx = navigate(selected(), bars().length, e.key)
|
||||
setActive(idx)
|
||||
const item = layout().items[idx]
|
||||
if (!item) return
|
||||
const left = item.x
|
||||
const right = item.x + item.width
|
||||
if (left < ref.scrollLeft) ref.scrollLeft = left
|
||||
if (right > ref.scrollLeft + ref.clientWidth) ref.scrollLeft = right - ref.clientWidth
|
||||
showTip(idx)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const el = ref
|
||||
if (!el) return
|
||||
@@ -164,13 +189,37 @@ export const TaskTimeline: Component = () => {
|
||||
onCleanup(() => el.removeEventListener("wheel", onWheel))
|
||||
})
|
||||
|
||||
const overlay = (idx: number, pulse = false) => {
|
||||
const item = layout().items[idx]
|
||||
if (!item) return null
|
||||
return (
|
||||
<div
|
||||
class="task-timeline-bar"
|
||||
classList={{ "task-timeline-bar--active": pulse }}
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
left: `${item.x}px`,
|
||||
width: `${item.width}px`,
|
||||
height: `${item.height}px`,
|
||||
"--timeline-color": item.bg,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="task-timeline-outer">
|
||||
<div
|
||||
ref={ref}
|
||||
class="task-timeline"
|
||||
data-timeline-count={bars().length}
|
||||
role="img"
|
||||
tabIndex={0}
|
||||
aria-label={aria()}
|
||||
style={{ height: `${MAX_HEIGHT}px` }}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={hideTip}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerMove={onPointerMove}
|
||||
onPointerUp={onPointerUp}
|
||||
@@ -178,34 +227,19 @@ export const TaskTimeline: Component = () => {
|
||||
onPointerLeave={hideTip}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<Index each={bars()}>
|
||||
{(bar) => {
|
||||
const active = () => busy() && bar().idx === bars().length - 1
|
||||
return (
|
||||
<div
|
||||
class="task-timeline-bar"
|
||||
data-tip={bar().tip}
|
||||
role="img"
|
||||
aria-label={bar().tip}
|
||||
style={{
|
||||
width: `${bar().width}px`,
|
||||
height: `${MAX_HEIGHT}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="task-timeline-bar-fill task-timeline-bar-fill--new"
|
||||
classList={{
|
||||
"task-timeline-bar-fill--active": active(),
|
||||
}}
|
||||
style={{
|
||||
background: bar().bg,
|
||||
height: `${(bar().height / MAX_HEIGHT) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Index>
|
||||
<div class="task-timeline-content" style={{ width: `${layout().width}px`, height: `${MAX_HEIGHT}px` }}>
|
||||
<svg
|
||||
class="task-timeline-svg"
|
||||
width={layout().width}
|
||||
height={MAX_HEIGHT}
|
||||
viewBox={`0 0 ${layout().width} ${MAX_HEIGHT}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<For each={layout().paths}>{(path) => <path d={path.d} fill={path.bg} />}</For>
|
||||
</svg>
|
||||
<Show when={hover() >= 0}>{overlay(hover())}</Show>
|
||||
<Show when={busy() && bars().length > 0}>{overlay(bars().length - 1, true)}</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={tip()}>
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { type Component, Show, createEffect } from "solid-js"
|
||||
import { UserMessageDisplay } from "@kilocode/kilo-ui/message-part"
|
||||
import { DiffChanges } from "@kilocode/kilo-ui/diff-changes"
|
||||
import { Icon } from "@kilocode/kilo-ui/icon"
|
||||
import { useI18n } from "@kilocode/kilo-ui/context/i18n"
|
||||
import type { AssistantMessage as SDKAssistantMessage, Part as SDKPart, SnapshotFileDiff } from "@kilocode/sdk/v2"
|
||||
import type { TranscriptRow } from "../../context/transcript-rows"
|
||||
import { useSession } from "../../context/session"
|
||||
import { useServer } from "../../context/server"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
import { useFeedback } from "../../context/feedback"
|
||||
import { AssistantMessage } from "./AssistantMessage"
|
||||
import { ErrorDisplay, type ErrorDisplayProps } from "./ErrorDisplay"
|
||||
|
||||
interface TranscriptRowViewProps {
|
||||
row: TranscriptRow
|
||||
index?: number
|
||||
onForkMessage?: (sessionId: string, messageId: string) => void
|
||||
}
|
||||
|
||||
export const TranscriptRowView: Component<TranscriptRowViewProps> = (props) => {
|
||||
const session = useSession()
|
||||
const server = useServer()
|
||||
const language = useLanguage()
|
||||
const vscode = useVSCode()
|
||||
const feedback = useFeedback()
|
||||
const i18n = useI18n()
|
||||
|
||||
createEffect(() => session.hydrateParts([props.row.message.id]))
|
||||
|
||||
const open = () => vscode.postMessage({ type: "openChanges", turnId: props.row.message.id })
|
||||
|
||||
return (
|
||||
<div
|
||||
class="vscode-session-turn"
|
||||
data-message={props.row.message.id}
|
||||
data-row={props.row.type}
|
||||
data-row-key={props.row.key}
|
||||
data-row-index={props.index}
|
||||
data-turn={props.row.turn}
|
||||
data-live={props.row.live ? "" : undefined}
|
||||
>
|
||||
<Show when={props.row.type === "user" ? props.row : undefined}>
|
||||
{(row) => (
|
||||
<div
|
||||
class="vscode-session-turn-user"
|
||||
data-revert-disabled={row().answered && session.status() !== "idle" ? "" : undefined}
|
||||
title={row().answered && session.status() !== "idle" ? language.t("revert.disabled.agentBusy") : undefined}
|
||||
>
|
||||
<UserMessageDisplay
|
||||
message={row().message as unknown as Parameters<typeof UserMessageDisplay>[0]["message"]}
|
||||
parts={row().parts as unknown as Parameters<typeof UserMessageDisplay>[0]["parts"]}
|
||||
interrupted={row().interrupted}
|
||||
queued={row().queued}
|
||||
onFork={
|
||||
props.onForkMessage ? () => props.onForkMessage?.(row().message.sessionID, row().message.id) : undefined
|
||||
}
|
||||
onRevert={
|
||||
row().answered
|
||||
? () => {
|
||||
if (session.status() !== "idle") return
|
||||
session.revertSession(row().message.id)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={props.row.type === "assistant" ? props.row : undefined}>
|
||||
{(row) => (
|
||||
<div class="vscode-session-turn-assistant">
|
||||
<AssistantMessage
|
||||
message={row().message as unknown as SDKAssistantMessage}
|
||||
parts={row().parts as unknown as SDKPart[]}
|
||||
showAssistantCopyPartID={row().copy}
|
||||
feedback={{
|
||||
enabled: feedback.telemetryEnabled(),
|
||||
rating: feedback.getRating(row().message.id),
|
||||
onRate: (next) =>
|
||||
feedback.rate({
|
||||
messageID: row().message.id,
|
||||
sessionID: row().message.sessionID,
|
||||
parentMessageID: row().message.parentID ?? "",
|
||||
providerID: row().message.providerID ?? row().message.model?.providerID ?? "",
|
||||
modelID: row().message.modelID ?? row().message.model?.modelID ?? "",
|
||||
variant: row().message.model?.variant,
|
||||
next,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={props.row.type === "diff" ? props.row : undefined}>
|
||||
{(row) => (
|
||||
<Show when={server.gitInstalled()}>
|
||||
<div class="vscode-session-turn-diffs" data-component="session-turn">
|
||||
<button
|
||||
type="button"
|
||||
class="vscode-session-turn-diffs-trigger"
|
||||
onClick={open}
|
||||
aria-label={i18n.t("ui.sessionReview.change.modified")}
|
||||
>
|
||||
<span data-slot="session-turn-diffs-label">{i18n.t("ui.sessionReview.change.modified")}</span>
|
||||
<span data-slot="session-turn-diffs-count">
|
||||
{row().diffs.length}{" "}
|
||||
{i18n.t(row().diffs.length === 1 ? "ui.common.file.one" : "ui.common.file.other")}
|
||||
</span>
|
||||
<span data-slot="session-turn-diffs-meta">
|
||||
<DiffChanges changes={row().diffs as SnapshotFileDiff[]} variant="bars" />
|
||||
</span>
|
||||
<span data-slot="session-turn-diffs-chevron" aria-hidden="true">
|
||||
<Icon name="chevron-right" size="small" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={props.row.type === "error" ? props.row : undefined}>
|
||||
{(row) => <ErrorDisplay error={row().error as ErrorDisplayProps["error"]} onLogin={server.goToLogin} />}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { CacheSnapshot } from "virtua"
|
||||
|
||||
const MEASUREMENT_LIMIT = 16
|
||||
const SCROLL_LIMIT = 50
|
||||
|
||||
export interface LayoutMetrics {
|
||||
width: number
|
||||
ratio: number
|
||||
font: string
|
||||
size: string
|
||||
line: string
|
||||
}
|
||||
|
||||
export type ScrollState =
|
||||
| { type: "bottom" }
|
||||
| {
|
||||
type: "anchor"
|
||||
key: string
|
||||
offset: number
|
||||
}
|
||||
|
||||
interface MeasurementState {
|
||||
keys: string
|
||||
layout: string
|
||||
cache: CacheSnapshot
|
||||
}
|
||||
|
||||
const measurements = new Map<string, MeasurementState>()
|
||||
const scrolls = new Map<string, ScrollState>()
|
||||
|
||||
function touch<T>(cache: Map<string, T>, key: string, value: T, limit: number) {
|
||||
cache.delete(key)
|
||||
cache.set(key, value)
|
||||
if (cache.size <= limit) return
|
||||
const oldest = cache.keys().next().value
|
||||
if (oldest !== undefined) cache.delete(oldest)
|
||||
}
|
||||
|
||||
export function rowFingerprint(keys: readonly string[]) {
|
||||
return keys.map((key) => `${key.length}:${key}`).join("|")
|
||||
}
|
||||
|
||||
export function layoutFingerprint(metrics: LayoutMetrics) {
|
||||
return [metrics.width, metrics.ratio, metrics.font, metrics.size, metrics.line]
|
||||
.map((value) => `${String(value).length}:${value}`)
|
||||
.join("|")
|
||||
}
|
||||
|
||||
export function setMeasurement(id: string, keys: string, layout: string, cache: CacheSnapshot) {
|
||||
touch(measurements, id, { keys, layout, cache }, MEASUREMENT_LIMIT)
|
||||
}
|
||||
|
||||
export function getMeasurement(id: string, keys: string, layout: string) {
|
||||
const value = measurements.get(id)
|
||||
if (!value) return undefined
|
||||
if (value.keys !== keys || value.layout !== layout) {
|
||||
measurements.delete(id)
|
||||
return undefined
|
||||
}
|
||||
touch(measurements, id, value, MEASUREMENT_LIMIT)
|
||||
return value.cache
|
||||
}
|
||||
|
||||
export function setScroll(id: string, state: ScrollState) {
|
||||
touch(scrolls, id, state, SCROLL_LIMIT)
|
||||
}
|
||||
|
||||
export function getScroll(id: string) {
|
||||
const value = scrolls.get(id)
|
||||
if (!value) return undefined
|
||||
touch(scrolls, id, value, SCROLL_LIMIT)
|
||||
return value
|
||||
}
|
||||
|
||||
export function resolveAnchor(state: ScrollState | undefined, keys: readonly string[]) {
|
||||
if (!state || state.type === "bottom") return undefined
|
||||
const index = keys.indexOf(state.key)
|
||||
if (index < 0) return undefined
|
||||
return { index, offset: state.offset }
|
||||
}
|
||||
|
||||
export function resetTranscriptCaches() {
|
||||
measurements.clear()
|
||||
scrolls.clear()
|
||||
}
|
||||
@@ -91,9 +91,11 @@ export const ModelPreview: Component<Props> = (props) => {
|
||||
})()}
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={model().isFree}>
|
||||
<Show when={model().isFree || isDataCollectedModel(model())}>
|
||||
<span class="model-preview-free-data">
|
||||
<span class="model-preview-badge model-preview-badge--free">{freeLabel()}</span>
|
||||
<Show when={model().isFree}>
|
||||
<span class="model-preview-badge model-preview-badge--free">{freeLabel()}</span>
|
||||
</Show>
|
||||
<Show when={isDataCollectedModel(model())}>
|
||||
<Tooltip value={dataLabel()} placement="top">
|
||||
<span class="model-preview-free-data-icon" aria-label={dataLabel()}>
|
||||
|
||||
@@ -828,11 +828,13 @@ export const ModelSelectorBase: Component<ModelSelectorBaseProps> = (props) => {
|
||||
)
|
||||
})()}
|
||||
</span>
|
||||
<Show when={isFree(model)}>
|
||||
<Show when={isFree(model) || isDataCollectedModel(model)}>
|
||||
<span class="model-selector-free-data">
|
||||
<span class="model-selector-data-badge">
|
||||
<Tag data-variant="member">{freeLabel()}</Tag>
|
||||
</span>
|
||||
<Show when={isFree(model)}>
|
||||
<span class="model-selector-data-badge">
|
||||
<Tag data-variant="member">{freeLabel()}</Tag>
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={isDataCollectedModel(model)}>
|
||||
<Tooltip value={dataLabel()} placement="top">
|
||||
<span class="model-selector-free-data-icon" aria-label={dataLabel()}>
|
||||
|
||||
@@ -22,8 +22,8 @@ export function isFree(model: Pick<EnrichedModel, "isFree">): boolean {
|
||||
return model.isFree === true
|
||||
}
|
||||
|
||||
export function isDataCollectedModel(model: Pick<EnrichedModel, "providerID" | "isFree">): boolean {
|
||||
return model.isFree === true && model.providerID === KILO_GATEWAY_ID
|
||||
export function isDataCollectedModel(model: Pick<EnrichedModel, "mayTrainOnYourPrompts">): boolean {
|
||||
return model.mayTrainOnYourPrompts === true
|
||||
}
|
||||
|
||||
export function freeDataLabel(_free: string, data: string): string {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import type { Message, Part } from "../types/messages"
|
||||
import type { MessageTurn } from "./session-queue"
|
||||
|
||||
interface TranscriptMeta {
|
||||
turn: string
|
||||
partial: boolean
|
||||
queued: boolean
|
||||
live: boolean
|
||||
}
|
||||
|
||||
export interface TranscriptUserRow extends TranscriptMeta {
|
||||
type: "user"
|
||||
key: string
|
||||
message: Message
|
||||
parts: Part[]
|
||||
interrupted: boolean
|
||||
answered: boolean
|
||||
}
|
||||
|
||||
export interface TranscriptAssistantRow extends TranscriptMeta {
|
||||
type: "assistant"
|
||||
key: string
|
||||
message: Message
|
||||
parts: Part[]
|
||||
copy?: string
|
||||
}
|
||||
|
||||
export interface TranscriptDiffRow extends TranscriptMeta {
|
||||
type: "diff"
|
||||
key: string
|
||||
message: Message
|
||||
diffs: unknown[]
|
||||
}
|
||||
|
||||
export interface TranscriptErrorRow extends TranscriptMeta {
|
||||
type: "error"
|
||||
key: string
|
||||
message: Message
|
||||
error: NonNullable<Message["error"]>
|
||||
}
|
||||
|
||||
export type TranscriptRow = TranscriptUserRow | TranscriptAssistantRow | TranscriptDiffRow | TranscriptErrorRow
|
||||
|
||||
export interface TranscriptOptions {
|
||||
size?: number
|
||||
queued?: ReadonlySet<string>
|
||||
live?: ReadonlySet<string>
|
||||
hidden?: (id: string) => boolean
|
||||
}
|
||||
|
||||
export interface TranscriptPartition {
|
||||
virtual: TranscriptRow[]
|
||||
keep: number[]
|
||||
queued: TranscriptRow[]
|
||||
}
|
||||
|
||||
function same<T>(a: T[], b: T[]) {
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i += 1) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function meta(a: TranscriptRow, b: TranscriptRow) {
|
||||
return a.turn === b.turn && a.partial === b.partial && a.queued === b.queued && a.live === b.live
|
||||
}
|
||||
|
||||
function equal(a: TranscriptRow, b: TranscriptRow) {
|
||||
if (a.type !== b.type || !meta(a, b)) return false
|
||||
if (a.type === "user" && b.type === "user") {
|
||||
return (
|
||||
a.message === b.message && same(a.parts, b.parts) && a.interrupted === b.interrupted && a.answered === b.answered
|
||||
)
|
||||
}
|
||||
if (a.type === "assistant" && b.type === "assistant") {
|
||||
return a.message === b.message && same(a.parts, b.parts) && a.copy === b.copy
|
||||
}
|
||||
if (a.type === "diff" && b.type === "diff") {
|
||||
return a.message === b.message && same(a.diffs, b.diffs)
|
||||
}
|
||||
if (a.type === "error" && b.type === "error") {
|
||||
return a.message === b.message && a.error === b.error
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function diffs(msg: Message) {
|
||||
if (!msg.summary || typeof msg.summary === "boolean") return []
|
||||
return msg.summary.diffs ?? []
|
||||
}
|
||||
|
||||
function copy(messages: Message[], getParts: (id: string) => Part[]) {
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
const parts = getParts(messages[i]!.id)
|
||||
for (let j = parts.length - 1; j >= 0; j -= 1) {
|
||||
const part = parts[j]
|
||||
if (part?.type !== "text" || part.synthetic || !part.text.trim()) continue
|
||||
return part.id
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function transcriptRows(
|
||||
turns: MessageTurn[],
|
||||
getParts: (id: string) => Part[],
|
||||
opts: TranscriptOptions = {},
|
||||
prev: TranscriptRow[] = [],
|
||||
): TranscriptRow[] {
|
||||
const size = Math.max(1, Math.floor(opts.size ?? 8))
|
||||
const rows: TranscriptRow[] = []
|
||||
|
||||
for (const turn of turns) {
|
||||
const meta = {
|
||||
turn: turn.id,
|
||||
partial: turn.partial === true,
|
||||
queued: opts.queued?.has(turn.id) === true,
|
||||
live: opts.live?.has(turn.id) === true,
|
||||
}
|
||||
const copied = copy(turn.assistant, getParts)
|
||||
|
||||
if (!turn.partial) {
|
||||
rows.push({
|
||||
...meta,
|
||||
type: "user",
|
||||
key: `${turn.id}:user`,
|
||||
message: turn.user,
|
||||
parts: getParts(turn.user.id),
|
||||
interrupted: turn.assistant.some((msg) => msg.error?.name === "MessageAbortedError"),
|
||||
answered: turn.assistant.length > 0,
|
||||
})
|
||||
}
|
||||
|
||||
for (const msg of turn.assistant) {
|
||||
const parts = getParts(msg.id)
|
||||
if (parts.length === 0) {
|
||||
rows.push({
|
||||
...meta,
|
||||
type: "assistant",
|
||||
key: `${turn.id}:assistant:${msg.id}:empty`,
|
||||
message: msg,
|
||||
parts,
|
||||
copy: copied,
|
||||
})
|
||||
continue
|
||||
}
|
||||
for (let start = 0; start < parts.length; start += size) {
|
||||
const chunk = parts.slice(start, start + size)
|
||||
rows.push({
|
||||
...meta,
|
||||
type: "assistant",
|
||||
key: `${turn.id}:assistant:${msg.id}:${chunk[0]!.id}`,
|
||||
message: msg,
|
||||
parts: chunk,
|
||||
copy: copied,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const changes = diffs(turn.user)
|
||||
if (changes.length > 0) {
|
||||
rows.push({ ...meta, type: "diff", key: `${turn.id}:diff`, message: turn.user, diffs: changes })
|
||||
}
|
||||
|
||||
const failed = turn.assistant.find(
|
||||
(msg) => msg.error && msg.error.name !== "MessageAbortedError" && opts.hidden?.(msg.id) !== true,
|
||||
)
|
||||
if (failed?.error) {
|
||||
rows.push({ ...meta, type: "error", key: `${turn.id}:error:${failed.id}`, message: failed, error: failed.error })
|
||||
}
|
||||
}
|
||||
|
||||
if (prev.length === 0) return rows
|
||||
const prior = new Map(prev.map((row) => [row.key, row]))
|
||||
return rows.map((row) => {
|
||||
const old = prior.get(row.key)
|
||||
return old && equal(old, row) ? old : row
|
||||
})
|
||||
}
|
||||
|
||||
export function partitionRows(rows: TranscriptRow[], limit = 2): TranscriptPartition {
|
||||
const queued = rows.filter((row) => row.queued)
|
||||
const virtual = rows.filter((row) => !row.queued)
|
||||
const size = Math.max(0, Math.floor(limit))
|
||||
const keep: number[] = []
|
||||
for (let i = virtual.length - 1; i >= 0 && keep.length < size; i -= 1) {
|
||||
const row = virtual[i]!
|
||||
if (!row.live) break
|
||||
keep.unshift(i)
|
||||
}
|
||||
return { virtual, keep, queued }
|
||||
}
|
||||
@@ -121,8 +121,7 @@
|
||||
}
|
||||
|
||||
.task-timeline {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
cursor: grab;
|
||||
@@ -130,37 +129,36 @@
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.task-timeline:focus-visible {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.task-timeline::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.task-timeline-bar {
|
||||
.task-timeline-content {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
margin-right: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.task-timeline-bar-fill {
|
||||
.task-timeline-svg {
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.task-timeline-bar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px 2px 0 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.task-timeline-bar:hover .task-timeline-bar-fill {
|
||||
background: var(--timeline-color);
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Fade-in animation for new bars */
|
||||
.task-timeline-bar-fill--new {
|
||||
animation: timeline-fade-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Active (latest) bar pulse when session is busy */
|
||||
.task-timeline-bar-fill--active {
|
||||
.task-timeline-bar--active {
|
||||
opacity: 1;
|
||||
animation: timeline-pulse 2s ease-in-out infinite 0.5s;
|
||||
}
|
||||
|
||||
@@ -172,18 +170,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes timeline-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
transform-origin: bottom;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes timeline-pulse {
|
||||
0%,
|
||||
100% {
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface ProviderModel {
|
||||
options?: { description?: string }
|
||||
recommendedIndex?: number
|
||||
isFree?: boolean
|
||||
mayTrainOnYourPrompts?: boolean
|
||||
terminalBench?: {
|
||||
overallScore: number
|
||||
avgAttemptCostUsd: number
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
export interface TimelineGeometryBar {
|
||||
bg: string
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface TimelineGeometryItem extends TimelineGeometryBar {
|
||||
idx: number
|
||||
x: number
|
||||
}
|
||||
|
||||
export interface TimelineGeometry {
|
||||
items: TimelineGeometryItem[]
|
||||
paths: Array<{ bg: string; d: string }>
|
||||
width: number
|
||||
}
|
||||
|
||||
const shape = (x: number, width: number, height: number, max: number) => {
|
||||
const y = max - height
|
||||
const radius = Math.min(2, width / 2, height)
|
||||
return `M${x},${max}V${y + radius}Q${x},${y} ${x + radius},${y}H${x + width - radius}Q${x + width},${y} ${x + width},${y + radius}V${max}Z`
|
||||
}
|
||||
|
||||
export function geometry(bars: TimelineGeometryBar[], max: number, gap = 1): TimelineGeometry {
|
||||
const paths = new Map<string, string[]>()
|
||||
const items: TimelineGeometryItem[] = []
|
||||
let x = 0
|
||||
|
||||
for (const [idx, bar] of bars.entries()) {
|
||||
const item = { ...bar, idx, x }
|
||||
items.push(item)
|
||||
const path = paths.get(bar.bg) ?? []
|
||||
path.push(shape(x, bar.width, bar.height, max))
|
||||
paths.set(bar.bg, path)
|
||||
x += bar.width + gap
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
paths: Array.from(paths, ([bg, parts]) => ({ bg, d: parts.join("") })),
|
||||
width: x,
|
||||
}
|
||||
}
|
||||
|
||||
export function hit(items: TimelineGeometryItem[], x: number) {
|
||||
let low = 0
|
||||
let high = items.length - 1
|
||||
|
||||
while (low <= high) {
|
||||
const mid = Math.floor((low + high) / 2)
|
||||
const item = items[mid]!
|
||||
if (x < item.x) {
|
||||
high = mid - 1
|
||||
continue
|
||||
}
|
||||
if (x >= item.x + item.width) {
|
||||
low = mid + 1
|
||||
continue
|
||||
}
|
||||
return item.idx
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
export function navigate(index: number, count: number, key: string) {
|
||||
if (count === 0) return -1
|
||||
if (key === "Home") return 0
|
||||
if (key === "End") return count - 1
|
||||
if (key === "ArrowLeft") return Math.max(0, index < 0 ? count - 1 : index - 1)
|
||||
if (key === "ArrowRight") return Math.min(count - 1, index < 0 ? 0 : index + 1)
|
||||
return index
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export const FreeModelDisclosure = {
|
||||
label: "May train",
|
||||
panel: "Free - data may be used for training",
|
||||
collectsData(model: { isFree?: boolean; api?: { npm?: string } }): boolean {
|
||||
return model.isFree === true && model.api?.npm === "@kilocode/kilo-gateway"
|
||||
panel: "Data may be used for training",
|
||||
collectsData(model: { mayTrainOnYourPrompts?: boolean }): boolean {
|
||||
return model.mayTrainOnYourPrompts === true
|
||||
},
|
||||
} as const
|
||||
|
||||
@@ -35,6 +35,7 @@ export const KILO_MODEL_SCHEMA_EXTENSIONS = {
|
||||
recommendedIndex: optionalOmitUndefined(Schema.Finite),
|
||||
prompt: Schema.optional(Schema.Literals(PROMPTS)),
|
||||
isFree: Schema.optional(Schema.Boolean),
|
||||
mayTrainOnYourPrompts: Schema.optional(Schema.Boolean),
|
||||
terminalBench: optionalOmitUndefined(
|
||||
Schema.Struct({
|
||||
overallScore: Schema.Finite,
|
||||
@@ -49,12 +50,12 @@ export const KILO_MODEL_SCHEMA_EXTENSIONS = {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function patchModelsDevModel(providerID: string, source: any) {
|
||||
const free = providerID === "kilo" && source.cost?.input === 0 && source.cost?.output === 0
|
||||
return {
|
||||
variants: providerID === "kilo" ? (source.variants ?? {}) : {},
|
||||
recommendedIndex: source.recommendedIndex,
|
||||
prompt: source.prompt,
|
||||
isFree: source.isFree ?? (free ? true : undefined),
|
||||
isFree: source.isFree,
|
||||
mayTrainOnYourPrompts: source.mayTrainOnYourPrompts,
|
||||
terminalBench: source.terminalBench,
|
||||
ai_sdk_provider: source.ai_sdk_provider,
|
||||
options: source.options ?? {},
|
||||
@@ -70,6 +71,7 @@ export function patchConfigModel(cfg: any, existing: any) {
|
||||
recommendedIndex: cfg.recommendedIndex ?? existing?.recommendedIndex,
|
||||
prompt: cfg.prompt ?? existing?.prompt,
|
||||
isFree: cfg.isFree ?? existing?.isFree,
|
||||
mayTrainOnYourPrompts: cfg.mayTrainOnYourPrompts ?? existing?.mayTrainOnYourPrompts,
|
||||
terminalBench: existing?.terminalBench,
|
||||
ai_sdk_provider: cfg.ai_sdk_provider ?? existing?.ai_sdk_provider,
|
||||
variants: cfg.variants
|
||||
|
||||
@@ -4,27 +4,12 @@ import { FreeModelDisclosure } from "../../src/kilocode/components/free-model-di
|
||||
describe("FreeModelDisclosure", () => {
|
||||
test("uses compact CLI labels", () => {
|
||||
expect(FreeModelDisclosure.label).toBe("May train")
|
||||
expect(FreeModelDisclosure.panel).toBe("Free - data may be used for training")
|
||||
expect(FreeModelDisclosure.panel).toBe("Data may be used for training")
|
||||
})
|
||||
|
||||
test("only Kilo Gateway free models get the training disclosure", () => {
|
||||
expect(
|
||||
FreeModelDisclosure.collectsData({
|
||||
isFree: true,
|
||||
api: { npm: "@kilocode/kilo-gateway" },
|
||||
}),
|
||||
).toBe(true)
|
||||
expect(
|
||||
FreeModelDisclosure.collectsData({
|
||||
isFree: true,
|
||||
api: { npm: "@openrouter/ai-sdk-provider" },
|
||||
}),
|
||||
).toBe(false)
|
||||
expect(
|
||||
FreeModelDisclosure.collectsData({
|
||||
isFree: false,
|
||||
api: { npm: "@kilocode/kilo-gateway" },
|
||||
}),
|
||||
).toBe(false)
|
||||
test("uses only explicit prompt training metadata", () => {
|
||||
expect(FreeModelDisclosure.collectsData({ mayTrainOnYourPrompts: true })).toBe(true)
|
||||
expect(FreeModelDisclosure.collectsData({ mayTrainOnYourPrompts: false })).toBe(false)
|
||||
expect(FreeModelDisclosure.collectsData({})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -86,6 +86,8 @@ function layer() {
|
||||
id: "paid-model",
|
||||
name: "Paid Model",
|
||||
cost: { input: 1, output: 2 },
|
||||
isFree: false,
|
||||
mayTrainOnYourPrompts: true,
|
||||
limit: { context: 128000, output: 4096 },
|
||||
},
|
||||
},
|
||||
@@ -129,11 +131,13 @@ it.live("assembles paid Kilo models without auth", () =>
|
||||
id: "paid-model",
|
||||
providerID: "kilo",
|
||||
cost: { input: 1, output: 2 },
|
||||
isFree: false,
|
||||
mayTrainOnYourPrompts: true,
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("marks zero-cost Kilo models as free when the catalog omits isFree", () =>
|
||||
it.live("does not infer free status from zero catalog prices", () =>
|
||||
Effect.gen(function* () {
|
||||
const providers = yield* ModelsDev.Service.use((models) => models.get()).pipe(
|
||||
Effect.provide(layer()),
|
||||
@@ -141,7 +145,7 @@ it.live("marks zero-cost Kilo models as free when the catalog omits isFree", ()
|
||||
)
|
||||
const kilo = Provider.fromModelsDevProvider(providers.kilo)
|
||||
|
||||
expect(kilo.models["free-model"].isFree).toBe(true)
|
||||
expect(kilo.models["free-model"].isFree).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -5,16 +5,10 @@ export type ClientOptions = {
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| EventServerInstanceDisposed
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventGlobalConfigUpdated
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow1
|
||||
| EventTuiSessionSelect
|
||||
| EventKilocodeAgentManagerStart
|
||||
| EventIndexingStatus
|
||||
| EventServerInstanceDisposed
|
||||
| EventFileEdited
|
||||
| EventFileWatcherUpdated
|
||||
| EventQuestionAsked
|
||||
@@ -22,6 +16,10 @@ export type Event =
|
||||
| EventQuestionRejected
|
||||
| EventLspClientDiagnostics
|
||||
| EventLspUpdated
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow1
|
||||
| EventTuiSessionSelect
|
||||
| EventMcpToolsChanged
|
||||
| EventMcpBrowserOpenFailed
|
||||
| EventSessionNetworkAsked
|
||||
@@ -46,6 +44,7 @@ export type Event =
|
||||
| EventSessionCompacted
|
||||
| EventCommandExecuted
|
||||
| EventProjectUpdated
|
||||
| EventKilocodeAgentManagerStart
|
||||
| EventVcsBranchUpdated
|
||||
| EventKiloSessionsRemoteStatusChanged
|
||||
| EventWorkspaceReady
|
||||
@@ -59,6 +58,7 @@ export type Event =
|
||||
| EventPtyDeleted
|
||||
| EventInstallationUpdated
|
||||
| EventInstallationUpdateAvailable
|
||||
| EventIndexingStatus
|
||||
|
||||
export type OAuth = {
|
||||
type: "oauth"
|
||||
@@ -85,71 +85,6 @@ export type WellKnownAuth = {
|
||||
|
||||
export type Auth = OAuth | ApiAuth | WellKnownAuth
|
||||
|
||||
export type EventTuiPromptAppend = {
|
||||
id: string
|
||||
type: "tui.prompt.append"
|
||||
properties: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiCommandExecute = {
|
||||
id: string
|
||||
type: "tui.command.execute"
|
||||
properties: {
|
||||
command:
|
||||
| "session.list"
|
||||
| "session.new"
|
||||
| "session.share"
|
||||
| "session.interrupt"
|
||||
| "session.compact"
|
||||
| "session.page.up"
|
||||
| "session.page.down"
|
||||
| "session.line.up"
|
||||
| "session.line.down"
|
||||
| "session.half.page.up"
|
||||
| "session.half.page.down"
|
||||
| "session.first"
|
||||
| "session.last"
|
||||
| "prompt.clear"
|
||||
| "prompt.submit"
|
||||
| "agent.cycle"
|
||||
| string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiToastShow = {
|
||||
id: string
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
title?: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiSessionSelect = {
|
||||
id: string
|
||||
type: "tui.session.select"
|
||||
properties: {
|
||||
/**
|
||||
* Session ID to navigate to
|
||||
*/
|
||||
sessionID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type IndexingStatusState = "Disabled" | "In Progress" | "Complete" | "Error" | "Standby"
|
||||
|
||||
export type IndexingStatus = {
|
||||
state: IndexingStatusState
|
||||
message: string
|
||||
processedFiles: number
|
||||
totalFiles: number
|
||||
percent: number
|
||||
}
|
||||
|
||||
export type QuestionOption = {
|
||||
/**
|
||||
* Display text (1-5 words, concise)
|
||||
@@ -212,6 +147,61 @@ export type QuestionRejected = {
|
||||
requestID: string
|
||||
}
|
||||
|
||||
export type EventTuiPromptAppend = {
|
||||
id: string
|
||||
type: "tui.prompt.append"
|
||||
properties: {
|
||||
text: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiCommandExecute = {
|
||||
id: string
|
||||
type: "tui.command.execute"
|
||||
properties: {
|
||||
command:
|
||||
| "session.list"
|
||||
| "session.new"
|
||||
| "session.share"
|
||||
| "session.interrupt"
|
||||
| "session.compact"
|
||||
| "session.page.up"
|
||||
| "session.page.down"
|
||||
| "session.line.up"
|
||||
| "session.line.down"
|
||||
| "session.half.page.up"
|
||||
| "session.half.page.down"
|
||||
| "session.first"
|
||||
| "session.last"
|
||||
| "prompt.clear"
|
||||
| "prompt.submit"
|
||||
| "agent.cycle"
|
||||
| string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiToastShow = {
|
||||
id: string
|
||||
type: "tui.toast.show"
|
||||
properties: {
|
||||
title?: string
|
||||
message: string
|
||||
variant: "info" | "success" | "warning" | "error"
|
||||
duration?: number
|
||||
}
|
||||
}
|
||||
|
||||
export type EventTuiSessionSelect = {
|
||||
id: string
|
||||
type: "tui.session.select"
|
||||
properties: {
|
||||
/**
|
||||
* Session ID to navigate to
|
||||
*/
|
||||
sessionID: string
|
||||
}
|
||||
}
|
||||
|
||||
export type SessionNetworkWait = {
|
||||
id: string
|
||||
sessionID: string
|
||||
@@ -426,6 +416,16 @@ export type Pty = {
|
||||
sessionID?: string | null
|
||||
}
|
||||
|
||||
export type IndexingStatusState = "Disabled" | "In Progress" | "Complete" | "Error" | "Standby"
|
||||
|
||||
export type IndexingStatus = {
|
||||
state: IndexingStatusState
|
||||
message: string
|
||||
processedFiles: number
|
||||
totalFiles: number
|
||||
percent: number
|
||||
}
|
||||
|
||||
export type OutputFormatText = {
|
||||
type: "text"
|
||||
}
|
||||
@@ -859,16 +859,10 @@ export type GlobalEvent = {
|
||||
project?: string
|
||||
workspace?: string
|
||||
payload:
|
||||
| EventServerInstanceDisposed
|
||||
| EventServerConnected
|
||||
| EventGlobalDisposed
|
||||
| EventGlobalConfigUpdated
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow
|
||||
| EventTuiSessionSelect
|
||||
| EventKilocodeAgentManagerStart
|
||||
| EventIndexingStatus
|
||||
| EventServerInstanceDisposed
|
||||
| EventFileEdited
|
||||
| EventFileWatcherUpdated
|
||||
| EventQuestionAsked
|
||||
@@ -876,6 +870,10 @@ export type GlobalEvent = {
|
||||
| EventQuestionRejected
|
||||
| EventLspClientDiagnostics
|
||||
| EventLspUpdated
|
||||
| EventTuiPromptAppend
|
||||
| EventTuiCommandExecute
|
||||
| EventTuiToastShow
|
||||
| EventTuiSessionSelect
|
||||
| EventMcpToolsChanged
|
||||
| EventMcpBrowserOpenFailed
|
||||
| EventSessionNetworkAsked
|
||||
@@ -900,6 +898,7 @@ export type GlobalEvent = {
|
||||
| EventSessionCompacted
|
||||
| EventCommandExecuted
|
||||
| EventProjectUpdated
|
||||
| EventKilocodeAgentManagerStart
|
||||
| EventVcsBranchUpdated
|
||||
| EventKiloSessionsRemoteStatusChanged
|
||||
| EventWorkspaceReady
|
||||
@@ -913,6 +912,7 @@ export type GlobalEvent = {
|
||||
| EventPtyDeleted
|
||||
| EventInstallationUpdated
|
||||
| EventInstallationUpdateAvailable
|
||||
| EventIndexingStatus
|
||||
| SyncEventMessageUpdated
|
||||
| SyncEventMessageRemoved
|
||||
| SyncEventMessagePartUpdated
|
||||
@@ -1511,6 +1511,7 @@ export type Model = {
|
||||
recommendedIndex?: number
|
||||
prompt?: "codex" | "gemini" | "beast" | "anthropic" | "trinity" | "anthropic_without_todo" | "ling" | "gpt55"
|
||||
isFree?: boolean
|
||||
mayTrainOnYourPrompts?: boolean
|
||||
terminalBench?: {
|
||||
overallScore: number
|
||||
avgAttemptCostUsd: number
|
||||
@@ -2722,6 +2723,14 @@ export type SyncEventSessionNextCompactionEnded = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerInstanceDisposed = {
|
||||
id: string
|
||||
type: "server.instance.disposed"
|
||||
properties: {
|
||||
directory: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerConnected = {
|
||||
id: string
|
||||
type: "server.connected"
|
||||
@@ -2746,38 +2755,6 @@ export type EventGlobalConfigUpdated = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventKilocodeAgentManagerStart = {
|
||||
id: string
|
||||
type: "kilocode.agent_manager.start"
|
||||
properties: {
|
||||
requestID: string
|
||||
sessionID: string
|
||||
mode: "worktree" | "local"
|
||||
versions?: boolean
|
||||
tasks: Array<{
|
||||
prompt?: string
|
||||
name?: string
|
||||
branchName?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export type EventIndexingStatus = {
|
||||
id: string
|
||||
type: "indexing.status"
|
||||
properties: {
|
||||
status: IndexingStatus
|
||||
}
|
||||
}
|
||||
|
||||
export type EventServerInstanceDisposed = {
|
||||
id: string
|
||||
type: "server.instance.disposed"
|
||||
properties: {
|
||||
directory: string
|
||||
}
|
||||
}
|
||||
|
||||
export type EventFileEdited = {
|
||||
id: string
|
||||
type: "file.edited"
|
||||
@@ -3055,6 +3032,22 @@ export type EventProjectUpdated = {
|
||||
properties: Project
|
||||
}
|
||||
|
||||
export type EventKilocodeAgentManagerStart = {
|
||||
id: string
|
||||
type: "kilocode.agent_manager.start"
|
||||
properties: {
|
||||
requestID: string
|
||||
sessionID: string
|
||||
mode: "worktree" | "local"
|
||||
versions?: boolean
|
||||
tasks: Array<{
|
||||
prompt?: string
|
||||
name?: string
|
||||
branchName?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
export type EventVcsBranchUpdated = {
|
||||
id: string
|
||||
type: "vcs.branch.updated"
|
||||
@@ -3163,6 +3156,14 @@ export type EventInstallationUpdateAvailable = {
|
||||
}
|
||||
}
|
||||
|
||||
export type EventIndexingStatus = {
|
||||
id: string
|
||||
type: "indexing.status"
|
||||
properties: {
|
||||
status: IndexingStatus
|
||||
}
|
||||
}
|
||||
|
||||
export type PromptSource = {
|
||||
start: number
|
||||
end: number
|
||||
|
||||
+312
-309
@@ -14344,6 +14344,9 @@
|
||||
"schemas": {
|
||||
"Event": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerConnected"
|
||||
},
|
||||
@@ -14353,27 +14356,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventGlobalConfigUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventTuiToastShow1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventKilocodeAgent_managerStart"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingStatus"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventFileEdited"
|
||||
},
|
||||
@@ -14395,6 +14377,18 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventLspUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventTuiToastShow1"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventMcpToolsChanged"
|
||||
},
|
||||
@@ -14467,6 +14461,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventProjectUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventKilocodeAgent_managerStart"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventVcsBranchUpdated"
|
||||
},
|
||||
@@ -14505,6 +14502,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventInstallationUpdate-available"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingStatus"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -14585,170 +14585,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Event.tui.prompt.append": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.prompt.append"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.command.execute": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.command.execute"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
"session.interrupt",
|
||||
"session.compact",
|
||||
"session.page.up",
|
||||
"session.page.down",
|
||||
"session.line.up",
|
||||
"session.line.down",
|
||||
"session.half.page.up",
|
||||
"session.half.page.down",
|
||||
"session.first",
|
||||
"session.last",
|
||||
"prompt.clear",
|
||||
"prompt.submit",
|
||||
"agent.cycle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.toast.show": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.toast.show"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["info", "success", "warning", "error"]
|
||||
},
|
||||
"duration": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["message", "variant"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.session.select": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.session.select"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses",
|
||||
"description": "Session ID to navigate to"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IndexingStatusState": {
|
||||
"type": "string",
|
||||
"enum": ["Disabled", "In Progress", "Complete", "Error", "Standby"]
|
||||
},
|
||||
"IndexingStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/IndexingStatusState"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"processedFiles": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"totalFiles": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"percent": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
}
|
||||
},
|
||||
"required": ["state", "message", "processedFiles", "totalFiles", "percent"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"QuestionOption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14891,6 +14727,140 @@
|
||||
"required": ["sessionID", "requestID"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.prompt.append": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.prompt.append"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.command.execute": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.command.execute"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"session.list",
|
||||
"session.new",
|
||||
"session.share",
|
||||
"session.interrupt",
|
||||
"session.compact",
|
||||
"session.page.up",
|
||||
"session.page.down",
|
||||
"session.line.up",
|
||||
"session.line.down",
|
||||
"session.half.page.up",
|
||||
"session.half.page.down",
|
||||
"session.first",
|
||||
"session.last",
|
||||
"prompt.clear",
|
||||
"prompt.submit",
|
||||
"agent.cycle"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.toast.show": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.toast.show"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"variant": {
|
||||
"type": "string",
|
||||
"enum": ["info", "success", "warning", "error"]
|
||||
},
|
||||
"duration": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["message", "variant"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Event.tui.session.select": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["tui.session.select"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses",
|
||||
"description": "Session ID to navigate to"
|
||||
}
|
||||
},
|
||||
"required": ["sessionID"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"SessionNetworkWait": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15520,6 +15490,36 @@
|
||||
"required": ["id", "title", "command", "args", "cwd", "status", "pid"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"IndexingStatusState": {
|
||||
"type": "string",
|
||||
"enum": ["Disabled", "In Progress", "Complete", "Error", "Standby"]
|
||||
},
|
||||
"IndexingStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"state": {
|
||||
"$ref": "#/components/schemas/IndexingStatusState"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"processedFiles": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"totalFiles": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"percent": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 100
|
||||
}
|
||||
},
|
||||
"required": ["state", "message", "processedFiles", "totalFiles", "percent"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"OutputFormatText": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -16871,6 +16871,9 @@
|
||||
},
|
||||
"payload": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerConnected"
|
||||
},
|
||||
@@ -16880,27 +16883,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventGlobalConfigUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.toast.show"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventKilocodeAgent_managerStart"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingStatus"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventServerInstanceDisposed"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventFileEdited"
|
||||
},
|
||||
@@ -16922,6 +16904,18 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventLspUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.prompt.append"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.command.execute"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.toast.show"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.tui.session.select"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventMcpToolsChanged"
|
||||
},
|
||||
@@ -16994,6 +16988,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventProjectUpdated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventKilocodeAgent_managerStart"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventVcsBranchUpdated"
|
||||
},
|
||||
@@ -17033,6 +17030,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/EventInstallationUpdate-available"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EventIndexingStatus"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SyncEventMessageUpdated"
|
||||
},
|
||||
@@ -18684,6 +18684,9 @@
|
||||
"isFree": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mayTrainOnYourPrompts": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"terminalBench": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -22633,6 +22636,30 @@
|
||||
"required": ["type", "name", "id", "seq", "aggregateID", "data"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventServerInstanceDisposed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["server.instance.disposed"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["directory"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventServerConnected": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -22687,109 +22714,6 @@
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventKilocodeAgent_managerStart": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["kilocode.agent_manager.start"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requestID": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["worktree", "local"]
|
||||
},
|
||||
"versions": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"branchName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 20
|
||||
}
|
||||
},
|
||||
"required": ["requestID", "sessionID", "mode", "tasks"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventIndexingStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["indexing.status"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/IndexingStatus"
|
||||
}
|
||||
},
|
||||
"required": ["status"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventServerInstanceDisposed": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["server.instance.disposed"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"directory": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["directory"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventFileEdited": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23629,6 +23553,61 @@
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventKilocodeAgent_managerStart": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["kilocode.agent_manager.start"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"requestID": {
|
||||
"type": "string"
|
||||
},
|
||||
"sessionID": {
|
||||
"type": "string",
|
||||
"pattern": "^ses"
|
||||
},
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["worktree", "local"]
|
||||
},
|
||||
"versions": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"branchName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 20
|
||||
}
|
||||
},
|
||||
"required": ["requestID", "sessionID", "mode", "tasks"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventVcsBranchUpdated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23957,6 +23936,30 @@
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EventIndexingStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["indexing.status"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"$ref": "#/components/schemas/IndexingStatus"
|
||||
}
|
||||
},
|
||||
"required": ["status"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["id", "type", "properties"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PromptSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user