fix(vscode): hide manual interruption warning

This commit is contained in:
marius-kilocode
2026-08-24 17:08:16 +02:00
parent ff74e2ea3f
commit e779480abc
12 changed files with 465 additions and 52 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Stop manually aborted turns without briefly showing an interruption warning.
+9 -2
View File
@@ -4036,7 +4036,15 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.cancelRetry(sid)
const client = this.client
if (!client) return Promise.resolve(false)
return this.aborts.stop(client, sid, this.getWorkspaceDirectory(sid))
const directory = this.getWorkspaceDirectory(sid)
const dirs = this.aborts.directories(sid, directory)
const ids = new Map(dirs.map((dir) => [dir, this.connectionService.beginExplicitAbort(sid, dir)]))
return this.aborts.stop(client, sid, directory, dirs).then((result) => {
for (const attempt of result.attempts) {
this.connectionService.finishExplicitAbort(sid, attempt.dir, ids.get(attempt.dir)!, attempt.aborted)
}
return result.complete
})
}
private async handleAbort(sessionID?: string): Promise<void> {
@@ -4044,7 +4052,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (!sid || !(await this.stopSession(sid))) return
this.sessionStatusMap.set(sid, "idle")
this.streams.flush(sid)
this.postMessage({ type: "sessionTurnClosed", sessionID: sid, reason: "interrupted" })
this.postMessage({ type: "sessionStatus", sessionID: sid, status: "idle" })
}
@@ -1804,7 +1804,7 @@ export class AgentManagerProvider implements Disposable {
await continueInWorktree(
{
root,
getClient: () => this.connectionService.getClient(),
connection: this.connectionService,
createWorktreeOnDisk: (opts) => this.createWorktreeOnDisk(opts),
runSetupScript: (p, b, id) => this.runSetupScriptForWorktree(p, b, id),
cleanupWorktree: async (id) => {
@@ -8,7 +8,10 @@ import { recordForkHandoff } from "./fork-handoff"
export interface ContinueContext {
root: string
getClient: () => KiloClient
connection: {
getClient: () => KiloClient
runExplicitAbort: <T>(sessionId: string, directory: string, action: () => Promise<T>) => Promise<T>
}
createWorktreeOnDisk: (opts: { baseBranch: string; baseRef: string }) => Promise<{
worktree: { id: string }
result: CreateWorktreeResult
@@ -30,10 +33,13 @@ export type StepResult<T> = { ok: true; value: T } | { ok: false; error: string
/** Abort a running session. Best-effort — failures are logged but not fatal. */
export async function abortSession(ctx: ContinueContext, sessionId: string): Promise<void> {
try {
const client = ctx.getClient()
await client.session.abort({ sessionID: sessionId }).catch((err) => {
ctx.log("Session abort failed (may already be idle):", getErrorMessage(err))
})
await ctx.connection
.runExplicitAbort(sessionId, ctx.root, async () => {
await ctx.connection.getClient().session.abort({ sessionID: sessionId }, { throwOnError: true })
})
.catch((err) => {
ctx.log("Session abort failed (may already be idle):", getErrorMessage(err))
})
} catch (err) {
ctx.log("Client not available for abort, continuing:", getErrorMessage(err))
}
@@ -96,7 +102,7 @@ async function rollback(
export async function forkSession(ctx: ContinueContext, sessionId: string, dir: string): Promise<StepResult<Session>> {
let client: KiloClient
try {
client = ctx.getClient()
client = ctx.connection.getClient()
} catch (err) {
ctx.log("Client not available for session fork:", getErrorMessage(err))
return { ok: false, error: "Not connected to CLI backend" }
@@ -27,20 +27,27 @@ export class SessionAbort {
this.observe(sessionID, status, dir)
}
async stop(client: KiloClient, sessionID: string, fallback: string) {
const known = this.active.has(sessionID)
directories(sessionID: string, fallback: string) {
const dirs = [...(this.active.get(sessionID) ?? [])]
if (!dirs.some((dir) => sameDirectory(dir, fallback))) dirs.push(fallback)
return dirs
}
async stop(client: KiloClient, sessionID: string, fallback: string, dirs = this.directories(sessionID, fallback)) {
const known = this.active.has(sessionID)
const results = await Promise.allSettled(dirs.map((dir) => abortSession({ client, sessionID, dir })))
const failures = results.flatMap((result, index) =>
result.status === "rejected" ? [{ dir: dirs[index], error: result.reason }] : [],
)
if (failures.length > 0) {
console.error("[Kilo New] KiloProvider: Failed to abort session in one or more directories:", failures)
return false
return {
complete: false,
attempts: results.map((result, index) => ({ dir: dirs[index], aborted: result.status === "fulfilled" })),
}
}
if (known) this.active.delete(sessionID)
return known
return { complete: known, attempts: dirs.map((dir) => ({ dir, aborted: true })) }
}
dispose(dir: string) {
@@ -1,6 +1,7 @@
import { describe, expect, test } from "bun:test"
import * as vscode from "vscode"
import { KiloConnectionService } from "./connection-service"
import type { SSEPayload } from "./sdk-sse-adapter"
function state(value: boolean) {
return {
@@ -39,6 +40,74 @@ describe("KiloConnectionService clients", () => {
})
})
describe("KiloConnectionService explicit aborts", () => {
const close = {
id: "event-close",
type: "session.turn.close",
properties: { sessionID: "session", reason: "interrupted" },
} as SSEPayload
const status = {
type: "session.status",
properties: { sessionID: "session", status: { type: "busy" } },
} as SSEPayload
test("suppresses a successful explicit abort for every subscriber", () => {
const service = new KiloConnectionService({} as any)
const raw: SSEPayload[] = []
const first: SSEPayload[] = []
const second: SSEPayload[] = []
service.onEvent((event) => raw.push(event))
service.onEventFiltered(
() => true,
(event) => first.push(event),
)
service.onEventFiltered(
() => true,
(event) => second.push(event),
)
;(service as any).broadcast(status, "/repo")
raw.length = 0
first.length = 0
second.length = 0
const id = service.beginExplicitAbort("session", "/repo")
;(service as any).broadcast(close, "/repo")
service.finishExplicitAbort("session", "/repo", id, true)
expect(first).toEqual([])
expect(second).toEqual([])
expect(raw).toEqual([close])
})
test("replays a failed explicit abort for every subscriber", () => {
const service = new KiloConnectionService({} as any)
const raw: SSEPayload[] = []
const first: SSEPayload[] = []
const second: SSEPayload[] = []
service.onEvent((event) => raw.push(event))
service.onEventFiltered(
() => true,
(event) => first.push(event),
)
service.onEventFiltered(
() => true,
(event) => second.push(event),
)
;(service as any).broadcast(status, "/repo")
raw.length = 0
first.length = 0
second.length = 0
const id = service.beginExplicitAbort("session", "/repo")
;(service as any).broadcast(close, "/repo")
service.finishExplicitAbort("session", "/repo", id, false)
expect(first).toEqual([close])
expect(second).toEqual([close])
expect(raw).toEqual([close])
})
})
describe("KiloConnectionService viewed sessions", () => {
test("keeps Agent Manager sessions when sidebar visibility changes during a flush", async () => {
const service = new KiloConnectionService({} as any)
@@ -5,6 +5,7 @@ import { SdkSSEAdapter, type SSEPayload } from "./sdk-sse-adapter"
import type { ServerConfig } from "./types"
import { resolveEventSessionId as resolveEventSessionIdPure } from "./connection-utils"
import { SandboxPreference } from "../sandbox-preference"
import { ExplicitAbortState } from "./explicit-abort"
export type ConnectionState = "connecting" | "connected" | "disconnected" | "error"
type SSEEventListener = (event: SSEPayload, directory?: string) => void
@@ -96,6 +97,8 @@ export class KiloConnectionService {
private remoteService: import("../RemoteStatusService").RemoteStatusService | null = null
private readonly eventListeners: Set<SSEEventListener> = new Set()
private readonly filteredListeners = new Set<{ filter: SSEEventFilter; listener: SSEEventListener }>()
private readonly explicitAborts = new ExplicitAbortState()
private readonly stateListeners: Set<StateListener> = new Set()
private readonly notificationDismissListeners: Set<NotificationDismissListener> = new Set()
private readonly languageChangeListeners: Set<LanguageChangeListener> = new Set()
@@ -276,13 +279,34 @@ export class KiloConnectionService {
* Subscribe to SSE events with a filter. The filter runs for every incoming SSE event.
*/
onEventFiltered(filter: SSEEventFilter, listener: SSEEventListener): () => void {
const wrapped: SSEEventListener = (event, directory) => {
if (!filter(event, directory)) {
return
}
listener(event, directory)
const entry = { filter, listener }
this.filteredListeners.add(entry)
return () => {
this.filteredListeners.delete(entry)
}
return this.onEvent(wrapped)
}
beginExplicitAbort(sessionID: string, directory: string): number | undefined {
return this.explicitAborts.begin(sessionID, directory)
}
finishExplicitAbort(sessionID: string, directory: string, id: number | undefined, stopped: boolean): void {
for (const item of this.explicitAborts.finish(sessionID, directory, id, stopped))
this.broadcastFiltered(item.event, item.directory)
}
async runExplicitAbort<T>(sessionID: string, directory: string, action: () => Promise<T>): Promise<T> {
const id = this.beginExplicitAbort(sessionID, directory)
return action().then(
(result) => {
this.finishExplicitAbort(sessionID, directory, id, true)
return result
},
(error) => {
this.finishExplicitAbort(sessionID, directory, id, false)
throw error
},
)
}
/**
@@ -305,6 +329,7 @@ export class KiloConnectionService {
* id after external (CLI/TUI/cascade) deletes arrive via SSE.
*/
pruneSession(sessionId: string): void {
this.explicitAborts.remove(sessionId)
for (const [mid, sid] of this.messageSessionIdsByMessageId) {
if (sid === sessionId) this.messageSessionIdsByMessageId.delete(mid)
}
@@ -681,6 +706,8 @@ export class KiloConnectionService {
this.sseClient?.dispose()
this.serverManager.dispose()
this.eventListeners.clear()
this.filteredListeners.clear()
this.explicitAborts.clear()
this.stateListeners.clear()
this.notificationDismissListeners.clear()
this.profileChangeListeners.clear()
@@ -780,6 +807,7 @@ export class KiloConnectionService {
this.stopHealthPoll()
this.stopCheckin()
const sse = this.sseClient
this.explicitAborts.clear()
this.sseClient = null
sse?.disconnect()
this.client = null
@@ -837,11 +865,7 @@ export class KiloConnectionService {
// Wire SSE events → broadcast to all registered listeners
sse.onEvent((event, directory) => {
if (this.sseClient !== sse) return
this.handlePermissionEvent(event, directory)
this.handleQuestionEvent(event, directory)
for (const listener of this.eventListeners) {
listener(event, directory)
}
this.broadcast(event, directory)
})
sse.onError((error) => {
@@ -887,6 +911,20 @@ export class KiloConnectionService {
this.startHealthPoll(config.baseUrl, config.password)
}
private broadcast(event: SSEPayload, directory?: string): void {
this.handlePermissionEvent(event, directory)
this.handleQuestionEvent(event, directory)
for (const listener of this.eventListeners) listener(event, directory)
if (!this.explicitAborts.event(event, directory)) return
this.broadcastFiltered(event, directory)
}
private broadcastFiltered(event: SSEPayload, directory?: string): void {
for (const entry of this.filteredListeners) {
if (entry.filter(event, directory)) entry.listener(event, directory)
}
}
private startCheckin(): void {
this.stopCheckin()
this.checkinTimer = setInterval(() => this.flushViewed(), 60_000)
@@ -0,0 +1,112 @@
import path from "node:path"
import type { SSEPayload } from "./sdk-sse-adapter"
type Buffered = { event: SSEPayload; directory?: string }
type State = { attempts: Set<number>; stopped: boolean; buffered: Buffered[]; generation: number; idle: boolean }
export class ExplicitAbortState {
private readonly active = new Set<string>()
private readonly states = new Map<string, State>()
private readonly generations = new Map<string, number>()
private next = 0
begin(sessionID: string, directory: string): number | undefined {
const key = scope(sessionID, directory)
if (!this.active.has(key)) return
const id = ++this.next
const state = this.states.get(key) ?? {
attempts: new Set(),
stopped: false,
buffered: [],
generation: this.generations.get(key) ?? 0,
idle: false,
}
state.attempts.add(id)
this.states.set(key, state)
return id
}
finish(sessionID: string, directory: string, id: number | undefined, stopped: boolean): Buffered[] {
if (id === undefined) return []
const key = scope(sessionID, directory)
const state = this.states.get(key)
if (!state || !state.attempts.delete(id)) return []
if (stopped) {
state.stopped = true
state.buffered = []
return []
}
if (state.stopped || state.attempts.size > 0) return []
this.states.delete(key)
return state.buffered
}
event(event: SSEPayload, directory?: string): boolean {
if (event.type === "session.status" && directory) return this.status(event, directory)
if (event.type === "session.turn.open") return this.open(event.properties.sessionID, directory)
if (event.type !== "session.turn.close") return true
const keys = this.keys(event.properties.sessionID, directory).filter((key) => this.states.has(key))
if (keys.length !== 1) return true
const key = keys[0]
const state = this.states.get(key)
if (!state) return true
if (state.generation !== (this.generations.get(key) ?? 0) || event.properties.reason !== "interrupted") {
this.states.delete(key)
return true
}
if (state.stopped) return false
if (state.attempts.size === 0) {
this.states.delete(key)
return true
}
state.buffered.push({ event, directory })
return false
}
private status(event: Extract<SSEPayload, { type: "session.status" }>, directory: string) {
const key = scope(event.properties.sessionID, directory)
const state = this.states.get(key)
if (event.properties.status.type === "idle") {
this.active.delete(key)
if (state) state.idle = true
return true
}
this.active.add(key)
if (state?.idle) this.states.delete(key)
return true
}
private open(sessionID: string, directory?: string) {
for (const key of this.keys(sessionID, directory)) {
this.generations.set(key, (this.generations.get(key) ?? 0) + 1)
this.states.delete(key)
}
return true
}
clear() {
this.active.clear()
this.states.clear()
this.generations.clear()
}
remove(sessionID: string) {
for (const key of this.keys(sessionID)) {
this.active.delete(key)
this.states.delete(key)
this.generations.delete(key)
}
}
private keys(sessionID: string, directory?: string): string[] {
if (directory) return [scope(sessionID, directory)]
const prefix = `${sessionID}\0`
return [...new Set([...this.active, ...this.states.keys(), ...this.generations.keys()])].filter((key) =>
key.startsWith(prefix),
)
}
}
function scope(sessionID: string, directory: string) {
return `${sessionID}\0${path.resolve(directory)}`
}
+26 -3
View File
@@ -20,7 +20,13 @@ describe("SessionAbort", () => {
const aborts = new SessionAbort()
aborts.observe("session_1", "busy", "/repo")
expect(await aborts.stop(client(calls), "session_1", "/repo/worktree")).toBe(true)
expect(await aborts.stop(client(calls), "session_1", "/repo/worktree")).toEqual({
complete: true,
attempts: [
{ dir: "/repo", aborted: true },
{ dir: "/repo/worktree", aborted: true },
],
})
expect(calls).toEqual([
{
type: "abort",
@@ -41,7 +47,10 @@ describe("SessionAbort", () => {
aborts.observe("session_1", "busy", "/repo")
aborts.observe("session_1", "idle", "/repo")
expect(await aborts.stop(client(calls), "session_1", "/repo/worktree")).toBe(false)
expect(await aborts.stop(client(calls), "session_1", "/repo/worktree")).toEqual({
complete: false,
attempts: [{ dir: "/repo/worktree", aborted: true }],
})
expect(calls).toEqual([
{
type: "abort",
@@ -56,9 +65,23 @@ describe("SessionAbort", () => {
const aborts = new SessionAbort()
aborts.observe("session_1", "busy", "/repo/worktree")
expect(await aborts.stop(client(calls), "session_1", "/repo/worktree/.")).toBe(true)
expect(await aborts.stop(client(calls), "session_1", "/repo/worktree/.")).toEqual({
complete: true,
attempts: [{ dir: "/repo/worktree", aborted: true }],
})
expect(calls).toHaveLength(1)
})
it("reports a failed HTTP abort separately from ownership", async () => {
const calls: unknown[] = []
const aborts = new SessionAbort()
aborts.observe("session_1", "busy", "/repo")
expect(await aborts.stop(client(calls, true), "session_1", "/repo")).toEqual({
complete: false,
attempts: [{ dir: "/repo", aborted: false }],
})
})
})
describe("abortSession", () => {
@@ -72,8 +72,11 @@ function result(path: string): CreateWorktreeResult {
function ctx(overrides: Partial<ContinueContext> = {}): ContinueContext {
return {
root: "/tmp/test",
getClient: () => {
throw new Error("no client")
connection: {
getClient: () => {
throw new Error("no client")
},
runExplicitAbort: async (_sessionId, _directory, action) => action(),
},
createWorktreeOnDisk: async () => null,
runSetupScript: async () => {},
@@ -98,10 +101,12 @@ describe("continue-in-worktree steps", () => {
it("does not throw when abort rejects", async () => {
const c = ctx({
getClient: () =>
({
session: { abort: () => Promise.reject(new Error("fail")) },
}) as never,
connection: {
getClient: () => ({ session: { abort: async () => undefined } }) as never,
runExplicitAbort: async () => {
throw new Error("fail")
},
},
})
await abortSession(c, "session-1")
})
@@ -109,15 +114,17 @@ describe("continue-in-worktree steps", () => {
it("calls abort on the client", async () => {
let called = false
const c = ctx({
getClient: () =>
({
session: {
abort: () => {
called = true
return Promise.resolve()
connection: {
getClient: () =>
({
session: {
abort: async () => {
called = true
},
},
},
}) as never,
}) as never,
runExplicitAbort: async (_sessionId, _directory, action) => action(),
},
})
await abortSession(c, "session-1")
expect(called).toBe(true)
@@ -134,10 +141,13 @@ describe("continue-in-worktree steps", () => {
it("returns error when fork rejects", async () => {
const c = ctx({
getClient: () =>
({
session: { fork: () => Promise.reject(new Error("fork failed")) },
}) as never,
connection: {
getClient: () =>
({
session: { fork: () => Promise.reject(new Error("fork failed")) },
}) as never,
runExplicitAbort: async (_s, _d, action) => action(),
},
})
const res = await forkSession(c, "session-1", "/tmp/wt")
expect(res.ok).toBe(false)
@@ -148,10 +158,13 @@ describe("continue-in-worktree steps", () => {
const forked = session("forked-1")
const promptAsync = mock(async () => ({}))
const c = ctx({
getClient: () =>
({
session: { fork: () => Promise.resolve({ data: forked }), promptAsync },
}) as never,
connection: {
getClient: () =>
({
session: { fork: () => Promise.resolve({ data: forked }), promptAsync },
}) as never,
runExplicitAbort: async (_s, _d, action) => action(),
},
})
const res = await forkSession(c, "session-1", "/tmp/wt")
expect(res.ok).toBe(true)
@@ -215,7 +228,7 @@ describe("continueInWorktree", () => {
let created: CreateWorktreeResult | undefined
const c = ctx({
root,
getClient: () => api,
connection: { getClient: () => api, runExplicitAbort: async (_s, _d, action) => action() },
createWorktreeOnDisk: async (opts) => {
const value = await manager.createWorktree(opts)
created = value
@@ -246,7 +259,7 @@ describe("continueInWorktree", () => {
let created: CreateWorktreeResult | undefined
const c = ctx({
root,
getClient: () => client(),
connection: { getClient: () => client(), runExplicitAbort: async (_s, _d, action) => action() },
createWorktreeOnDisk: async (opts) => {
const value = await manager.createWorktree(opts)
created = value
@@ -0,0 +1,131 @@
import { describe, expect, it } from "bun:test"
import type { SSEPayload } from "../../src/services/cli-backend/sdk-sse-adapter"
import { ExplicitAbortState } from "../../src/services/cli-backend/explicit-abort"
const open = (sessionID = "session") =>
({ id: "event-open", type: "session.turn.open", properties: { sessionID } }) as SSEPayload
const status = (type: "idle" | "busy", sessionID = "session") =>
({ type: "session.status", properties: { sessionID, status: { type } } }) as SSEPayload
const close = (reason: "completed" | "interrupted", sessionID = "session") =>
({ id: `event-${reason}`, type: "session.turn.close", properties: { sessionID, reason } }) as SSEPayload
describe("explicit abort state", () => {
it("does not suppress an unexpected interruption", () => {
const state = new ExplicitAbortState()
expect(state.event(close("interrupted"))).toBe(true)
})
it("drops an interrupted close after an explicit abort succeeds", () => {
const state = new ExplicitAbortState()
state.event(status("busy"), "/repo")
const id = state.begin("session", "/repo")
expect(state.event(close("interrupted"), "/repo")).toBe(false)
expect(state.finish("session", "/repo", id, true)).toEqual([])
})
it("drops an interrupted close that arrives after abort success", () => {
const state = new ExplicitAbortState()
state.event(status("busy"), "/repo")
const id = state.begin("session", "/repo")
state.finish("session", "/repo", id, true)
expect(state.event(close("interrupted"), "/repo")).toBe(false)
})
it("replays an interrupted close when the abort fails", () => {
const state = new ExplicitAbortState()
state.event(status("busy"), "/repo")
const id = state.begin("session", "/repo")
const event = close("interrupted")
state.event(event, "/repo")
expect(state.finish("session", "/repo", id, false)).toEqual([{ event, directory: "/repo" }])
})
it("never suppresses a completed close", () => {
const state = new ExplicitAbortState()
state.event(status("busy"), "/repo")
state.begin("session", "/repo")
expect(state.event(close("completed"))).toBe(true)
})
it("waits for concurrent abort attempts before replaying", () => {
const state = new ExplicitAbortState()
state.event(status("busy"), "/repo")
const first = state.begin("session", "/repo")
const second = state.begin("session", "/repo")
state.event(close("interrupted"), "/repo")
expect(state.finish("session", "/repo", first, false)).toEqual([])
expect(state.finish("session", "/repo", second, true)).toEqual([])
})
it("allows a later real interruption in the same session", () => {
const state = new ExplicitAbortState()
state.event(open(), "/repo")
state.event(status("busy"), "/repo")
const id = state.begin("session", "/repo")
state.finish("session", "/repo", id, true)
expect(state.event(close("interrupted"), "/repo")).toBe(false)
state.event(status("idle"), "/repo")
state.event(open(), "/repo")
state.event(status("busy"), "/repo")
expect(state.event(close("interrupted"), "/repo")).toBe(true)
})
it("clears a pending abort when a new turn opens", () => {
const state = new ExplicitAbortState()
state.event(status("busy"), "/repo")
const id = state.begin("session", "/repo")
state.event(open(), "/repo")
expect(state.event(close("interrupted"), "/repo")).toBe(true)
expect(state.finish("session", "/repo", id, true)).toEqual([])
})
it("isolates identical session ids by directory", () => {
const state = new ExplicitAbortState()
state.event(status("busy"), "/repo/a")
const id = state.begin("session", "/repo/a")
state.finish("session", "/repo/a", id, true)
expect(state.event(close("interrupted"), "/repo/b")).toBe(true)
expect(state.event(close("interrupted"), "/repo/a")).toBe(false)
})
it("does not mark an already idle session", () => {
const state = new ExplicitAbortState()
state.event(status("idle"), "/repo")
expect(state.begin("session", "/repo")).toBeUndefined()
expect(state.event(close("interrupted"), "/repo")).toBe(true)
})
it("clears suppression on a later busy status without turn-open", () => {
const state = new ExplicitAbortState()
state.event(status("busy"), "/repo")
const id = state.begin("session", "/repo")
state.finish("session", "/repo", id, true)
state.event(status("idle"), "/repo")
state.event(status("busy"), "/repo")
expect(state.event(close("interrupted"), "/repo")).toBe(true)
})
it("does not carry a pending abort into a new busy turn", () => {
const state = new ExplicitAbortState()
state.event(status("busy"), "/repo")
const id = state.begin("session", "/repo")
state.event(status("idle"), "/repo")
state.event(status("busy"), "/repo")
state.finish("session", "/repo", id, true)
expect(state.event(close("interrupted"), "/repo")).toBe(true)
})
})
@@ -191,6 +191,8 @@ function createConnection(client: ReturnType<typeof createClient>) {
},
connect: async () => {},
getClient: () => client,
beginExplicitAbort: () => 1 as number | undefined,
finishExplicitAbort: () => undefined,
onEventFiltered: () => () => undefined,
onStateChange: (_l: (s: State) => void) => () => undefined,
onNotificationDismissed: () => () => undefined,
@@ -1238,7 +1240,7 @@ describe("KiloProvider.handleLoadMessages / slim payload", () => {
expect(client.aborted).toContainEqual({ sessionID: "s1", directory: "/repo" })
expect(sent).toContainEqual({ type: "sessionCostAlertResolved", sessionID: "s1", limit: 1 })
expect(sent).toContainEqual({ type: "sessionTurnClosed", sessionID: "s1", reason: "interrupted" })
expect(sent).not.toContainEqual({ type: "sessionTurnClosed", sessionID: "s1", reason: "interrupted" })
})
it("strips transcript-only metadata before posting messages to the webview", async () => {