mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
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:
@@ -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()
|
||||
},
|
||||
},
|
||||
])
|
||||
}
|
||||
Reference in New Issue
Block a user