Files
CodeNomad/packages/ui/src/lib/hooks/use-app-lifecycle.ts
Shantur Rathore d0a0325d7e feat(sidecars): add proxied sidecar tabs (#279)
## Summary
- add SideCar support across the server and UI, including proxied tabs,
picker/settings flows, and websocket-aware proxying
- unify top-level tab handling so workspace instances and SideCars share
the same tab model and navigation flows
- limit SideCars to port-based services only, removing server-managed
process control from the final API and UI

---------

Co-authored-by: Shantur <shantur@Mac.home>
Co-authored-by: Shantur <shantur@Shanturs-MacBook-Pro-M5.local>
2026-04-02 23:00:17 +01:00

145 lines
4.8 KiB
TypeScript

import { onMount, onCleanup, type Accessor } from "solid-js"
import { setupTabKeyboardShortcuts } from "../keyboard"
import { registerNavigationShortcuts } from "../shortcuts/navigation"
import { registerInputShortcuts } from "../shortcuts/input"
import { registerAgentShortcuts } from "../shortcuts/agent"
import { registerEscapeShortcut, setEscapeStateChangeHandler } from "../shortcuts/escape"
import { keyboardRegistry } from "../keyboard-registry"
import { abortSession, getSessions, isSessionBusy } from "../../stores/sessions"
import { showCommandPalette, hideCommandPalette } from "../../stores/command-palette"
import type { Instance } from "../../types/instance"
import { getLogger } from "../logger"
import { emitSessionSidebarRequest } from "../session-sidebar-events"
const log = getLogger("actions")
interface UseAppLifecycleOptions {
setEscapeInDebounce: (value: boolean) => void
handleNewInstanceRequest: () => void
handleCloseActiveTab: () => Promise<void>
handleCloseInstance: (instanceId: string) => Promise<void>
handleNewSession: (instanceId: string) => Promise<void>
handleCloseSession: (instanceId: string, sessionId: string) => Promise<void>
showFolderSelection: Accessor<boolean>
setShowFolderSelection: (value: boolean) => void
getActiveInstance: () => Instance | null
getActiveSessionIdForInstance: () => string | null
}
export function useAppLifecycle(options: UseAppLifecycleOptions) {
onMount(() => {
setEscapeStateChangeHandler(options.setEscapeInDebounce)
setupTabKeyboardShortcuts(
options.handleNewInstanceRequest,
options.handleCloseActiveTab,
options.handleNewSession,
options.handleCloseSession,
() => {
const instance = options.getActiveInstance()
if (instance) {
showCommandPalette(instance.id)
}
},
)
registerNavigationShortcuts()
registerInputShortcuts(
() => {
const textarea = document.querySelector(
".session-cache-pane[aria-hidden=\"false\"] .prompt-input",
) as HTMLTextAreaElement
if (textarea) textarea.value = ""
},
() => {
const textarea = document.querySelector(
".session-cache-pane[aria-hidden=\"false\"] .prompt-input",
) as HTMLTextAreaElement
textarea?.focus()
},
)
registerAgentShortcuts(
() => {
const instance = options.getActiveInstance()
if (!instance) return
emitSessionSidebarRequest({ instanceId: instance.id, action: "focus-model-selector" })
},
() => {
const instance = options.getActiveInstance()
if (!instance) return
emitSessionSidebarRequest({ instanceId: instance.id, action: "focus-agent-selector" })
},
() => {
const instance = options.getActiveInstance()
if (!instance) return
emitSessionSidebarRequest({ instanceId: instance.id, action: "focus-variant-selector" })
},
)
registerEscapeShortcut(
() => {
if (options.showFolderSelection()) return true
const instance = options.getActiveInstance()
if (!instance) return false
const sessionId = options.getActiveSessionIdForInstance()
if (!sessionId || sessionId === "info") return false
const sessions = getSessions(instance.id)
const session = sessions.find((s) => s.id === sessionId)
if (!session) return false
return isSessionBusy(instance.id, sessionId)
},
async () => {
if (options.showFolderSelection()) {
options.setShowFolderSelection(false)
return
}
const instance = options.getActiveInstance()
const sessionId = options.getActiveSessionIdForInstance()
if (!instance || !sessionId || sessionId === "info") return
try {
await abortSession(instance.id, sessionId)
log.info("Session aborted successfully", { instanceId: instance.id, sessionId })
} catch (error) {
log.error("Failed to abort session", error)
}
},
() => {
const active = document.activeElement as HTMLElement
active?.blur()
},
() => hideCommandPalette(),
)
const handleKeyDown = (e: KeyboardEvent) => {
const target = e.target as HTMLElement
const isInCombobox = target.closest('[role="combobox"]') !== null
const isInListbox = target.closest('[role="listbox"]') !== null
const isInAgentSelect = target.closest('[role="button"][data-agent-selector]') !== null
if (isInCombobox || isInListbox || isInAgentSelect) {
return
}
const shortcut = keyboardRegistry.findMatch(e)
if (shortcut) {
e.preventDefault()
shortcut.handler()
}
}
window.addEventListener("keydown", handleKeyDown)
onCleanup(() => {
window.removeEventListener("keydown", handleKeyDown)
})
})
}