Merge branch 'main' into docs/custom-provider-options

This commit is contained in:
Christiaan Arnoldus
2026-06-19 14:38:23 +02:00
committed by GitHub
6 changed files with 394 additions and 36 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@kilocode/cli": patch
---
Show the docs URL in an alert dialog when the browser cannot be opened on headless systems instead of silently failing.
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Keep chat output updating after reverting and resubmitting a prompt.
+99 -28
View File
@@ -172,6 +172,16 @@ const SESSION_SCOPED_PART_EVENTS = new Set(["message.part.updated", "message.par
const isSessionScopedPartEvent = (type: string) => SESSION_SCOPED_PART_EVENTS.has(type)
type SyncPayload = Extract<GlobalEvent["payload"], { type: "sync" }>
type RawSyncPayload = {
type: "sync"
syncEvent: {
type: SyncPayload["name"]
id: string
seq: number
aggregateID: string
data: unknown
}
}
type LegacySyncEvent =
| {
id: string
@@ -201,6 +211,7 @@ type LegacySyncEvent =
| {
source: "sync"
id: string
seq: number
type: "session.updated"
properties: Extract<SyncPayload, { name: "session.updated.1" }>["data"]
}
@@ -234,24 +245,35 @@ function isFullSessionUpdatedEvent(event: ProviderEvent): event is FullSessionUp
return event.type === "session.updated" && !isLegacySyncEvent(event)
}
function unwrapSyncEvent(event: GlobalEvent["payload"]): ProviderEvent | undefined {
export function unwrapSyncEvent(event: GlobalEvent["payload"] | RawSyncPayload): ProviderEvent | undefined {
if (event.type !== "sync") return event
const payload =
"syncEvent" in event
? ({
type: "sync",
name: event.syncEvent.type,
id: event.syncEvent.id,
seq: event.syncEvent.seq,
aggregateID: event.syncEvent.aggregateID,
data: event.syncEvent.data,
} as SyncPayload)
: event
switch (event.name) {
switch (payload.name) {
case "message.updated.1":
return { id: event.id, type: "message.updated", properties: event.data }
return { id: payload.id, type: "message.updated", properties: payload.data }
case "message.removed.1":
return { id: event.id, type: "message.removed", properties: event.data }
return { id: payload.id, type: "message.removed", properties: payload.data }
case "message.part.updated.1":
return { id: event.id, type: "message.part.updated", properties: event.data }
return { id: payload.id, type: "message.part.updated", properties: payload.data }
case "message.part.removed.1":
return { id: event.id, type: "message.part.removed", properties: event.data }
return { id: payload.id, type: "message.part.removed", properties: payload.data }
case "session.created.1":
return { id: event.id, type: "session.created", properties: event.data }
return { id: payload.id, type: "session.created", properties: payload.data }
case "session.updated.1":
return { source: "sync", id: event.id, type: "session.updated", properties: event.data }
return { source: "sync", id: payload.id, seq: payload.seq, type: "session.updated", properties: payload.data }
case "session.deleted.1":
return { id: event.id, type: "session.deleted", properties: event.data }
return { id: payload.id, type: "session.deleted", properties: payload.data }
default:
return undefined
}
@@ -308,6 +330,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
private promptRecovery: Promise<void> | null = null
private trackedSessionIds: Set<string> = new Set()
private syncedChildSessions: Set<string> = new Set()
private readonly checkpoints = new Map<string, Promise<void>>()
private readonly revisions = new Map<string, { id: string; seq: number }>()
private readonly refreshes = new Map<string, number>()
private sessionStatusMap = new Map<string, SessionStatus["type"]>() // Latest status used for destructive config warnings.
private sessionDirectories = new Map<string, string>() // Per-session directory overrides, such as Agent Manager worktrees.
private readonly aborts = new SessionAbort()
@@ -389,10 +414,27 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
}
private setCurrentSession(session: Session | null): void {
const ids = new Set([this.currentSession?.id, session?.id])
for (const id of ids) {
if (id) this.refreshes.set(id, (this.refreshes.get(id) ?? 0) + 1)
}
this.currentSession = session
this.opts.tabTitle?.(nativeTitle(session))
}
private checkpoint(sid: string, run: () => Promise<void>): void {
const prior = this.checkpoints.get(sid) ?? Promise.resolve()
const pending = prior.catch(() => undefined).then(run)
const cleanup = () => {
if (this.checkpoints.get(sid) === pending) this.checkpoints.delete(sid)
}
this.checkpoints.set(sid, pending)
void pending.then(cleanup, (error) => {
console.error("[Kilo New] checkpoint mutation failed:", error)
cleanup()
})
}
private stopCurrentSessionProcesses(next?: string): void {
const sid = this.contextSessionID ?? this.currentSession?.id
if (!sid || sid === next) return
@@ -546,6 +588,10 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
data: profileData,
})
if (this.currentSession) {
this.refreshSessionDetails(this.currentSession.id, this.getWorkspaceDirectory(this.currentSession.id))
}
// Re-send cached worktree stats and git status after webview reload.
if (this.cachedStats) this.postMessage(this.cachedStats)
this.postMessage({ type: "gitStatus", repo: this.cachedGitRepo })
@@ -853,14 +899,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await this.handleAbort(message.sessionID)
break
case "revertSession":
this.handleRevertSession(message.sessionID, message.messageID, message.partID).catch((e) =>
console.error("[Kilo New] handleRevertSession failed:", e),
this.checkpoint(message.sessionID, () =>
this.handleRevertSession(message.sessionID, message.messageID, message.partID),
)
break
case "unrevertSession":
this.handleUnrevertSession(message.sessionID).catch((e) =>
console.error("[Kilo New] handleUnrevertSession failed:", e),
)
this.checkpoint(message.sessionID, () => this.handleUnrevertSession(message.sessionID))
break
case "permissionResponse":
await handlePermissionResponse(
@@ -1506,13 +1550,24 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
/** Non-blocking: refresh session metadata + status for the webview after switching. */
private refreshSessionDetails(sessionID: string, dir: string, signal?: AbortSignal): void {
if (!this.client) return
const revision = this.revisions.get(sessionID)
const refresh = (this.refreshes.get(sessionID) ?? 0) + 1
this.refreshes.set(sessionID, refresh)
this.client.session
.get({ sessionID, directory: dir })
.then((r) => {
if (r.data && !signal?.aborted && this.contextSessionID === sessionID) {
this.setCurrentSession(r.data)
this.contextSessionID = r.data.id
if (!r.data || signal?.aborted || this.contextSessionID !== sessionID) return
if (this.refreshes.get(sessionID) !== refresh) {
if (this.revisions.get(sessionID) !== revision) this.refreshSessionDetails(sessionID, dir, signal)
return
}
if (this.revisions.get(sessionID) !== revision) {
this.refreshSessionDetails(sessionID, dir, signal)
return
}
this.setCurrentSession(r.data)
this.contextSessionID = r.data.id
this.postMessage({ type: "sessionUpdated", session: this.sessionToWebview(r.data) })
})
.catch((e: unknown) => console.warn("[Kilo New] KiloProvider: getSession failed (non-critical):", e))
this.postMessage({ type: "workspaceDirectoryChanged", directory: this.getWorkspaceDirectory(sessionID) })
@@ -1760,6 +1815,9 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.sessionDirectories.delete(sessionID)
this.aborts.delete(sessionID)
this.lastReconciledAt.delete(sessionID)
this.checkpoints.delete(sessionID)
this.revisions.delete(sessionID)
this.refreshes.delete(sessionID)
this.connectionService.pruneSession(sessionID)
if (this.currentSession?.id === sessionID) {
this.contextSessionID = undefined
@@ -2646,6 +2704,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.connectionService.recordMessageSessionId(messageID, sid)
}
await this.checkpoints.get(sid)
await runWithMessageConfirmation(this.confirmations, messageID, "KiloProvider: Message request", () =>
this.withRetry(
() =>
@@ -2724,6 +2783,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const sid = resolved!.sid
const dir = resolved!.dir
await this.checkpoints.get(sid)
await runWithMessageConfirmation(this.confirmations, messageID, "KiloProvider: Command request", () =>
this.withRetry(
() =>
@@ -2772,9 +2832,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (error) {
console.error("[Kilo New] KiloProvider: Failed to revert session:", error)
this.postMessage({ type: "error", message: "Failed to revert session", sessionID })
return
throw error
}
if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
if (!data) throw new Error("Revert returned no session")
this.refreshes.set(sessionID, (this.refreshes.get(sessionID) ?? 0) + 1)
if (this.currentSession?.id === sessionID) this.setCurrentSession(data)
this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
}
private async handleUnrevertSession(sessionID: string): Promise<void> {
@@ -2784,9 +2847,12 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
if (error) {
console.error("[Kilo New] KiloProvider: Failed to unrevert session:", error)
this.postMessage({ type: "error", message: "Failed to redo session", sessionID })
return
throw error
}
if (data) this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
if (!data) throw new Error("Redo returned no session")
this.refreshes.set(sessionID, (this.refreshes.get(sessionID) ?? 0) + 1)
if (this.currentSession?.id === sessionID) this.setCurrentSession(data)
this.postMessage({ type: "sessionUpdated", session: sessionToWebview(data) })
}
/**
@@ -3171,6 +3237,16 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
return
}
if (event.type === "session.updated") {
// Full bus snapshots duplicate sync patches with the same event ID but no sequence metadata.
if (isFullSessionUpdatedEvent(event)) return
const sid = event.properties.sessionID
const revision = this.revisions.get(sid)
const versioned = event.seq > 0 || (revision?.seq ?? 0) > 0
if (revision && (versioned ? event.seq <= revision.seq : event.id <= revision.id)) return
this.revisions.set(sid, { id: event.id, seq: event.seq })
}
// Refresh provider and agent lists when the server signals a state disposal
if (event.type === "global.disposed") {
void this.reloadAfterAuthChange()
@@ -3202,10 +3278,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
this.trackedSessionIds.add(event.properties.info.id)
}
if (event.type === "session.updated" && this.currentSession?.id === event.properties.sessionID) {
const session = isLegacySyncEvent(event)
? applySessionPatch(this.currentSession, event.properties.info)
: event.properties.info
this.setCurrentSession(session)
this.setCurrentSession(applySessionPatch(this.currentSession, event.properties.info))
this.contextSessionID = event.properties.sessionID
}
@@ -3248,9 +3321,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
const msg = isLegacySyncEvent(event)
? this.mapSyncEventToWebviewMessage(event)
: isFullSessionUpdatedEvent(event)
? { type: "sessionUpdated" as const, session: this.sessionToWebview(event.properties.info) }
: mapSSEEventToWebviewMessage(event, sessionID)
: mapSSEEventToWebviewMessage(event, sessionID)
if (!msg) return
if (msg.type === "partUpdated") {
this.streams.push({ ...msg, part: this.slimPart(msg.part) })
@@ -2,7 +2,7 @@ import { describe, it, expect, spyOn } from "bun:test"
import type { PartUpdate } from "../../src/shared/stream-messages"
// vscode mock is provided by the shared preload (tests/setup/vscode-mock.ts)
const { KiloProvider } = await import("../../src/KiloProvider")
const { KiloProvider, unwrapSyncEvent } = await import("../../src/KiloProvider")
type State = "connecting" | "connected" | "disconnected" | "error"
@@ -34,6 +34,21 @@ function mkMessage(id: string, role: "user" | "assistant", time = 0) {
}
}
function mkSession(revert?: { messageID: string }) {
return {
id: "s1",
slug: "session",
version: "1",
projectID: "project",
directory: "/repo",
title: "Session",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
time: { created: 1, updated: 1 },
revert,
}
}
function mkResult(items: unknown[]) {
return { data: items, response: { headers: new Headers() } }
}
@@ -42,6 +57,7 @@ function createClient(options?: {
messagesDeferred?: Deferred<{ data: unknown[]; response: { headers: Headers } }>
messagesData?: unknown[]
deleteDeferred?: Deferred<unknown>
revertDeferred?: Deferred<{ data?: unknown; error?: unknown }>
sessionData?: unknown
sessionGet?: (params: { sessionID: string; directory?: string }) => Promise<{ data: unknown }>
abortFailures?: string[]
@@ -49,10 +65,14 @@ function createClient(options?: {
const calls: { before?: string; limit?: number }[] = []
const stopped: { sessionID: string; directory?: string }[] = []
const aborted: { sessionID: string; directory?: string }[] = []
const prompted: Array<Record<string, unknown>> = []
const reverted: Array<Record<string, unknown>> = []
return {
calls,
stopped,
aborted,
prompted,
reverted,
session: {
list: async () => ({ data: [] }),
get: async (params: { sessionID: string; directory?: string }) => {
@@ -60,6 +80,15 @@ function createClient(options?: {
return { data: options?.sessionData ?? null }
},
status: async () => ({ data: {} }),
revert: async (params: Record<string, unknown>) => {
reverted.push(params)
if (options?.revertDeferred) return options.revertDeferred.promise
return { data: mkSession({ messageID: String(params.messageID) }) }
},
promptAsync: async (params: Record<string, unknown>) => {
prompted.push(params)
return { data: undefined }
},
abort: async (params: { sessionID: string; directory?: string }) => {
aborted.push(params)
if (params.directory && options?.abortFailures?.includes(params.directory)) throw new Error("abort failed")
@@ -120,14 +149,21 @@ function createConnection(client: ReturnType<typeof createClient>) {
type ProviderInternals = {
connectionState: State
webview: { postMessage: (message: unknown) => Promise<unknown> } | null
currentSession: { id: string; directory?: string } | null
currentSession: { id: string; directory?: string; revert?: { messageID: string } } | null
contextSessionID: string | undefined
sessionDirectories: Map<string, string>
trackedSessionIds: Set<string>
checkpoints: Map<string, Promise<void>>
revisions: Map<string, { id: string; seq: number }>
streams: { push: (msg: PartUpdate) => void }
checkpoint: (sid: string, run: () => Promise<void>) => void
gatherEditorContext: () => Promise<Record<string, never>>
refreshSessionDetails: (sid: string, dir: string) => void
stopCurrentSessionProcesses: (next?: string) => void
handleEvent: (event: unknown, directory?: string) => void
handleAbort: (sid?: string) => Promise<void>
handleRevertSession: (sid: string, messageID: string) => Promise<void>
handleSendMessage: (text: string, messageID?: string, sessionID?: string) => Promise<void>
handleLoadMessages: (sid: string, opts?: { mode?: string; before?: string; limit?: number }) => Promise<void>
handleDeleteSession: (sid: string) => Promise<void>
}
@@ -210,6 +246,240 @@ describe("KiloProvider.handleAbort", () => {
})
})
describe("KiloProvider revert ordering", () => {
it("unwraps the nested sync payload emitted by the live SSE endpoint", () => {
const event = unwrapSyncEvent({
type: "sync",
syncEvent: {
type: "session.updated.1",
id: "evt_clear",
seq: 0,
aggregateID: "sessionID",
data: { sessionID: "s1", info: { revert: null } },
},
})
expect(event).toEqual({
source: "sync",
id: "evt_clear",
seq: 0,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: null } },
})
})
it("waits for an in-flight revert before submitting the replacement prompt", async () => {
const revert = defer<{ data?: unknown; error?: unknown }>()
const client = createClient({ revertDeferred: revert })
const { internal } = makeProvider(client)
internal.currentSession = mkSession()
internal.gatherEditorContext = async () => ({})
internal.checkpoint("s1", () => internal.handleRevertSession("s1", "m1"))
const send = internal.handleSendMessage("replacement", "m2", "s1")
await Promise.resolve()
await Promise.resolve()
expect(client.reverted).toHaveLength(1)
expect(client.prompted).toHaveLength(0)
revert.resolve({ data: mkSession({ messageID: "m1" }) })
await send
expect(client.prompted).toHaveLength(1)
expect(client.prompted[0]?.sessionID).toBe("s1")
})
it("waits for a revert queued while the replacement prompt gathers context", async () => {
const context = defer<Record<string, never>>()
const revert = defer<{ data?: unknown; error?: unknown }>()
const client = createClient({ revertDeferred: revert })
const { internal } = makeProvider(client)
internal.currentSession = mkSession()
internal.gatherEditorContext = () => context.promise
const send = internal.handleSendMessage("replacement", "m2", "s1")
await Promise.resolve()
internal.checkpoint("s1", () => internal.handleRevertSession("s1", "m1"))
context.resolve({})
await Promise.resolve()
await Promise.resolve()
expect(client.prompted).toHaveLength(0)
revert.resolve({ data: mkSession({ messageID: "m1" }) })
await send
expect(client.prompted).toHaveLength(1)
})
it("does not submit the replacement prompt when the revert fails", async () => {
const error = spyOn(console, "error").mockImplementation(() => {})
const revert = defer<{ data?: unknown; error?: unknown }>()
const client = createClient({ revertDeferred: revert })
const { internal, sent } = makeProvider(client)
internal.currentSession = mkSession()
internal.gatherEditorContext = async () => ({})
internal.checkpoint("s1", () => internal.handleRevertSession("s1", "m1"))
const send = internal.handleSendMessage("replacement", "m2", "s1")
await Promise.resolve()
revert.resolve({ error: new Error("revert failed") })
await send
expect(client.prompted).toHaveLength(0)
expect(sent).toContainEqual(expect.objectContaining({ type: "sendMessageFailed", messageID: "m2" }))
error.mockRestore()
})
it("does not restore a stale revert boundary after a newer clear update", () => {
const client = createClient()
const { internal, sent } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.trackedSessionIds.add("s1")
internal.handleEvent({
source: "sync",
id: "evt_000000000002",
seq: 0,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: null } },
})
const count = sent.length
internal.handleEvent({
source: "sync",
id: "evt_000000000001",
seq: 0,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: { messageID: "m1" } } },
})
internal.handleEvent({
id: "evt_000000000001",
type: "session.updated",
properties: { sessionID: "s1", info: mkSession({ messageID: "m1" }) },
})
expect(internal.currentSession?.revert).toBeUndefined()
expect(internal.revisions.get("s1")).toEqual({ id: "evt_000000000002", seq: 0 })
expect(sent).toHaveLength(count)
expect(sent.at(-1)).toMatchObject({ type: "sessionUpdated", session: { id: "s1", revert: null } })
})
it("uses sequence ordering for workspace-replayed session updates", () => {
const client = createClient()
const { internal } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.trackedSessionIds.add("s1")
internal.handleEvent({
source: "sync",
id: "evt_ffffffffffff",
seq: 1,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: { messageID: "m1" } } },
})
internal.handleEvent({
source: "sync",
id: "evt_000000000001",
seq: 2,
type: "session.updated",
properties: { sessionID: "s1", info: { revert: null } },
})
expect(internal.currentSession?.revert).toBeUndefined()
expect(internal.revisions.get("s1")).toEqual({ id: "evt_000000000001", seq: 2 })
})
it("publishes authoritative session state after a missed clear event", async () => {
const client = createClient({ sessionData: mkSession() })
const { internal, sent } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.contextSessionID = "s1"
internal.refreshSessionDetails("s1", "/repo")
await Promise.resolve()
await Promise.resolve()
expect(internal.currentSession?.revert).toBeUndefined()
expect(sent.at(-1)).toMatchObject({ type: "sessionUpdated", session: { id: "s1", revert: null } })
})
it("retries a focused session refresh after a concurrent session update", async () => {
const first = defer<{ data: unknown }>()
const second = defer<{ data: unknown }>()
let calls = 0
const client = createClient({
sessionGet: async () => {
calls += 1
return calls === 1 ? first.promise : second.promise
},
})
const { internal } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.contextSessionID = "s1"
internal.trackedSessionIds.add("s1")
internal.refreshSessionDetails("s1", "/repo")
internal.handleEvent({
source: "sync",
id: "evt_000000000001",
seq: 0,
type: "session.updated",
properties: { sessionID: "s1", info: { title: "updated" } },
})
first.resolve({ data: mkSession() })
await Bun.sleep(0)
expect(calls).toBe(2)
second.resolve({ data: { ...mkSession(), title: "updated" } })
await Bun.sleep(0)
expect(internal.currentSession?.id).toBe("s1")
expect(internal.currentSession?.revert).toBeUndefined()
})
it("ignores an older session refresh that resolves last", async () => {
const first = defer<{ data: unknown }>()
const second = defer<{ data: unknown }>()
let calls = 0
const client = createClient({
sessionGet: async () => {
calls += 1
return calls === 1 ? first.promise : second.promise
},
})
const { internal, sent } = makeProvider(client)
internal.currentSession = mkSession({ messageID: "m1" })
internal.contextSessionID = "s1"
internal.refreshSessionDetails("s1", "/repo")
internal.refreshSessionDetails("s1", "/repo")
second.resolve({ data: mkSession() })
await Bun.sleep(0)
first.resolve({ data: mkSession({ messageID: "m1" }) })
await Bun.sleep(0)
expect(internal.currentSession?.revert).toBeUndefined()
expect(sent.filter((msg) => (msg as { type?: string }).type === "sessionUpdated")).toHaveLength(1)
})
it("ignores a session refresh superseded by a revert response", async () => {
const session = defer<{ data: unknown }>()
const client = createClient({ sessionGet: async () => session.promise })
const { internal } = makeProvider(client)
internal.currentSession = mkSession()
internal.contextSessionID = "s1"
internal.refreshSessionDetails("s1", "/repo")
await internal.handleRevertSession("s1", "m1")
session.resolve({ data: mkSession() })
await Bun.sleep(0)
expect(internal.currentSession?.revert).toEqual({ messageID: "m1" })
})
})
describe("KiloProvider.handleLoadMessages / focus mode freshness", () => {
it("stops background processes for the previous session when switching sessions", async () => {
const client = createClient({
@@ -40,16 +40,14 @@ describe("revert session synchronization", () => {
expect(unrevert).toContain('type: "sessionUpdated"')
})
it("distinguishes partial sync patches from full bus snapshots", () => {
it("uses ordered sync patches instead of duplicate bus snapshots", () => {
expect(provider).toMatch(/source: "sync"/)
expect(provider).toMatch(
/if \(event\.type === "session\.updated"\) return "source" in event && event\.source === "sync"/,
)
expect(provider).toMatch(/if \(isFullSessionUpdatedEvent\(event\)\) return/)
expect(provider).toMatch(
/isLegacySyncEvent\(event\)\s*\? applySessionPatch\(this\.currentSession, event\.properties\.info\)\s*:\s*event\.properties\.info/,
)
expect(provider).toMatch(
/isFullSessionUpdatedEvent\(event\)\s*\? \{ type: "sessionUpdated" as const, session: this\.sessionToWebview\(event\.properties\.info\) \}/,
/this\.setCurrentSession\(applySessionPatch\(this\.currentSession, event\.properties\.info\)\)/,
)
})
})
+10 -1
View File
@@ -703,8 +703,17 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
name: "docs.open",
title: "Open docs",
run: () => {
open(KiloApp.DOCS_URL).catch(() => {}) // kilocode_change
dialog.clear()
open(KiloApp.DOCS_URL).catch(() => {
// kilocode_change start
dialog.replace(() => (
<DialogAlert
title="Cannot open browser"
message={`No display detected.\n\nOpen the docs manually:\n${KiloApp.DOCS_URL}`}
/>
))
// kilocode_change end
})
},
category: "System",
},