Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clear-completed-indicator-on-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Clear completed session indicators when you return to their chat across sidebar, editor, worktree, and subagent tabs. Keep indicators for sessions that finish while already open, and preserve unresolved input requests, errors, and turn diagnostics.
25 changes: 25 additions & 0 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private readonly anacondaDesktop = new AnacondaDesktopBridge()
private sessionStatusMap = new Map<string, SessionStatus["type"]>() // Latest status used for destructive config warnings.
private activity: Activity = "idle"
private active = false
private caption: string | undefined
private readonly epochs = new Map<string, Map<string, number>>()
private readonly requests = new Map<string, number>()
Expand Down Expand Up @@ -441,6 +442,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private unsubscribeState: (() => void) | null = null
private migrationCache: MigrationContext["migrationCache"] = new Map()
private unsubscribeNotificationDismiss: (() => void) | null = null
private unsubscribeAcknowledged: (() => void) | null = null
private unsubscribeLanguageChange: (() => void) | null = null
private unsubscribeProfileChange: (() => void) | null = null
private unsubscribeFavoritesChange: (() => void) | null = null
Expand Down Expand Up @@ -582,6 +584,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper

public setStreamVisibility(active: boolean): void {
this.visibleTaskStreams.setActive(active)
this.active = active
if (!this.isWebviewReady) return
this.postMessage({ type: "webviewActiveChanged", active })
}

public setProjectDirectory(directory: string | null): void {
Expand Down Expand Up @@ -805,11 +810,13 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.setupWebviewMessageHandler(panel.webview)
this.viewStateDisposable?.dispose()
this.viewStateDisposable = this.visibleTaskStreams.bindPanel(panel, () => {
this.setStreamVisibility(panel.active && panel.visible)
if (this.opts.disableViewedRegistration) return
const id = this.contextSessionID
this.streams.focus(panel.visible ? id : undefined)
this.connectionService.registerVisible(this.instanceId, panel.visible && id ? [id] : [])
})
this.setStreamVisibility(panel.active && panel.visible)
this.initializeConnection()
}

Expand Down Expand Up @@ -1014,8 +1021,20 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.initializeConnection()
}

private acknowledge(message: Record<string, unknown>): boolean {
if (message.type !== "acknowledgeSession") return false
if (typeof message.sessionID === "string" && typeof message.eventID === "string") {
this.connectionService.notifySessionAcknowledged(message.sessionID, message.eventID)
}
return true
}

