feat(settings): implement Notifications tab with globalState-backed client-side settings

This commit is contained in:
Mark IJbema
2026-02-12 15:12:47 +01:00
parent 0c361bb2a4
commit 63feb13cc4
4 changed files with 246 additions and 19 deletions
+39 -1
View File
@@ -16,6 +16,8 @@ export class KiloProvider implements vscode.WebviewViewProvider {
/** Cached configLoaded payload so requestConfig can be served before httpClient is ready */
private cachedConfigMessage: unknown = null
private readonly globalState: vscode.Memento
private trackedSessionIds: Set<string> = new Set()
private unsubscribeEvent: (() => void) | null = null
private unsubscribeState: (() => void) | null = null
@@ -24,7 +26,10 @@ export class KiloProvider implements vscode.WebviewViewProvider {
constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
) {}
globalState: vscode.Memento,
) {
this.globalState = globalState
}
/**
* Convenience getter that returns the shared HttpClient or null if not yet connected.
@@ -234,6 +239,12 @@ export class KiloProvider implements vscode.WebviewViewProvider {
case "requestBrowserSettings":
this.sendBrowserSettings()
break
case "requestLocalSettings":
await this.handleRequestLocalSettings()
break
case "saveLocalSetting":
await this.handleSaveLocalSetting(message.key, message.value)
break
}
})
}
@@ -316,6 +327,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
await this.fetchAndSendProviders()
await this.fetchAndSendAgents()
await this.fetchAndSendConfig()
await this.handleRequestLocalSettings()
console.log("[Kilo New] KiloProvider: ✅ initializeConnection completed successfully")
} catch (error) {
@@ -623,6 +635,32 @@ export class KiloProvider implements vscode.WebviewViewProvider {
}
}
/**
* Send all local settings to the webview.
*/
private async handleRequestLocalSettings(): Promise<void> {
const keys = [
"notifications.agent",
"notifications.permissions",
"notifications.errors",
"sounds.agent",
"sounds.permissions",
"sounds.errors",
]
const settings: Record<string, unknown> = {}
for (const key of keys) {
settings[key] = this.globalState.get(key)
}
this.postMessage({ type: "localSettingsLoaded", settings })
}
/**
* Save a local setting to globalState.
*/
private async handleSaveLocalSetting(key: string, value: unknown): Promise<void> {
await this.globalState.update(key, value)
}
/**
* Handle config update request from the webview.
* Applies a partial config update, then re-fetches and pushes the full config.
+2 -2
View File
@@ -23,7 +23,7 @@ export function activate(context: vscode.ExtensionContext) {
})
// Create the provider with shared service
const provider = new KiloProvider(context.extensionUri, connectionService)
const provider = new KiloProvider(context.extensionUri, connectionService, context.globalState)
// Register the webview view provider for the sidebar
context.subscriptions.push(vscode.window.registerWebviewViewProvider(KiloProvider.viewType, provider))
@@ -91,7 +91,7 @@ async function openKiloInNewTab(context: vscode.ExtensionContext, connectionServ
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "kilo-dark.svg"),
}
const tabProvider = new KiloProvider(context.extensionUri, connectionService)
const tabProvider = new KiloProvider(context.extensionUri, connectionService, context.globalState)
tabProvider.resolveWebviewPanel(panel)
// Wait for the new panel to become active before locking the editor group.
@@ -1,29 +1,200 @@
import { Component } from "solid-js"
import { Component, createSignal, onCleanup } from "solid-js"
import { useVSCode } from "../../context/vscode"
import type { ExtensionMessage } from "../../types/messages"
const NotificationsTab: Component = () => {
const vscode = useVSCode()
const [agentNotify, setAgentNotify] = createSignal(true)
const [permNotify, setPermNotify] = createSignal(true)
const [errorNotify, setErrorNotify] = createSignal(true)
const [agentSound, setAgentSound] = createSignal("default")
const [permSound, setPermSound] = createSignal("default")
const [errorSound, setErrorSound] = createSignal("default")
// Listen for local settings loaded
const unsubscribe = vscode.onMessage((message: ExtensionMessage) => {
if (message.type !== "localSettingsLoaded") {
return
}
const s = message.settings
if (s["notifications.agent"] !== undefined) {
setAgentNotify(s["notifications.agent"] as boolean)
}
if (s["notifications.permissions"] !== undefined) {
setPermNotify(s["notifications.permissions"] as boolean)
}
if (s["notifications.errors"] !== undefined) {
setErrorNotify(s["notifications.errors"] as boolean)
}
if (s["sounds.agent"] !== undefined) {
setAgentSound(s["sounds.agent"] as string)
}
if (s["sounds.permissions"] !== undefined) {
setPermSound(s["sounds.permissions"] as string)
}
if (s["sounds.errors"] !== undefined) {
setErrorSound(s["sounds.errors"] as string)
}
})
onCleanup(unsubscribe)
// Request settings
vscode.postMessage({ type: "requestLocalSettings" })
const saveSetting = (key: string, value: unknown) => {
vscode.postMessage({ type: "saveLocalSetting", key, value })
}
const selectStyle = {
padding: "4px 8px",
"border-radius": "4px",
border: "1px solid var(--vscode-dropdown-border, var(--vscode-panel-border))",
background: "var(--vscode-dropdown-background)",
color: "var(--vscode-dropdown-foreground)",
"font-size": "12px",
"font-family": "var(--vscode-font-family)",
cursor: "pointer",
outline: "none",
"min-width": "100px",
}
interface RowProps {
label: string
description: string
last?: boolean
children: any
}
const Row: Component<RowProps> = (props) => (
<div
style={{
display: "flex",
"align-items": "center",
"justify-content": "space-between",
padding: "10px 12px",
background: "var(--vscode-editor-background)",
"border-bottom": props.last ? "none" : "1px solid var(--vscode-panel-border)",
}}
>
<div style={{ flex: 1, "min-width": 0, "margin-right": "12px" }}>
<div style={{ "font-size": "12px", "font-weight": "500", color: "var(--vscode-foreground)" }}>
{props.label}
</div>
<div style={{ "font-size": "11px", color: "var(--vscode-descriptionForeground)", "margin-top": "2px" }}>
{props.description}
</div>
</div>
{props.children}
</div>
)
return (
<div>
{/* Notification toggles */}
<h4 style={{ "font-size": "13px", "margin-top": "0", "margin-bottom": "8px", color: "var(--vscode-foreground)" }}>
Notifications
</h4>
<div
style={{
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
padding: "16px",
overflow: "hidden",
"margin-bottom": "16px",
}}
>
<p
style={{
"font-size": "12px",
color: "var(--vscode-descriptionForeground)",
margin: 0,
"line-height": "1.5",
}}
>
<strong style={{ color: "var(--vscode-foreground)" }}>This section is not implemented yet.</strong> It will
contain configuration options and explanatory text related to the selected settings category. During
reimplementation, use this space to validate layout, spacing, scrolling behavior, and navigation state before
wiring up real controls.
</p>
<Row label="Agent Completion" description="Show notification when agent completes a task">
<label style={{ display: "flex", "align-items": "center", cursor: "pointer" }}>
<input
type="checkbox"
checked={agentNotify()}
onChange={(e) => {
setAgentNotify(e.currentTarget.checked)
saveSetting("notifications.agent", e.currentTarget.checked)
}}
style={{ cursor: "pointer" }}
/>
</label>
</Row>
<Row label="Permission Requests" description="Show notification on permission requests">
<label style={{ display: "flex", "align-items": "center", cursor: "pointer" }}>
<input
type="checkbox"
checked={permNotify()}
onChange={(e) => {
setPermNotify(e.currentTarget.checked)
saveSetting("notifications.permissions", e.currentTarget.checked)
}}
style={{ cursor: "pointer" }}
/>
</label>
</Row>
<Row label="Errors" description="Show notification on errors" last>
<label style={{ display: "flex", "align-items": "center", cursor: "pointer" }}>
<input
type="checkbox"
checked={errorNotify()}
onChange={(e) => {
setErrorNotify(e.currentTarget.checked)
saveSetting("notifications.errors", e.currentTarget.checked)
}}
style={{ cursor: "pointer" }}
/>
</label>
</Row>
</div>
{/* Sound settings */}
<h4 style={{ "font-size": "13px", "margin-top": "0", "margin-bottom": "8px", color: "var(--vscode-foreground)" }}>
Sounds
</h4>
<div
style={{
border: "1px solid var(--vscode-panel-border)",
"border-radius": "4px",
overflow: "hidden",
}}
>
<Row label="Agent Completion Sound" description="Sound to play when agent completes">
<select
style={selectStyle}
value={agentSound()}
onChange={(e) => {
setAgentSound(e.currentTarget.value)
saveSetting("sounds.agent", e.currentTarget.value)
}}
>
<option value="default">Default</option>
<option value="none">None</option>
</select>
</Row>
<Row label="Permission Request Sound" description="Sound to play on permission requests">
<select
style={selectStyle}
value={permSound()}
onChange={(e) => {
setPermSound(e.currentTarget.value)
saveSetting("sounds.permissions", e.currentTarget.value)
}}
>
<option value="default">Default</option>
<option value="none">None</option>
</select>
</Row>
<Row label="Error Sound" description="Sound to play on errors" last>
<select
style={selectStyle}
value={errorSound()}
onChange={(e) => {
setErrorSound(e.currentTarget.value)
saveSetting("sounds.errors", e.currentTarget.value)
}}
>
<option value="default">Default</option>
<option value="none">None</option>
</select>
</Row>
</div>
</div>
)
@@ -450,6 +450,11 @@ export interface ConfigUpdatedMessage {
config: Config
}
export interface LocalSettingsLoadedMessage {
type: "localSettingsLoaded"
settings: Record<string, unknown>
}
export type ExtensionMessage =
| ReadyMessage
| ConnectionStateMessage
@@ -478,6 +483,7 @@ export type ExtensionMessage =
| BrowserSettingsLoadedMessage
| ConfigLoadedMessage
| ConfigUpdatedMessage
| LocalSettingsLoadedMessage
// ============================================
// Messages FROM webview TO extension
@@ -607,6 +613,16 @@ export interface UpdateConfigMessage {
config: Partial<Config>
}
export interface SaveLocalSettingMessage {
type: "saveLocalSetting"
key: string
value: unknown
}
export interface RequestLocalSettingsMessage {
type: "requestLocalSettings"
}
export type WebviewMessage =
| SendMessageRequest
| AbortRequest
@@ -633,6 +649,8 @@ export type WebviewMessage =
| RequestBrowserSettingsMessage
| RequestConfigMessage
| UpdateConfigMessage
| SaveLocalSettingMessage
| RequestLocalSettingsMessage
// ============================================
// VS Code API type