feat(vscode): support sidebar message forks (#9244)

* feat(vscode): support sidebar message forks

* fix(vscode): type sidebar fork message
This commit is contained in:
Marius
2026-04-20 13:54:28 +00:00
committed by GitHub
parent 2b17a7b4e8
commit 18442f695d
5 changed files with 94 additions and 22 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---
Support forking sidebar conversations from any user message.
+17 -21
View File
@@ -55,6 +55,7 @@ import { fetchMessagePage, MESSAGE_PAGE_LIMIT } from "./kilo-provider/message-pa
import { childID } from "./kilo-provider/task-session"
import { handleNetworkEvent, clearNetworkWaits } from "./kilo-provider/network"
import { abortSession, parseQueued } from "./kilo-provider/abort"
import { handleForkSession } from "./kilo-provider/fork-session"
import { retryable, backoff, MAX_RETRIES } from "./util/retry"
import { hasGit } from "./kilo-provider/git-status"
// legacy-migration start
@@ -303,11 +304,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return slimParts(parts)
}
/**
* Synchronize current extension-side state to the webview.
* This is primarily used after a webview refresh where early postMessage calls
* may have been dropped before the webview registered its message listeners.
*/
private get forkCtx() {
return {
connection: this.connectionService,
post: (msg: { type: "error"; message: string }) => this.postMessage(msg),
register: (session: Session) => this.registerSession(session),
forked: (session: Session) => this.postMessage({ type: "sessionForked", sessionID: session.id }),
status: (sessionID: string) => this.sessionStatusMap.get(sessionID),
}
}
private async syncWebviewState(reason: string): Promise<void> {
const serverInfo = this.connectionService.getServerInfo()
console.log("[Kilo New] KiloProvider: 🔄 syncWebviewState()", {
@@ -529,21 +535,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.postMessage({ type: "openCloudSession", sessionId })
}
/** Register the handler for "Continue in Worktree" messages from the sidebar. */
public setContinueInWorktreeHandler(
handler: (sessionId: string, progress: (status: string, detail?: string, error?: string) => void) => Promise<void>,
): void {
this.continueInWorktreeHandler = handler
}
/**
* Attach to a webview that already has its own HTML set.
* Sets up message handling and connection without overriding HTML content.
*
* @param options.onBeforeMessage - Optional interceptor called before the standard handler.
* Return null to consume the message (stop propagation), or return the message
* (possibly transformed) to continue with standard handling.
*/
public attachToWebview(
webview: vscode.Webview,
options?: { onBeforeMessage?: (msg: Record<string, unknown>) => Promise<Record<string, unknown> | null> },
@@ -555,10 +552,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.initializeConnection()
}
/**
* Set up the shared message handler for both sidebar and tab webviews.
* Handles ALL message types so tabs have full functionality.
*/
private setupWebviewMessageHandler(webview: vscode.Webview): void {
this.webviewMessageDisposable?.dispose()
this.webviewMessageDisposable = webview.onDidReceiveMessage(async (message) => {
@@ -709,6 +702,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
post: (msg) => this.postMessage(msg),
})
break
case "forkSession":
handleForkSession(this.forkCtx, message.sessionId, message.messageId).catch((e) =>
console.error("[Kilo New] handleForkSession failed:", e),
)
break
case "retryConnection":
console.log("[Kilo New] KiloProvider: 🔄 Retrying connection...")
this.initializeConnection().catch((e) =>
@@ -1255,9 +1254,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return sessionToWebview(session)
}
/**
* Handle creating a new session.
*/
private async handleCreateSession(): Promise<void> {
if (!this.client) {
this.postMessage({
@@ -0,0 +1,38 @@
import type { Session, SessionStatus } from "@kilocode/sdk/v2/client"
import type { KiloConnectionService } from "../services/cli-backend"
import { forkSession } from "../agent-manager/fork-session"
export interface ForkContext {
connection: KiloConnectionService
post: (message: { type: "error"; message: string }) => void
register: (session: Session) => void
forked: (session: Session) => void
status: (sessionID: string) => SessionStatus["type"] | undefined
}
export async function handleForkSession(ctx: ForkContext, sessionId: string, messageId?: string): Promise<void> {
const status = ctx.status(sessionId)
if (status && status !== "idle") {
ctx.post({ type: "error", message: "Wait for the session to finish before forking it." })
return
}
await forkSession(
{
getClient: () => ctx.connection.getClient(),
state: undefined,
postError: (message) => ctx.post({ type: "error", message }),
registerWorktreeSession: () => {},
pushState: () => {},
notifyForked: (session) => {
ctx.register(session)
ctx.forked(session)
},
registerSession: () => {},
log: (...args) => console.log("[Kilo New] KiloProvider:", ...args),
},
sessionId,
undefined,
messageId,
)
}
+21 -1
View File
@@ -182,6 +182,12 @@ const AppContent: Component = () => {
if (agent) session.selectAgent(agent.name)
}
const handleForked = (message: { type?: string; sessionID?: string }) => {
if (message.type !== "sessionForked" || !message.sessionID) return
session.selectSession(message.sessionID)
setCurrentView("newTask")
}
onMount(() => {
const handler = (event: MessageEvent) => {
const message = event.data
@@ -199,6 +205,7 @@ const AppContent: Component = () => {
session.selectCloudSession(message.sessionId)
setCurrentView("newTask")
}
handleForked(message)
if (message?.type === "viewSubAgentSession" && message.sessionID) {
console.log("[Kilo New] App: 🔍 viewSubAgentSession:", message.sessionID)
session.setCurrentSessionID(message.sessionID)
@@ -219,17 +226,30 @@ const AppContent: Component = () => {
setCurrentView("newTask")
}
const handleForkMessage = (sessionId: string, messageId: string) => {
vscode.postMessage({ type: "forkSession", sessionId, messageId })
}
return (
<div class="container">
{/* legacy-migration start — state-driven overlay, independent of currentView */}
<Show
when={migrationNeeded()}
fallback={
<Switch fallback={<ChatView continueInWorktree promptBoxId="sidebar:fallback" />}>
<Switch
fallback={
<ChatView
continueInWorktree
onForkMessage={session.status() === "idle" ? handleForkMessage : undefined}
promptBoxId="sidebar:fallback"
/>
}
>
<Match when={currentView() === "newTask"}>
<ChatView
onSelectSession={handleSelectSession}
onShowHistory={() => setCurrentView("history")}
onForkMessage={session.status() === "idle" ? handleForkMessage : undefined}
continueInWorktree
promptBoxId="sidebar:new-task"
/>
@@ -573,6 +573,11 @@ export interface SessionCreatedMessage {
draftID?: string
}
export interface SessionForkedMessage {
type: "sessionForked"
sessionID: string
}
export interface SessionUpdatedMessage {
type: "sessionUpdated"
session: SessionInfo
@@ -1527,6 +1532,7 @@ export type ExtensionMessage =
| PermissionErrorMessage
| TodoUpdatedMessage
| SessionCreatedMessage
| SessionForkedMessage
| SessionUpdatedMessage
| SessionDeletedMessage
| MessageRemovedMessage
@@ -2057,6 +2063,12 @@ export interface ForkSessionRequest {
messageId?: string
}
export interface SidebarForkSessionRequest {
type: "forkSession"
sessionId: string
messageId?: string
}
// Close (remove) a session from its worktree
export interface CloseSessionRequest {
type: "agentManager.closeSession"
@@ -2556,6 +2568,7 @@ export type WebviewMessage =
| OpenLocallyRequest
| AddSessionToWorktreeRequest
| ForkSessionRequest
| SidebarForkSessionRequest
| CloseSessionRequest
| PersistSessionRequest
| ForgetSessionRequest