private setupWebviewMessageHandler(webview: vscode.Webview): void {
this.webviewMessageDisposable?.dispose()
this.unsubscribeAcknowledged?.()
this.unsubscribeAcknowledged = this.connectionService.onSessionAcknowledged((sessionID, eventID) => {
this.postMessage({ type: "sessionAcknowledged", sessionID, eventID })
})
this.setFocusTarget("other")
this.autocompleteConfigDisposable?.dispose()
this.autocompleteConfigDisposable = watchAutocompleteConfig((msg) => this.postMessage(msg))
Expand All @@ -1030,6 +1049,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.telemetryStateDisposable?.dispose()
this.telemetryStateDisposable = watchTelemetryState((msg) => this.postMessage(msg))
this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => {
if (this.acknowledge(message)) return
const intercepted = await interceptMessage(message, {
workspaceDir: (sid) => this.getWorkspaceDirectory(sid ?? this.currentSession?.id),
post: (m) => this.postMessage(m),
Expand Down Expand Up @@ -1109,6 +1129,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "webviewReady":
console.log("[Kilo New] KiloProvider: ✅ webviewReady received")
this.isWebviewReady = true
for (const event of this.connectionService.getPendingCompletions()) {
this.postMessage(mapSSEEventToWebviewMessage(event, event.properties.sessionID))
}
this.postMessage({ type: "webviewActiveChanged", active: this.active })
this.visibleTaskStreams.clear()
this.flushPendingKiloModel()
await this.syncWebviewState("webviewReady")
Expand Down Expand Up @@ -5469,6 +5493,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.unsubscribeClearPendingPrompts?.()
this.unsubscribeDirectoryProvider?.()
this.unsubscribeSandboxPreference?.()
this.unsubscribeAcknowledged?.()
this.viewStateDisposable?.dispose()
this.visibilityDisposable?.dispose()
this.webviewMessageDisposable?.dispose()
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/src/kilo-provider-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ export type WebviewMessage =
| {
type: "sessionTurnClosed"
sessionID: string
eventID: string
reason: "completed" | "error" | "interrupted" | "superseded"
parentID?: string
}
Expand Down Expand Up @@ -560,6 +561,7 @@ export function mapSSEEventToWebviewMessage(event: StreamEvent, sessionID: strin
return {
type: "sessionTurnClosed",
sessionID: event.properties.sessionID,
eventID: event.id,
reason: event.properties.reason,
...(event.properties.parentID ? { parentID: event.properties.parentID } : {}),
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as vscode from "vscode"
import { ServerManager } from "./server-manager"
import { createKiloClient, type KiloClient } from "@kilocode/sdk/v2/client"
import { createKiloClient, type EventSessionTurnClose, type KiloClient } from "@kilocode/sdk/v2/client"
import { SdkSSEAdapter, type SSEPayload } from "./sdk-sse-adapter"
import type { ServerConfig } from "./types"
import { createDuplicateEventFilter, resolveEventSessionId as resolveEventSessionIdPure } from "./connection-utils"
Expand All @@ -12,6 +12,7 @@ type SSEEventListener = (event: SSEPayload, directory?: string) => void
type StateListener = (state: ConnectionState, error?: Error) => void
type SSEEventFilter = (event: SSEPayload, directory?: string) => boolean
type NotificationDismissListener = (notificationId: string) => void
type SessionAcknowledgedListener = (sessionID: string, eventID: string) => void
type LanguageChangeListener = (locale: string) => void
type ProfileChangeListener = (data: unknown) => void
type FavoritesChangeListener = (favorites: Array<{ providerID: string; modelID: string }>) => void
Expand Down Expand Up @@ -100,6 +101,8 @@ export class KiloConnectionService {
private readonly explicitAborts = new ExplicitAbortState()
private readonly stateListeners: Set<StateListener> = new Set()
private readonly notificationDismissListeners: Set<NotificationDismissListener> = new Set()
private readonly sessionAcknowledgedListeners: Set<SessionAcknowledgedListener> = new Set()
private readonly completions = new Map<string, { id: string; event?: EventSessionTurnClose }>()
private readonly languageChangeListeners: Set<LanguageChangeListener> = new Set()
private readonly profileChangeListeners: Set<ProfileChangeListener> = new Set()
private readonly favoritesChangeListeners: Set<FavoritesChangeListener> = new Set()
Expand Down Expand Up @@ -433,6 +436,25 @@ export class KiloConnectionService {
}
}

onSessionAcknowledged(listener: SessionAcknowledgedListener): () => void {
this.sessionAcknowledgedListeners.add(listener)
return () => {
this.sessionAcknowledgedListeners.delete(listener)
}
}

notifySessionAcknowledged(sessionID: string, eventID: string): void {
const state = this.completions.get(sessionID)
if (state?.event?.id === eventID) state.event = undefined
for (const listener of this.sessionAcknowledgedListeners) {
listener(sessionID, eventID)
}
}

getPendingCompletions() {
return [...this.completions.values()].flatMap((state) => (state.event ? [state.event] : []))
}

/**
* Subscribe to language change events broadcast from any KiloProvider. Returns unsubscribe function.
*/
Expand Down Expand Up @@ -691,6 +713,8 @@ export class KiloConnectionService {
this.explicitAborts.clear()
this.stateListeners.clear()
this.notificationDismissListeners.clear()
this.sessionAcknowledgedListeners.clear()
this.completions.clear()
this.profileChangeListeners.clear()
this.favoritesChangeListeners.clear()
this.clearPendingPromptsListeners.clear()
Expand Down Expand Up @@ -897,6 +921,27 @@ export class KiloConnectionService {
}

private broadcast(event: SSEPayload, directory?: string): void {
if (event.type === "session.turn.close") {
const sid = event.properties.sessionID
const state = this.completions.get(sid)
if (!state || event.id > state.id) {
this.completions.set(sid, {
id: event.id,
event: event.properties.reason === "completed" ? event : undefined,
})
}
}
if (
event.type === "session.turn.open" ||
(event.type === "session.status" && event.properties.status.type !== "idle") ||
event.type === "session.error" ||
event.type === "session.deleted" ||
(event.type === "sync" && event.name === "session.deleted.1")
) {
const sid = event.type === "sync" ? event.data.sessionID : event.properties.sessionID
const state = sid ? this.completions.get(sid) : undefined
if (sid && state && event.id > state.id) this.completions.set(sid, { id: event.id })
}
this.handlePermissionEvent(event, directory)
this.handleQuestionEvent(event, directory)
for (const listener of this.eventListeners) listener(event, directory)
Expand Down
Loading
Loading