mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
Merge remote-tracking branch 'origin/main' into puzzling-crowberry
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"name": "@opencode-ai/tui",
|
||||
"version": "7.4.22",
|
||||
"version": "7.4.23",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -38,6 +38,7 @@ import path from "path"
|
||||
import { useKV } from "./kv"
|
||||
import { handleSuggestionEvent } from "@/kilocode/suggestion/tui/sync" // kilocode_change
|
||||
import { appendTerminalOutput } from "@/kilocode/interactive-terminal/output" // kilocode_change
|
||||
import { at, recent, slot } from "../kilocode/message-order" // kilocode_change
|
||||
import { useToast } from "../ui/toast" // kilocode_change
|
||||
import { usePermission } from "./permission"
|
||||
|
||||
@@ -501,7 +502,7 @@ export const {
|
||||
setStore("message", event.properties.info.sessionID, [event.properties.info])
|
||||
break
|
||||
}
|
||||
const result = search(messages, event.properties.info.id, (m) => m.id)
|
||||
const result = slot(messages, event.properties.info) // kilocode_change - order by created time, ids wrap
|
||||
if (result.found) {
|
||||
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
|
||||
break
|
||||
@@ -537,7 +538,7 @@ export const {
|
||||
case "message.removed": {
|
||||
touchMessage(event.properties.sessionID, event.properties.messageID)
|
||||
const messages = store.message[event.properties.sessionID]
|
||||
const result = search(messages, event.properties.messageID, (m) => m.id)
|
||||
const result = at(messages, event.properties.messageID) // kilocode_change - list is time-ordered, not id-sorted
|
||||
if (result.found) {
|
||||
setStore(
|
||||
"message",
|
||||
@@ -680,7 +681,7 @@ export const {
|
||||
setStore("message", info.sessionID, [info])
|
||||
break
|
||||
}
|
||||
const match = search(messages, info.id, (item) => item.id)
|
||||
const match = slot(messages, info) // kilocode_change - order by created time, ids wrap
|
||||
if (match.found) {
|
||||
setStore("message", info.sessionID, match.index, reconcile(info))
|
||||
break
|
||||
@@ -710,7 +711,7 @@ export const {
|
||||
touchMessage(event.data.sessionID, event.data.messageID)
|
||||
const messages = store.message[event.data.sessionID]
|
||||
if (!messages) break
|
||||
const match = search(messages, event.data.messageID, (item) => item.id)
|
||||
const match = at(messages, event.data.messageID) // kilocode_change - list is time-ordered, not id-sorted
|
||||
if (!match.found) break
|
||||
setStore(
|
||||
"message",
|
||||
@@ -1014,9 +1015,11 @@ export const {
|
||||
(message) => tracker.messages.has(message.id) && !infos.some((item) => item.id === message.id),
|
||||
),
|
||||
)
|
||||
const removed = infos.slice(0, -100)
|
||||
const visible = infos.slice(-100)
|
||||
// kilocode_change start - window by created time so wrapped ids stay visible
|
||||
const visible = recent(infos)
|
||||
const visibleIDs = new Set(visible.map((message) => message.id))
|
||||
const removed = infos.filter((message) => !visibleIDs.has(message.id))
|
||||
// kilocode_change end
|
||||
for (const message of messages.data ?? []) {
|
||||
if (!visibleIDs.has(message.info.id)) {
|
||||
delete draft.part[message.info.id]
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
type Stamped = { id: string; time: { created: number } }
|
||||
|
||||
export function older(a: Stamped, b: Stamped) {
|
||||
if (a.time.created !== b.time.created) return a.time.created - b.time.created
|
||||
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
||||
}
|
||||
|
||||
export function at(list: readonly { id: string }[], id: string) {
|
||||
const index = list.findIndex((item) => item.id === id)
|
||||
return { found: index >= 0, index }
|
||||
}
|
||||
|
||||
export function slot<T extends Stamped>(list: readonly T[], item: T) {
|
||||
const hit = at(list, item.id)
|
||||
if (hit.found) return hit
|
||||
const index = list.findIndex((entry) => older(item, entry) < 0)
|
||||
return { found: false, index: index < 0 ? list.length : index }
|
||||
}
|
||||
|
||||
export function recent<T extends Stamped>(list: readonly T[], cap = 100) {
|
||||
return list.toSorted(older).slice(-cap)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { expect, test } from "bun:test"
|
||||
import type { GlobalEvent } from "@kilocode/sdk/v2"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { json, mount, wait } from "../cli/cmd/tui/sync-fixture"
|
||||
|
||||
const sessionID = "ses_order"
|
||||
const partID = "prt_order"
|
||||
const directory = "/tmp/opencode/packages/tui"
|
||||
let seq = 0
|
||||
const session = {
|
||||
id: sessionID,
|
||||
title: "order",
|
||||
time: { created: 0, updated: 0 },
|
||||
version: "1.15.13",
|
||||
directory,
|
||||
}
|
||||
const base = {
|
||||
sessionID,
|
||||
role: "assistant" as const,
|
||||
agent: "build",
|
||||
modelID: "model",
|
||||
providerID: "test",
|
||||
mode: "build",
|
||||
parentID: "msg_user",
|
||||
path: { cwd: directory, root: directory },
|
||||
cost: 0,
|
||||
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
||||
}
|
||||
const old = { ...base, id: "msg_ff0cb2300001Z6YIo5V52u114f", time: { created: 1, completed: 2 } }
|
||||
const next = { ...base, id: "msg_019f1d3da001955TwEJ8qKEbj3", time: { created: 3 } }
|
||||
|
||||
function wrap(payload: GlobalEvent["payload"]): GlobalEvent {
|
||||
return { directory, project: "proj_test", payload }
|
||||
}
|
||||
|
||||
function sync1(id: string, info: (typeof base & { id: string; time: { created: number } }) | undefined): GlobalEvent {
|
||||
return {
|
||||
directory,
|
||||
project: "proj_test",
|
||||
payload: {
|
||||
type: "sync",
|
||||
syncEvent: {
|
||||
id,
|
||||
type: "message.updated.1",
|
||||
seq: ++seq,
|
||||
aggregateID: sessionID,
|
||||
data: { sessionID, info },
|
||||
},
|
||||
},
|
||||
} as GlobalEvent
|
||||
}
|
||||
|
||||
function serve(infos: (typeof old)[]) {
|
||||
return (url: URL) => {
|
||||
if (url.pathname === `/session/${sessionID}`) return json(session)
|
||||
if (url.pathname === `/session/${sessionID}/message`) {
|
||||
return json(infos.map((info) => ({ info, parts: [] })))
|
||||
}
|
||||
if (url.pathname === `/session/${sessionID}/todo` || url.pathname === `/session/${sessionID}/diff`) return json([])
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
test("a later message with a lexicographically earlier id stays at the tail", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
const { app, emit, sync } = await mount((url) => {
|
||||
if (url.pathname === `/session/${sessionID}/message`) {
|
||||
return json([{ info: old, parts: [{ id: partID, sessionID, messageID: old.id, type: "text", text: "old" }] }])
|
||||
}
|
||||
return serve([])(url)
|
||||
}, tmp.path)
|
||||
try {
|
||||
await sync.session.sync(sessionID)
|
||||
await wait(() => sync.data.message[sessionID]?.some((item) => item.id === old.id) ?? false)
|
||||
emit(
|
||||
wrap({
|
||||
id: "evt_next",
|
||||
type: "message.updated",
|
||||
properties: { sessionID, info: next },
|
||||
}),
|
||||
)
|
||||
await wait(() => sync.data.message[sessionID]?.some((item) => item.id === next.id) ?? false)
|
||||
const ids = (sync.data.message[sessionID] ?? []).map((item) => item.id)
|
||||
expect(ids[0]).toBe(old.id)
|
||||
expect(ids.at(-1)).toBe(next.id)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("eviction at the window cap drops the oldest message, not the newest", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
const infos = Array.from({ length: 100 }, (_, index) => ({
|
||||
...base,
|
||||
id: `msg_ff0c${String(index).padStart(4, "0")}`,
|
||||
time: { created: index + 1, completed: index + 2 },
|
||||
}))
|
||||
const { app, emit, sync } = await mount(serve(infos), tmp.path)
|
||||
try {
|
||||
await sync.session.sync(sessionID)
|
||||
await wait(() => sync.data.message[sessionID]?.length === 100)
|
||||
emit(
|
||||
wrap({
|
||||
id: "evt_next",
|
||||
type: "message.updated",
|
||||
properties: { sessionID, info: { ...next, time: { created: 200 } } },
|
||||
}),
|
||||
)
|
||||
await wait(() => sync.data.message[sessionID]?.some((item) => item.id === next.id) ?? false)
|
||||
const ids = (sync.data.message[sessionID] ?? []).map((item) => item.id)
|
||||
expect(ids).toHaveLength(100)
|
||||
expect(ids.at(-1)).toBe(next.id)
|
||||
expect(ids).not.toContain(infos[0].id)
|
||||
expect(ids[0]).toBe(infos[1].id)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("the versioned sync channel also keeps a wrapped id at the tail", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await Bun.write(`${tmp.path}/kv.json`, "{}")
|
||||
const { app, emit, sync } = await mount(serve([old]), tmp.path)
|
||||
try {
|
||||
await sync.session.sync(sessionID)
|
||||
await wait(() => sync.data.message[sessionID]?.some((item) => item.id === old.id) ?? false)
|
||||
emit(sync1("evt_next_v1", next))
|
||||
await wait(() => sync.data.message[sessionID]?.some((item) => item.id === next.id) ?? false)
|
||||
const ids = (sync.data.message[sessionID] ?? []).map((item) => item.id)
|
||||
expect(ids[0]).toBe(old.id)
|
||||
expect(ids.at(-1)).toBe(next.id)
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { expect, test } from "bun:test"
|
||||
import { at, older, recent, slot } from "../../src/kilocode/message-order"
|
||||
|
||||
const old = { id: "msg_ff0cb2300001Z6YIo5V52u114f", time: { created: 1 } }
|
||||
const next = { id: "msg_019f1d3da001955TwEJ8qKEbj3", time: { created: 2 } }
|
||||
|
||||
test("a later message with an earlier id sorts after the older one", () => {
|
||||
expect(older(old, next)).toBeLessThan(0)
|
||||
expect(older(next, old)).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test("equal created times fall back to id order", () => {
|
||||
const a = { id: "msg_a", time: { created: 1 } }
|
||||
const b = { id: "msg_b", time: { created: 1 } }
|
||||
expect(older(a, b)).toBeLessThan(0)
|
||||
expect(older(a, a)).toBe(0)
|
||||
})
|
||||
|
||||
test("a later message with an earlier id inserts at the tail", () => {
|
||||
expect(slot([old], next)).toEqual({ found: false, index: 1 })
|
||||
})
|
||||
|
||||
test("slot finds an existing message by id", () => {
|
||||
expect(slot([old, next], next)).toEqual({ found: true, index: 1 })
|
||||
})
|
||||
|
||||
test("the message window keeps newest by created time, not id", () => {
|
||||
expect(recent([next, old], 1).map((item) => item.id)).toEqual([next.id])
|
||||
})
|
||||
|
||||
test("lookup by id is linear so time-ordered lists still find removals", () => {
|
||||
expect(at([old, next], next.id)).toEqual({ found: true, index: 1 })
|
||||
expect(at([old, next], "msg_missing")).toEqual({ found: false, index: -1 })
|
||||
})
|
||||
Reference in New Issue
Block a user