Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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/caffeination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Add a keep-awake toggle to the Agent Manager and Kilo extension commands.
4 changes: 4 additions & 0 deletions packages/kilo-ui/src/components/icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const icons: Record<string, { path: string; viewBox: string }> = {
viewBox: "0 0 24 24",
path: `<path d="M12 21V7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="m16 12 2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 6V4a1 1 0 0 0-1-1h-5a4 4 0 0 0-4 4 4 4 0 0 0-4-4H3a1 1 0 0 0-1 1v13a1 1 0 0 0 1 1h6a3 3 0 0 1 3 3 3 3 0 0 1 3-3h6a1 1 0 0 0 1-1v-1.3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`,
},
coffee: {
viewBox: "0 0 24 24",
path: `<path d="M17 8h1a4 4 0 1 1 0 8h-1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M6 2v2M10 2v2M14 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>`,
},
"circuit-board": {
viewBox: "0 0 16 16",
path: `<path d="M12.5 1H3.5C2.121 1 1 2.121 1 3.5V12.5C1 13.879 2.121 15 3.5 15H12.5C13.879 15 15 13.879 15 12.5V3.5C15 2.121 13.879 1 12.5 1ZM6 6.5C6 6.775 5.775 7 5.5 7C5.225 7 5 6.775 5 6.5C5 6.225 5.225 6 5.5 6C5.775 6 6 6.225 6 6.5ZM12.5 14H6V11.5C6 11.225 6.225 11 6.5 11H9.092C9.299 11.581 9.849 12 10.5 12C11.327 12 12 11.327 12 10.5C12 9.673 11.327 9 10.5 9C9.849 9 9.299 9.419 9.092 10H6.5C5.673 10 5 10.673 5 11.5V14H3.5C2.673 14 2 13.327 2 12.5V3.5C2 2.673 2.673 2 3.5 2H5V5.092C4.419 5.299 4 5.849 4 6.5C4 7.327 4.673 8 5.5 8C6.327 8 7 7.327 7 6.5C7 5.849 6.581 5.299 6 5.092V2H12.5C13.327 2 14 2.673 14 3.5V6H10.908C10.701 5.419 10.151 5 9.5 5C8.673 5 8 5.673 8 6.5C8 7.327 8.673 8 9.5 8C10.151 8 10.701 7.581 10.908 7H14V12.5C14 13.327 13.327 14 12.5 14ZM10 10.5C10 10.225 10.225 10 10.5 10C10.775 10 11 10.225 11 10.5C11 10.775 10.775 11 10.5 11C10.225 11 10 10.775 10 10.5ZM10 6.5C10 6.775 9.775 7 9.5 7C9.225 7 9 6.775 9 6.5C9 6.225 9.225 6 9.5 6C9.775 6 10 6.225 10 6.5Z" fill="currentColor"/>`,
Expand Down
5 changes: 5 additions & 0 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@
"title": "Toggle Project Memory",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.toggleCaffeination",
"title": "Toggle Keep Awake",
"category": "Kilo Code"
},
{
"command": "kilo-code.new.autocomplete.generateSuggestions",
"title": "Generate Suggested Edits",
Expand Down
19 changes: 9 additions & 10 deletions packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as fs from "fs"
import * as path from "path"
import type { KiloClient, Session } from "@kilocode/sdk/v2/client"
import type { KiloConnectionService } from "../services/cli-backend"
import type { CaffeinationService } from "../services/caffeination"
import { getErrorMessage, sessionToWebview } from "../kilo-provider-utils"
import { samePath } from "./project/paths"
import { resolveLocalDiffTarget } from "../diff/shared/target"
Expand Down Expand Up @@ -124,6 +125,7 @@ export class AgentManagerProvider implements Disposable {
constructor(
private readonly host: Host,
private readonly connectionService: KiloConnectionService,
private readonly caffeination: CaffeinationService,
binary: GitExecutable = () => Promise.resolve("git"),
) {
this.outputChannel = host.createOutput("Kilo Agent Manager")
Expand Down Expand Up @@ -155,6 +157,7 @@ export class AgentManagerProvider implements Disposable {
this.destination.sync(destination)
this.postToWebview({ type: "agentManager.terminal.destinationChanged", destination })
})
caffeination.onChange((state) => this.postToWebview({ type: "agentManager.caffeination", ...state }))
Comment thread
marius-kilocode marked this conversation as resolved.
Resolved
this.run = createRunController({
manager: this.scripts.manager,
root: () => this.getRoot(),
Expand Down Expand Up @@ -793,7 +796,11 @@ export class AgentManagerProvider implements Disposable {
this.state?.setSidebarCollapsed(m.collapsed)
return null
}
if (this.handleSection(m)) return null
if (m.type === "agentManager.setCaffeination") {
void this.caffeination.setEnabled(m.enabled)
return null
}
if (handleSection(this.state, m, this.pushState.bind(this), this.log.bind(this))) return null
if (m.type === "agentManager.setReviewDiffStyle") {
this.state?.setReviewDiffStyle(m.style)
return null
Expand Down Expand Up @@ -905,6 +912,7 @@ export class AgentManagerProvider implements Disposable {
// instance are reaped by the router's generation guard.
void this.terminalRouter.dispose()
this.scripts.manager.snapshot()
this.postToWebview({ type: "agentManager.caffeination", ...this.caffeination.getState() })
this.log(
`onRequestState: stateReady=${this.stateReady ? "pending" : "missing"}, state=${this.state ? "ok" : "missing"}`,
)
Expand Down Expand Up @@ -1846,15 +1854,6 @@ export class AgentManagerProvider implements Disposable {
queueMicrotask(() => this.postToWebview({ type: "action", action: "advancedWorktree" }))
}

private handleSection(m: AgentManagerInMessage): boolean {
return handleSection(
this.state,
m,
() => this.pushState(),
(...args) => this.log(...args),
)
}

public postMessage(message: unknown): void {
this.panel?.postMessage(message)
}
Expand Down
15 changes: 15 additions & 0 deletions packages/kilo-vscode/src/agent-manager/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,14 @@ interface RunStatusMessage extends RunStatus {
projectId?: string
}

interface CaffeinationMessage {
type: "agentManager.caffeination"
enabled: boolean
active: boolean
available: boolean
error?: string
}

/** All messages the Agent Manager extension sends to the webview. */
export type AgentManagerOutMessage =
| WorktreeStatsMessage
Expand Down Expand Up @@ -420,6 +428,7 @@ export type AgentManagerOutMessage =
| PRStatusOutMessage
| ActionOutMessage
| RunStatusMessage
| CaffeinationMessage
| TerminalCreatedMessage
| TerminalRestartedMessage
| TerminalClosedMessage
Expand Down Expand Up @@ -649,6 +658,11 @@ interface SetSidebarCollapsedIn {
collapsed: boolean
}

interface SetCaffeinationIn {
type: "agentManager.setCaffeination"
enabled: boolean
}

interface SetReviewDiffStyleIn {
type: "agentManager.setReviewDiffStyle"
style: "unified" | "split"
Expand Down Expand Up @@ -1016,6 +1030,7 @@ export type AgentManagerInMessage =
| SetWorktreeOrderIn
| SetSessionsCollapsedIn
| SetSidebarCollapsedIn
| SetCaffeinationIn
| SetReviewDiffStyleIn
| SetReviewMarkdownRenderIn
| SetDefaultBaseBranchIn
Expand Down
11 changes: 10 additions & 1 deletion packages/kilo-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { registerAutocompleteProvider } from "./services/autocomplete"
import { ensureBackendForAutocomplete } from "./services/autocomplete/ensure-backend"
import { AutocompleteServiceManager } from "./services/autocomplete/AutocompleteServiceManager"
import { AttentionService } from "./services/attention"
import { CaffeinationService } from "./services/caffeination"
import { BrowserAutomationService } from "./services/browser-automation"
import { TelemetryProxy } from "./services/telemetry"
import { registerCommitMessageService } from "./services/commit-message"
Expand All @@ -28,6 +29,7 @@ import { createNotebookBridge } from "./services/notebook"
import { createGitExecutable } from "./util/git-executable"

let agentManager: AgentManagerProvider | undefined
let caffeination: CaffeinationService | undefined
let shuttingDown = false

const RESTORE_KEY = "kilo.workbench.restore"
Expand Down Expand Up @@ -155,10 +157,11 @@ export function activate(context: vscode.ExtensionContext) {

// Create Agent Manager provider for editor panel
const agentManagerHost = new VscodeHost(context.extensionUri, connectionService, context, remoteService)
caffeination = new CaffeinationService()
const git = createGitExecutable({
log: (message) => console.warn(`[Kilo New] ${message}`),
})
const agentManagerProvider = new AgentManagerProvider(agentManagerHost, connectionService, git)
const agentManagerProvider = new AgentManagerProvider(agentManagerHost, connectionService, caffeination, git)
agentManagerProvider.onPanelVisibilityChange((visible) => remember({ agentManager: visible }))
agentManager = agentManagerProvider
context.subscriptions.push(agentManagerProvider)
Expand Down Expand Up @@ -401,6 +404,10 @@ export function activate(context: vscode.ExtensionContext) {
await target.waitForReady()
await target.toggleMemory()
}),
vscode.commands.registerCommand("kilo-code.new.toggleCaffeination", () => {
if (!caffeination) return
return caffeination.setEnabled(!caffeination.getState().enabled)
}),
// legacy-migration start
vscode.commands.registerCommand("kilo-code.new.openMigrationWizard", () => {
provider.postMessage({ type: "migrationState", needed: true, source: "legacy" })
Expand Down Expand Up @@ -568,6 +575,7 @@ export function activate(context: vscode.ExtensionContext) {
unsubscribeStateChange()
attention.dispose()
browserAutomationService.dispose()
void caffeination?.dispose()
provider.dispose()
notebookBridge.dispose()
connectionService.dispose()
Expand All @@ -578,6 +586,7 @@ export function activate(context: vscode.ExtensionContext) {
export async function deactivate() {
shuttingDown = true
await agentManager?.shutdown()
await caffeination?.dispose()
TelemetryProxy.getInstance().shutdown()
}

Expand Down
3 changes: 3 additions & 0 deletions packages/kilo-vscode/src/services/caffeination/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { CaffeinationService } from "./service"
export type { CaffeinationState } from "./service"
export type { CaffeinationDriver } from "./inhibitor"
195 changes: 195 additions & 0 deletions packages/kilo-vscode/src/services/caffeination/inhibitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { existsSync } from "node:fs"
import { delimiter, join } from "node:path"
import type { ChildProcess } from "node:child_process"
import { spawn } from "../../util/process"

export interface CaffeinationDriver {
readonly available: boolean
readonly reason?: string
start(parentPid: number, onExit: () => void): Promise<void>
stop(): Promise<void>
}

const STOP_TIMEOUT = 1_000

function locate(name: string): string | undefined {
const dirs = [
...(process.env.PATH ?? "").split(delimiter).filter(Boolean),
...(process.platform === "win32" ? [] : ["/usr/bin", "/bin"]),
]
return dirs.map((dir) => join(dir, name)).find((path) => existsSync(path))
}

function stopChild(child: ChildProcess): Promise<void> {
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve()

return new Promise((resolve) => {
let done = false
const kill = (signal: NodeJS.Signals) => {
try {
return child.kill(signal)
} catch (error) {
console.warn("[Kilo New] Failed to stop caffeination process:", error)
return false
}
}
const finish = () => {
if (done) return
done = true
clearTimeout(timer)
resolve()
}
const timer = setTimeout(() => {
if (child.exitCode === null && child.signalCode === null) kill("SIGKILL")
finish()
}, STOP_TIMEOUT)

child.once("exit", finish)
child.once("error", finish)
if (!kill("SIGTERM")) finish()
})
}

class ProcessDriver implements CaffeinationDriver {
private child: ChildProcess | undefined
private readonly stopping = new WeakSet<ChildProcess>()

constructor(
private readonly command: string,
private readonly args: (parentPid: number) => string[],
public readonly available: boolean,
public readonly reason?: string,
) {}

start(parentPid: number, onExit: () => void): Promise<void> {
if (this.child) return Promise.resolve()
if (!this.available) return Promise.reject(new Error(this.reason ?? "Caffeination is unavailable"))

const child = spawn(this.command, this.args(parentPid), { stdio: "ignore" })
this.child = child

return new Promise((resolve, reject) => {
let ready = false
let closed = false
let settled = false
const clear = () => {
if (this.child === child) this.child = undefined
}
const close = (error?: Error) => {
if (closed) return
closed = true
clear()
if (!ready) {
if (!settled) {
settled = true
reject(error ?? new Error("Caffeination process exited before starting"))
}
return
}
if (!this.stopping.has(child)) onExit()
}

child.once("spawn", () => {
ready = true
if (settled) return
settled = true
resolve()
})
child.once("error", (error) => close(error instanceof Error ? error : new Error(String(error))))
child.once("exit", (code, signal) => {
const detail = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`
close(new Error(`Caffeination process exited with ${detail}`))
})
})
}

async stop(): Promise<void> {
const child = this.child
if (!child) return
this.stopping.add(child)
this.child = undefined
await stopChild(child)
this.stopping.delete(child)
}
}

function powershell(parentPid: number): string {
return [
"$signature = '[DllImport(\"kernel32.dll\")] public static extern uint SetThreadExecutionState(uint flags);';",
"$type = Add-Type -MemberDefinition $signature -Name 'KiloCaffeination' -Namespace 'Kilo' -PassThru;",
"$flags = 0x80000000 -bor 0x00000001;",
"$result = $type::SetThreadExecutionState($flags);",
"if ($result -eq 0) { exit 1 };",
`$parent = ${parentPid};`,
"while (Get-Process -Id $parent -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 1 }",
].join(" ")
}

function mac(): CaffeinationDriver {
const command = "/usr/bin/caffeinate"
return new ProcessDriver(
command,
(parentPid) => ["-i", "-w", String(parentPid)],
existsSync(command),
`The ${command} command is not available`,
)
}

function linux(): CaffeinationDriver {
const command = locate("systemd-inhibit")
const shell = locate("sh")
return new ProcessDriver(
command ?? "systemd-inhibit",
(parentPid) => [
"--what=idle:sleep",
"--who=Kilo Code",
"--why=Kilo agent running",
"--mode=block",
shell ?? "/bin/sh",
"-c",
`while kill -0 ${parentPid} 2>/dev/null; do sleep 1; done`,
],
command !== undefined && shell !== undefined,
command === undefined ? "The systemd-inhibit command is not available" : "The sh command is not available",
)
}

function windows(): CaffeinationDriver {
const root = process.env.SystemRoot
const system = root ? join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe") : undefined
const command = system && existsSync(system) ? system : locate("powershell.exe")
return new ProcessDriver(
command ?? "powershell.exe",
(parentPid) => [
"-NoLogo",
"-NoProfile",
"-NonInteractive",
"-WindowStyle",
"Hidden",
"-Command",
powershell(parentPid),
],
command !== undefined,
"PowerShell is not available",
)
}

class UnsupportedDriver implements CaffeinationDriver {
readonly available = false
readonly reason = `Caffeination is not supported on ${process.platform}`

start(): Promise<void> {
return Promise.reject(new Error(this.reason))
}

stop(): Promise<void> {
return Promise.resolve()
}
}

export function createCaffeinationDriver(): CaffeinationDriver {
if (process.platform === "darwin") return mac()
if (process.platform === "linux") return linux()
if (process.platform === "win32") return windows()
return new UnsupportedDriver()
}
Loading
Loading