refactor(tui): extract kilo-specific logic into dedicated app module

Move Kilo TUI customizations (session effects, error handling,
command registration, permission helpers, and constants) from the
shared upstream app.tsx into a new kilocode/cli/cmd/tui/app.tsx
module. The upstream file now delegates to thin integration points,
keeping the fork diff minimal and improving separation of concerns.
This commit is contained in:
Imanol Maiztegui
2026-04-12 18:40:17 +02:00
parent 0512f116ed
commit 24087a6f9b
2 changed files with 191 additions and 87 deletions
+14 -87
View File
@@ -44,7 +44,6 @@ import { PromptHistoryProvider } from "./component/prompt/history"
import { FrecencyProvider } from "./component/prompt/frecency"
import { PromptStashProvider } from "./component/prompt/stash"
import { DialogAlert } from "./ui/dialog-alert"
import { isKiloError, showKiloErrorToast } from "@/kilocode/kilo-errors" // kilocode_change
import { DialogConfirm } from "./ui/dialog-confirm"
import { ToastProvider, useToast } from "./ui/toast"
import { ExitProvider, useExit } from "./context/exit"
@@ -58,24 +57,12 @@ import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open"
import { writeHeapSnapshot } from "v8"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { registerKiloCommands } from "@/kilocode/kilo-commands" // kilocode_change
import { KiloClawView } from "@/kilocode/claw/view" // kilocode_change
import { initializeTUIDependencies } from "@kilocode/kilo-gateway/tui" // kilocode_change
import * as KiloApp from "@/kilocode/cli/cmd/tui/app" // kilocode_change
import { TuiConfigProvider, useTuiConfig } from "./context/tui-config"
import { TuiConfig } from "@/config/tui"
import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin"
import { FormatError, FormatUnknownError } from "@/cli/error"
// kilocode_change start
function isAllowEverything(permission: unknown): boolean {
if (typeof permission !== "object" || permission === null) return false
const wildcard = (permission as Record<string, unknown>)["*"]
if (typeof wildcard === "string") return wildcard === "allow"
if (typeof wildcard === "object" && wildcard !== null) return (wildcard as Record<string, unknown>)["*"] === "allow"
return false
}
// kilocode_change end
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
// can't set raw mode if not a TTY
if (!process.stdin.isTTY) return "dark"
@@ -354,55 +341,38 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
}
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
// kilocode_change start — notify server which session the user is viewing (for live session indicators)
createEffect(() => {
const sessionID = route.data.type === "session" ? route.data.sessionID : undefined
sdk.client.session.viewed({ focused: sessionID ? [sessionID] : [] }).catch(() => {})
})
// kilocode_change end
// kilocode_change start — evict per-session data from store when navigating away
createEffect(
on(
() => (route.data.type === "session" ? route.data.sessionID : undefined),
(current, prev) => {
if (prev && prev !== current) sync.session.evict(prev)
},
),
)
// kilocode_change end
KiloApp.useSessionEffects({ route, sdk, sync }) // kilocode_change
// Update terminal window title based on current route and session
createEffect(() => {
if (!terminalTitleEnabled() || Flag.KILO_DISABLE_TERMINAL_TITLE) return
const titleDefault = "Kilo CLI" // kilocode_change
const titleDefault = KiloApp.APP_TITLE // kilocode_change
if (route.data.type === "home") {
renderer.setTerminalTitle(titleDefault) // kilocode_change
renderer.setTerminalTitle(titleDefault)
return
}
if (route.data.type === "session") {
const session = sync.session.get(route.data.sessionID)
if (!session || SessionApi.isDefaultTitle(session.title)) {
renderer.setTerminalTitle(titleDefault) // kilocode_change
renderer.setTerminalTitle(titleDefault)
return
}
const title = session.title.length > 40 ? session.title.slice(0, 37) + "..." : session.title
renderer.setTerminalTitle(`${titleDefault} | ${title}`) // kilocode_change
renderer.setTerminalTitle(`${titleDefault} | ${title}`)
return
}
if (route.data.type === "plugin") {
renderer.setTerminalTitle(`${titleDefault} | ${route.data.id}`) // kilocode_change
renderer.setTerminalTitle(`${titleDefault} | ${route.data.id}`)
}
// kilocode_change start
if (route.data.type === "kiloclaw") {
renderer.setTerminalTitle(`${titleDefault} | KiloClaw`)
}
const kiloTitle = KiloApp.getTerminalTitle(route, titleDefault)
if (kiloTitle) renderer.setTerminalTitle(kiloTitle)
// kilocode_change end
})
@@ -711,7 +681,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
title: "Open docs",
value: "docs.open",
onSelect: () => {
open("https://kilo.ai/docs").catch(() => {}) // kilocode_change
open(KiloApp.DOCS_URL).catch(() => {}) // kilocode_change
dialog.clear()
},
category: "System",
@@ -817,48 +787,10 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
dialog.clear()
},
},
// kilocode_change start
{
get title() {
return isAllowEverything(sync.data.config.permission) ? "Disable auto-approve mode" : "Enable auto-approve mode"
},
value: "permission.allow_everything",
category: "System",
onSelect: async (dialog) => {
const enabled = isAllowEverything(sync.data.config.permission)
const result = await sdk.client.permission.allowEverything({ enable: !enabled })
if (result.error) {
toast.show({
variant: "error",
message: `Failed to ${!enabled ? "enable" : "disable"} auto-approve mode`,
})
return
}
dialog.clear()
},
},
// kilocode_change end
])
// kilocode_change start - Initialize TUI dependencies for kilo-gateway
initializeTUIDependencies({
useCommandDialog: useCommandDialog,
useSync: useSync,
useDialog: useDialog,
useToast: useToast,
useTheme: useTheme,
useSDK: useSDK,
DialogAlert: DialogAlert,
DialogSelect: DialogSelect,
Link: Link,
Clipboard: Clipboard,
useKeyboard: useKeyboard,
TextAttributes: TextAttributes,
})
registerKiloCommands(useSDK)
// kilocode_change end
KiloApp.init() // kilocode_change
// kilocode_change - Delete OpenRouter Alert
sdk.event.on(TuiEvent.CommandExecute.type, (evt) => {
command.trigger(evt.properties.command)
})
@@ -892,12 +824,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
sdk.event.on("session.error", (evt) => {
const error = evt.properties.error
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
// kilocode_change start - Show warning toast for Kilo errors instead of generic error toast
if (error && typeof error === "object" && isKiloError(error)) {
showKiloErrorToast(error, toast)
return
}
// kilocode_change end
if (KiloApp.handleSessionError(error, toast)) return // kilocode_change
const message = errorMessage(error)
@@ -949,7 +876,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
await DialogAlert.show(
dialog,
"Update Complete",
`Successfully updated to Kilo v${result.data.version}. Please restart the application.`, // kilocode_change
`Successfully updated to ${KiloApp.APP_NAME} v${result.data.version}. Please restart the application.`, // kilocode_change
)
exit()
@@ -991,7 +918,7 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
</Match>
{/* kilocode_change start */}
<Match when={route.data.type === "kiloclaw"}>
<KiloClawView />
<KiloApp.KiloClawView />
</Match>
{/* kilocode_change end */}
</Switch>
@@ -0,0 +1,177 @@
// kilocode_change - new file
/**
* Kilo-specific TUI app customizations.
*
* Everything in this module is called from the shared upstream `app.tsx`
* via thin integration points so the upstream diff stays minimal.
*/
import { createEffect, on } from "solid-js"
import { useKeyboard } from "@opentui/solid"
import { TextAttributes } from "@opentui/core"
import { Clipboard } from "@tui/util/clipboard"
import { useCommandDialog } from "@tui/component/dialog-command"
import { useSDK } from "@tui/context/sdk"
import { useSync } from "@tui/context/sync"
import { useDialog } from "@tui/ui/dialog"
import { useToast } from "@tui/ui/toast"
import { useTheme } from "@tui/context/theme"
import { DialogAlert } from "@tui/ui/dialog-alert"
import { DialogSelect } from "@tui/ui/dialog-select"
import { Link } from "@tui/ui/link"
import { isKiloError, showKiloErrorToast } from "@/kilocode/kilo-errors"
import { registerKiloCommands } from "@/kilocode/kilo-commands"
import { initializeTUIDependencies } from "@kilocode/kilo-gateway/tui"
// Re-export so upstream can render the route without importing directly
export { KiloClawView } from "@/kilocode/claw/view"
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
/** Default terminal window title. */
export const APP_TITLE = "Kilo CLI"
/** Public docs URL shown in the command palette. */
export const DOCS_URL = "https://kilo.ai/docs"
/** Human-readable product name used in user-facing messages. */
export const APP_NAME = "Kilo"
// ---------------------------------------------------------------------------
// Utilities
// ---------------------------------------------------------------------------
export function isAllowEverything(permission: unknown): boolean {
if (typeof permission !== "object" || permission === null) return false
const wildcard = (permission as Record<string, unknown>)["*"]
if (typeof wildcard === "string") return wildcard === "allow"
if (typeof wildcard === "object" && wildcard !== null) return (wildcard as Record<string, unknown>)["*"] === "allow"
return false
}
// ---------------------------------------------------------------------------
// Session effects
// ---------------------------------------------------------------------------
/**
* Reactive effects for session management:
* - Notify the server which session the user is viewing (live indicators)
* - Evict per-session data from the store when navigating away
*
* Must be called inside the App component body (needs SolidJS owner).
*/
export function useSessionEffects(deps: {
route: ReturnType<typeof import("@tui/context/route").useRoute>
sdk: ReturnType<typeof useSDK>
sync: ReturnType<typeof useSync>
}) {
// Notify server which session the user is viewing
createEffect(() => {
const sessionID = deps.route.data.type === "session" ? deps.route.data.sessionID : undefined
deps.sdk.client.session.viewed({ focused: sessionID ? [sessionID] : [] }).catch(() => {})
})
// Evict per-session data from store when navigating away
createEffect(
on(
() => (deps.route.data.type === "session" ? deps.route.data.sessionID : undefined),
(current, prev) => {
if (prev && prev !== current) deps.sync.session.evict(prev)
},
),
)
}
// ---------------------------------------------------------------------------
// Terminal title
// ---------------------------------------------------------------------------
/**
* Returns the terminal title for kiloclaw routes.
* Returns undefined for other routes (caller should handle them).
*/
export function getTerminalTitle(
route: ReturnType<typeof import("@tui/context/route").useRoute>,
base: string,
): string | undefined {
if (route.data.type === "kiloclaw") return `${base} | KiloClaw`
return undefined
}
// ---------------------------------------------------------------------------
// Session error handling
// ---------------------------------------------------------------------------
/**
* Intercepts Kilo-specific errors and shows a warning toast.
* Returns `true` if the error was handled, `false` otherwise.
*/
export function handleSessionError(error: unknown, toast: ReturnType<typeof useToast>): boolean {
if (error && typeof error === "object" && isKiloError(error as any)) {
showKiloErrorToast(error as any, toast)
return true
}
return false
}
// ---------------------------------------------------------------------------
// Initialization
// ---------------------------------------------------------------------------
/**
* One-shot initialiser called from the App component body.
*
* - Injects TUI dependencies into kilo-gateway
* - Registers Kilo Gateway commands (profile, teams, kiloclaw, etc.)
* - Registers the auto-approve toggle command
*/
export function init() {
const command = useCommandDialog()
const sync = useSync()
const sdk = useSDK()
const toast = useToast()
// Inject TUI dependencies for kilo-gateway
initializeTUIDependencies({
useCommandDialog,
useSync,
useDialog,
useToast,
useTheme,
useSDK,
DialogAlert,
DialogSelect,
Link,
Clipboard,
useKeyboard,
TextAttributes,
})
// Register Kilo Gateway commands (profile, teams, kiloclaw, remote, etc.)
registerKiloCommands(useSDK)
// Register auto-approve toggle
command.register(() => [
{
get title() {
return isAllowEverything(sync.data.config.permission) ? "Disable auto-approve mode" : "Enable auto-approve mode"
},
value: "permission.allow_everything",
category: "System",
onSelect: async (dialog) => {
const enabled = isAllowEverything(sync.data.config.permission)
const result = await sdk.client.permission.allowEverything({ enable: !enabled })
if (result.error) {
toast.show({
variant: "error",
message: `Failed to ${!enabled ? "enable" : "disable"} auto-approve mode`,
})
return
}
dialog.clear()
},
},
])
}