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>
This commit is contained in:
Shantur Rathore
2026-04-02 23:00:17 +01:00
committed by GitHub
parent 19a4c3df16
commit d0a0325d7e
47 changed files with 2139 additions and 218 deletions

View File

@@ -10,6 +10,7 @@ import type {
SpeechCapabilitiesResponse,
SpeechSynthesisResponse,
SpeechTranscriptionResponse,
SideCar,
ServerMeta,
RemoteServerProbeRequest,
RemoteServerProbeResponse,
@@ -193,6 +194,33 @@ export const serverApi = {
body: JSON.stringify(payload),
})
},
fetchSidecars(): Promise<{ sidecars: SideCar[] }> {
return request<{ sidecars: SideCar[] }>("/api/sidecars")
},
createSidecar(payload: {
kind: "port"
name: string
port: number
insecure: boolean
prefixMode: "strip" | "preserve"
}): Promise<SideCar> {
return request<SideCar>("/api/sidecars", {
method: "POST",
body: JSON.stringify(payload),
})
},
updateSidecar(
id: string,
payload: Partial<{ name: string; port: number; insecure: boolean; prefixMode: "strip" | "preserve" }>,
): Promise<SideCar> {
return request<SideCar>(`/api/sidecars/${encodeURIComponent(id)}`, {
method: "PUT",
body: JSON.stringify(payload),
})
},
deleteSidecar(id: string): Promise<void> {
return request(`/api/sidecars/${encodeURIComponent(id)}`, { method: "DELETE" })
},
fetchServerMeta(): Promise<ServerMeta> {
return request<ServerMeta>("/api/meta")
},
@@ -438,4 +466,4 @@ function buildClientEventsUrl(identity: { clientId: string; connectionId: string
return `${url.pathname}${url.search}`
}
export type { WorkspaceDescriptor, WorkspaceLogEntry, WorkspaceEventPayload, WorkspaceEventType }
export type { WorkspaceDescriptor, WorkspaceLogEntry, WorkspaceEventPayload, WorkspaceEventType, SideCar }

View File

@@ -16,6 +16,7 @@ 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>
@@ -31,7 +32,7 @@ export function useAppLifecycle(options: UseAppLifecycleOptions) {
setupTabKeyboardShortcuts(
options.handleNewInstanceRequest,
options.handleCloseInstance,
options.handleCloseActiveTab,
options.handleNewSession,
options.handleCloseSession,
() => {

View File

@@ -2,7 +2,8 @@ import { createSignal, onMount } from "solid-js"
import type { Accessor } from "solid-js"
import type { Preferences, ExpansionPreference, ToolInputsVisibilityPreference } from "../../stores/preferences"
import { createCommandRegistry, type Command } from "../commands"
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
import { activeInstanceId } from "../../stores/instances"
import { selectNextAppTab, selectPreviousAppTab } from "../../stores/app-tabs"
import type { ClientPart, MessageInfo } from "../../types/message"
import { getSessions, getVisibleSessionIds, setActiveSession, setActiveSessionFromList } from "../../stores/sessions"
import { showAlertDialog } from "../../stores/alerts"
@@ -41,6 +42,7 @@ export interface UseCommandsOptions {
setThinkingBlocksExpansion: (mode: ExpansionPreference) => void
setToolInputsVisibility: (mode: ToolInputsVisibilityPreference) => void
handleNewInstanceRequest: () => void
handleCloseActiveTab: () => Promise<void>
handleCloseInstance: (instanceId: string) => Promise<void>
handleNewSession: (instanceId: string) => Promise<void>
handleCloseSession: (instanceId: string, sessionId: string) => Promise<void>
@@ -90,9 +92,7 @@ export function useCommands(options: UseCommandsOptions) {
keywords: () => splitKeywords("commands.closeInstance.keywords"),
shortcut: { key: "W", meta: true },
action: async () => {
const instance = activeInstance()
if (!instance) return
await options.handleCloseInstance(instance.id)
await options.handleCloseActiveTab()
},
})
@@ -103,13 +103,7 @@ export function useCommands(options: UseCommandsOptions) {
category: "Instance",
keywords: () => splitKeywords("commands.nextInstance.keywords"),
shortcut: { key: "]", meta: true },
action: () => {
const ids = Array.from(instances().keys())
if (ids.length <= 1) return
const current = ids.indexOf(activeInstanceId() || "")
const next = (current + 1) % ids.length
if (ids[next]) setActiveInstanceId(ids[next])
},
action: () => selectNextAppTab(),
})
commandRegistry.register({
@@ -119,13 +113,7 @@ export function useCommands(options: UseCommandsOptions) {
category: "Instance",
keywords: () => splitKeywords("commands.previousInstance.keywords"),
shortcut: { key: "[", meta: true },
action: () => {
const ids = Array.from(instances().keys())
if (ids.length <= 1) return
const current = ids.indexOf(activeInstanceId() || "")
const prev = current <= 0 ? ids.length - 1 : current - 1
if (ids[prev]) setActiveInstanceId(ids[prev])
},
action: () => selectPreviousAppTab(),
})
commandRegistry.register({

View File

@@ -15,17 +15,17 @@ export const commandMessages = {
"commands.newInstance.description": "Open folder picker to create new instance",
"commands.newInstance.keywords": "folder, project, workspace",
"commands.closeInstance.label": "Close Instance",
"commands.closeInstance.description": "Stop current instance's server",
"commands.closeInstance.keywords": "stop, quit, close",
"commands.closeInstance.label": "Close Tab",
"commands.closeInstance.description": "Close the current top-level tab",
"commands.closeInstance.keywords": "stop, quit, close, tab",
"commands.nextInstance.label": "Next Instance",
"commands.nextInstance.description": "Cycle to next instance tab",
"commands.nextInstance.keywords": "switch, navigate",
"commands.nextInstance.label": "Next Tab",
"commands.nextInstance.description": "Cycle to the next top-level tab",
"commands.nextInstance.keywords": "switch, navigate, tab",
"commands.previousInstance.label": "Previous Instance",
"commands.previousInstance.description": "Cycle to previous instance tab",
"commands.previousInstance.keywords": "switch, navigate",
"commands.previousInstance.label": "Previous Tab",
"commands.previousInstance.description": "Cycle to the previous top-level tab",
"commands.previousInstance.keywords": "switch, navigate, tab",
"commands.newSession.label": "New Session",
"commands.newSession.description": "Create a new parent session",

View File

@@ -69,4 +69,5 @@ export const folderSelectionMessages = {
"folderSelection.servers.dialog.connecting": "Connecting...",
"folderSelection.servers.dialog.errorRequired": "Server name and URL are required.",
"folderSelection.servers.dialog.errorConnect": "Could not connect to the remote server.",
"folderSelection.sidecars.button": "Open SideCar",
} as const

View File

@@ -195,4 +195,40 @@ export const settingsMessages = {
"settings.speech.save.saved": "Saved",
"settings.speech.save.unsaved": "Unsaved changes",
"settings.speech.save.error": "Save failed",
"settings.nav.sidecars": "SideCars",
"settings.section.sidecars.eyebrow": "Server services",
"settings.section.sidecars.title": "SideCars",
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
"sidecars.form.name": "Name",
"sidecars.form.validation": "Enter a valid SideCar name and port.",
"sidecars.form.port": "Port",
"sidecars.form.insecure": "Use HTTP",
"sidecars.form.protocol": "Protocol",
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
"sidecars.form.protocol.https": "HTTPS",
"sidecars.form.protocol.http": "HTTP",
"sidecars.form.prefixMode": "Prefix mode",
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
"sidecars.form.prefixMode.strip": "Strip prefix",
"sidecars.form.prefixMode.preserve": "Preserve prefix",
"sidecars.form.add": "Add SideCar",
"sidecars.kind.port": "Port",
"sidecars.status.running": "Running",
"sidecars.status.stopped": "Stopped",
"sidecars.basePath": "Base path",
"sidecars.settings.listTitle": "Configured SideCars",
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
"sidecars.settings.empty": "No SideCars configured yet.",
"sidecars.picker.title": "Open SideCar",
"sidecars.picker.loading": "Loading SideCars...",
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
"sidecars.picker.empty": "No port-based SideCars are available yet.",
"sidecars.picker.close": "Close",
"sidecars.open.errorTitle": "Unable to open SideCar",
"sidecars.open.notFound": "SideCar not found.",
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
"sidecars.back": "Back",
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
} as const

View File

@@ -15,17 +15,17 @@ export const commandMessages = {
"commands.newInstance.description": "Abrir el selector de carpetas para crear una nueva instancia",
"commands.newInstance.keywords": "carpeta, proyecto, workspace",
"commands.closeInstance.label": "Cerrar instancia",
"commands.closeInstance.description": "Detener el servidor de la instancia actual",
"commands.closeInstance.keywords": "detener, salir, cerrar",
"commands.closeInstance.label": "Cerrar pestaña",
"commands.closeInstance.description": "Cerrar la pestaña superior actual",
"commands.closeInstance.keywords": "detener, salir, cerrar, pestaña",
"commands.nextInstance.label": "Siguiente instancia",
"commands.nextInstance.description": "Cambiar a la siguiente pestaña de instancia",
"commands.nextInstance.keywords": "cambiar, navegar",
"commands.nextInstance.label": "Siguiente pestaña",
"commands.nextInstance.description": "Cambiar a la siguiente pestaña superior",
"commands.nextInstance.keywords": "cambiar, navegar, pestaña",
"commands.previousInstance.label": "Instancia anterior",
"commands.previousInstance.description": "Cambiar a la pestaña de instancia anterior",
"commands.previousInstance.keywords": "cambiar, navegar",
"commands.previousInstance.label": "Pestaña anterior",
"commands.previousInstance.description": "Cambiar a la pestaña superior anterior",
"commands.previousInstance.keywords": "cambiar, navegar, pestaña",
"commands.newSession.label": "Nueva sesión",
"commands.newSession.description": "Crear una nueva sesión principal",

View File

@@ -2,23 +2,23 @@ export const folderSelectionMessages = {
"folderSelection.language.ariaLabel": "Idioma",
"folderSelection.logoAlt": "Logo de CodeNomad",
"folderSelection.tagline": "Selecciona una carpeta para empezar a programar con IA",
"folderSelection.tagline": "Selecciona una carpeta para empezar a programar con AI",
"folderSelection.links.github": "GitHub de CodeNomad",
"folderSelection.links.githubStars": "Estrellas de CodeNomad en GitHub",
"folderSelection.links.githubStars": "Estrellas de GitHub de CodeNomad",
"folderSelection.links.discord": "Discord de CodeNomad",
"folderSelection.empty.title": "No hay carpetas recientes",
"folderSelection.empty.description": "Explora una carpeta para comenzar",
"folderSelection.empty.description": "Busca una carpeta para comenzar",
"folderSelection.recent.title": "Carpetas recientes",
"folderSelection.recent.subtitle.one": "{count} carpeta disponible",
"folderSelection.recent.subtitle.other": "{count} carpetas disponibles",
"folderSelection.recent.remove": "Quitar de recientes",
"folderSelection.recent.remove": "Eliminar de recientes",
"folderSelection.browse.title": "Explorar carpetas",
"folderSelection.browse.title": "Buscar carpeta",
"folderSelection.browse.subtitle": "Selecciona cualquier carpeta en tu ordenador",
"folderSelection.browse.button": "Explorar carpetas",
"folderSelection.browse.button": "Buscar carpetas",
"folderSelection.browse.buttonOpening": "Abriendo...",
"folderSelection.actions.title": "Abrir carpeta o conectar servidor",
"folderSelection.actions.subtitle": "Abre una carpeta local o conéctate a un servidor de CodeNomad",
@@ -29,11 +29,11 @@ export const folderSelectionMessages = {
"folderSelection.hints.navigate": "Navegar",
"folderSelection.hints.select": "Seleccionar",
"folderSelection.hints.remove": "Quitar",
"folderSelection.hints.browse": "Explorar",
"folderSelection.hints.remove": "Eliminar",
"folderSelection.hints.browse": "Buscar",
"folderSelection.loading.title": "Iniciando instancia...",
"folderSelection.loading.subtitle": "Espera un momento mientras preparamos tu workspace.",
"folderSelection.loading.subtitle": "Espera mientras preparamos tu espacio de trabajo.",
"folderSelection.drop.title": "Suelta una carpeta para abrirla",
"folderSelection.drop.subtitle": "Inicia una nueva instancia en la carpeta soltada.",
@@ -69,4 +69,5 @@ export const folderSelectionMessages = {
"folderSelection.servers.dialog.connecting": "Conectando...",
"folderSelection.servers.dialog.errorRequired": "El nombre y la URL del servidor son obligatorios.",
"folderSelection.servers.dialog.errorConnect": "No se pudo conectar al servidor remoto.",
"folderSelection.sidecars.button": "Open SideCar",
} as const

View File

@@ -194,4 +194,40 @@ export const settingsMessages = {
"settings.speech.save.saved": "Guardado",
"settings.speech.save.unsaved": "Cambios sin guardar",
"settings.speech.save.error": "Error al guardar",
"settings.nav.sidecars": "SideCars",
"settings.section.sidecars.eyebrow": "Server services",
"settings.section.sidecars.title": "SideCars",
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
"sidecars.form.name": "Name",
"sidecars.form.validation": "Enter a valid SideCar name and port.",
"sidecars.form.port": "Port",
"sidecars.form.insecure": "Use HTTP",
"sidecars.form.protocol": "Protocol",
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
"sidecars.form.protocol.https": "HTTPS",
"sidecars.form.protocol.http": "HTTP",
"sidecars.form.prefixMode": "Prefix mode",
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
"sidecars.form.prefixMode.strip": "Strip prefix",
"sidecars.form.prefixMode.preserve": "Preserve prefix",
"sidecars.form.add": "Add SideCar",
"sidecars.kind.port": "Port",
"sidecars.status.running": "Running",
"sidecars.status.stopped": "Stopped",
"sidecars.basePath": "Base path",
"sidecars.settings.listTitle": "Configured SideCars",
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
"sidecars.settings.empty": "No SideCars configured yet.",
"sidecars.picker.title": "Open SideCar",
"sidecars.picker.loading": "Loading SideCars...",
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
"sidecars.picker.empty": "No port-based SideCars are available yet.",
"sidecars.picker.close": "Close",
"sidecars.open.errorTitle": "Unable to open SideCar",
"sidecars.open.notFound": "SideCar not found.",
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
"sidecars.back": "Back",
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
} as const

View File

@@ -15,17 +15,17 @@ export const commandMessages = {
"commands.newInstance.description": "Ouvrir le sélecteur de dossiers pour créer une nouvelle instance",
"commands.newInstance.keywords": "dossier, projet, espace de travail",
"commands.closeInstance.label": "Fermer l'instance",
"commands.closeInstance.description": "Arrêter le serveur de l'instance actuelle",
"commands.closeInstance.keywords": "arrêter, quitter, fermer",
"commands.closeInstance.label": "Fermer l'onglet",
"commands.closeInstance.description": "Fermer l'onglet de premier niveau actuel",
"commands.closeInstance.keywords": "arrêter, quitter, fermer, onglet",
"commands.nextInstance.label": "Instance suivante",
"commands.nextInstance.description": "Passer à l'onglet d'instance suivant",
"commands.nextInstance.keywords": "changer, naviguer, suivant",
"commands.nextInstance.label": "Onglet suivant",
"commands.nextInstance.description": "Passer à l'onglet de premier niveau suivant",
"commands.nextInstance.keywords": "changer, naviguer, suivant, onglet",
"commands.previousInstance.label": "Instance précédente",
"commands.previousInstance.description": "Passer à l'onglet d'instance précédent",
"commands.previousInstance.keywords": "changer, naviguer, précédent",
"commands.previousInstance.label": "Onglet précédent",
"commands.previousInstance.description": "Passer à l'onglet de premier niveau précédent",
"commands.previousInstance.keywords": "changer, naviguer, précédent, onglet",
"commands.newSession.label": "Nouvelle session",
"commands.newSession.description": "Créer une nouvelle session parente",

View File

@@ -5,7 +5,7 @@ export const folderSelectionMessages = {
"folderSelection.tagline": "Sélectionnez un dossier pour commencer à coder avec l'IA",
"folderSelection.links.github": "GitHub de CodeNomad",
"folderSelection.links.githubStars": "Stars GitHub de CodeNomad",
"folderSelection.links.githubStars": "Étoiles GitHub de CodeNomad",
"folderSelection.links.discord": "Discord de CodeNomad",
"folderSelection.empty.title": "Aucun dossier récent",
@@ -16,13 +16,13 @@ export const folderSelectionMessages = {
"folderSelection.recent.subtitle.other": "{count} dossiers disponibles",
"folderSelection.recent.remove": "Retirer des récents",
"folderSelection.browse.title": "Parcourir les dossiers",
"folderSelection.browse.title": "Parcourir un dossier",
"folderSelection.browse.subtitle": "Sélectionnez n'importe quel dossier sur votre ordinateur",
"folderSelection.browse.button": "Parcourir les dossiers",
"folderSelection.browse.buttonOpening": "Ouverture...",
"folderSelection.actions.title": "Ouvrir un dossier ou connecter un serveur",
"folderSelection.actions.title": "Ouvrir un dossier ou se connecter à un serveur",
"folderSelection.actions.subtitle": "Ouvrez un dossier local ou connectez-vous à un serveur CodeNomad",
"folderSelection.actions.connectButton": "Connecter un serveur CodeNomad",
"folderSelection.actions.connectButton": "Se connecter au serveur CodeNomad",
"folderSelection.advancedSettings": "Paramètres avancés",
"folderSelection.opencode": "OpenCode",
@@ -69,4 +69,5 @@ export const folderSelectionMessages = {
"folderSelection.servers.dialog.connecting": "Connexion...",
"folderSelection.servers.dialog.errorRequired": "Le nom du serveur et l'URL sont requis.",
"folderSelection.servers.dialog.errorConnect": "Impossible de se connecter au serveur distant.",
"folderSelection.sidecars.button": "Open SideCar",
} as const

View File

@@ -194,4 +194,40 @@ export const settingsMessages = {
"settings.speech.save.saved": "Enregistré",
"settings.speech.save.unsaved": "Modifications non enregistrées",
"settings.speech.save.error": "Échec de l'enregistrement",
"settings.nav.sidecars": "SideCars",
"settings.section.sidecars.eyebrow": "Server services",
"settings.section.sidecars.title": "SideCars",
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
"sidecars.form.name": "Name",
"sidecars.form.validation": "Enter a valid SideCar name and port.",
"sidecars.form.port": "Port",
"sidecars.form.insecure": "Use HTTP",
"sidecars.form.protocol": "Protocol",
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
"sidecars.form.protocol.https": "HTTPS",
"sidecars.form.protocol.http": "HTTP",
"sidecars.form.prefixMode": "Prefix mode",
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
"sidecars.form.prefixMode.strip": "Strip prefix",
"sidecars.form.prefixMode.preserve": "Preserve prefix",
"sidecars.form.add": "Add SideCar",
"sidecars.kind.port": "Port",
"sidecars.status.running": "Running",
"sidecars.status.stopped": "Stopped",
"sidecars.basePath": "Base path",
"sidecars.settings.listTitle": "Configured SideCars",
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
"sidecars.settings.empty": "No SideCars configured yet.",
"sidecars.picker.title": "Open SideCar",
"sidecars.picker.loading": "Loading SideCars...",
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
"sidecars.picker.empty": "No port-based SideCars are available yet.",
"sidecars.picker.close": "Close",
"sidecars.open.errorTitle": "Unable to open SideCar",
"sidecars.open.notFound": "SideCar not found.",
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
"sidecars.back": "Back",
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
} as const

View File

@@ -15,17 +15,17 @@ export const commandMessages = {
"commands.newInstance.description": "פתח בורר תיקיות ליצירת מופע חדש",
"commands.newInstance.keywords": "תיקייה, פרויקט, סביבת עבודה",
"commands.closeInstance.label": "סגור מופע",
"commands.closeInstance.description": "עצור את השרת של המופע הנוכחי",
"commands.closeInstance.keywords": "עצור, סגור",
"commands.closeInstance.label": "סגור לשונית",
"commands.closeInstance.description": "סגור את הלשונית העליונה הנוכחית",
"commands.closeInstance.keywords": "עצור, סגור, לשונית",
"commands.nextInstance.label": "מופע הבא",
"commands.nextInstance.description": "עבור למופע הבא",
"commands.nextInstance.keywords": "החלף, נווט",
"commands.nextInstance.label": "הלשונית הבאה",
"commands.nextInstance.description": "עבור ללשונית העליונה הבאה",
"commands.nextInstance.keywords": "החלף, נווט, לשונית",
"commands.previousInstance.label": "מופע קודם",
"commands.previousInstance.description": "עבור למופע הקודם",
"commands.previousInstance.keywords": "החלף, נווט",
"commands.previousInstance.label": "הלשונית הקודמת",
"commands.previousInstance.description": "עבור ללשונית העליונה הקודמת",
"commands.previousInstance.keywords": "החלף, נווט, לשונית",
"commands.newSession.label": "סשן חדש",
"commands.newSession.description": "צור סשן הורה חדש",

View File

@@ -69,4 +69,5 @@ export const folderSelectionMessages = {
"folderSelection.servers.dialog.connecting": "מתחבר...",
"folderSelection.servers.dialog.errorRequired": "שם השרת והכתובת הם שדות חובה.",
"folderSelection.servers.dialog.errorConnect": "לא ניתן היה להתחבר לשרת המרוחק.",
"folderSelection.sidecars.button": "Open SideCar",
} as const

View File

@@ -193,4 +193,40 @@ export const settingsMessages = {
"settings.speech.save.saved": "נשמר",
"settings.speech.save.unsaved": "יש שינויים שלא נשמרו",
"settings.speech.save.error": "השמירה נכשלה",
"settings.nav.sidecars": "SideCars",
"settings.section.sidecars.eyebrow": "Server services",
"settings.section.sidecars.title": "SideCars",
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
"sidecars.form.name": "Name",
"sidecars.form.validation": "Enter a valid SideCar name and port.",
"sidecars.form.port": "Port",
"sidecars.form.insecure": "Use HTTP",
"sidecars.form.protocol": "Protocol",
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
"sidecars.form.protocol.https": "HTTPS",
"sidecars.form.protocol.http": "HTTP",
"sidecars.form.prefixMode": "Prefix mode",
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
"sidecars.form.prefixMode.strip": "Strip prefix",
"sidecars.form.prefixMode.preserve": "Preserve prefix",
"sidecars.form.add": "Add SideCar",
"sidecars.kind.port": "Port",
"sidecars.status.running": "Running",
"sidecars.status.stopped": "Stopped",
"sidecars.basePath": "Base path",
"sidecars.settings.listTitle": "Configured SideCars",
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
"sidecars.settings.empty": "No SideCars configured yet.",
"sidecars.picker.title": "Open SideCar",
"sidecars.picker.loading": "Loading SideCars...",
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
"sidecars.picker.empty": "No port-based SideCars are available yet.",
"sidecars.picker.close": "Close",
"sidecars.open.errorTitle": "Unable to open SideCar",
"sidecars.open.notFound": "SideCar not found.",
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
"sidecars.back": "Back",
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
} as const

View File

@@ -15,17 +15,17 @@ export const commandMessages = {
"commands.newInstance.description": "フォルダ選択を開いて新しいインスタンスを作成",
"commands.newInstance.keywords": "フォルダ, プロジェクト, ワークスペース, folder, project, workspace",
"commands.closeInstance.label": "インスタンスを閉じる",
"commands.closeInstance.description": "現在のインスタンスのサーバーを停止",
"commands.closeInstance.keywords": "停止, 終了, 閉じる, stop, quit, close",
"commands.closeInstance.label": "タブを閉じる",
"commands.closeInstance.description": "現在のトップレベルタブを閉じる",
"commands.closeInstance.keywords": "閉じる, タブ, stop, quit, close",
"commands.nextInstance.label": "次のインスタンス",
"commands.nextInstance.description": "次のインスタンスタブへ切り替え",
"commands.nextInstance.keywords": "切り替え, 移動, switch, navigate",
"commands.nextInstance.label": "次のタブ",
"commands.nextInstance.description": "次のトップレベルタブへ切り替え",
"commands.nextInstance.keywords": "切り替え, 移動, タブ, switch, navigate",
"commands.previousInstance.label": "前のインスタンス",
"commands.previousInstance.description": "前のインスタンスタブへ切り替え",
"commands.previousInstance.keywords": "切り替え, 移動, switch, navigate",
"commands.previousInstance.label": "前のタブ",
"commands.previousInstance.description": "前のトップレベルタブへ切り替え",
"commands.previousInstance.keywords": "切り替え, 移動, タブ, switch, navigate",
"commands.newSession.label": "新しいセッション",
"commands.newSession.description": "新しい親セッションを作成",

View File

@@ -69,4 +69,5 @@ export const folderSelectionMessages = {
"folderSelection.servers.dialog.connecting": "接続中...",
"folderSelection.servers.dialog.errorRequired": "サーバー名と URL は必須です。",
"folderSelection.servers.dialog.errorConnect": "リモートサーバーに接続できませんでした。",
"folderSelection.sidecars.button": "Open SideCar",
} as const

View File

@@ -194,4 +194,40 @@ export const settingsMessages = {
"settings.speech.save.saved": "保存済み",
"settings.speech.save.unsaved": "未保存の変更",
"settings.speech.save.error": "保存に失敗しました",
"settings.nav.sidecars": "SideCars",
"settings.section.sidecars.eyebrow": "Server services",
"settings.section.sidecars.title": "SideCars",
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
"sidecars.form.name": "Name",
"sidecars.form.validation": "Enter a valid SideCar name and port.",
"sidecars.form.port": "Port",
"sidecars.form.insecure": "Use HTTP",
"sidecars.form.protocol": "Protocol",
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
"sidecars.form.protocol.https": "HTTPS",
"sidecars.form.protocol.http": "HTTP",
"sidecars.form.prefixMode": "Prefix mode",
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
"sidecars.form.prefixMode.strip": "Strip prefix",
"sidecars.form.prefixMode.preserve": "Preserve prefix",
"sidecars.form.add": "Add SideCar",
"sidecars.kind.port": "Port",
"sidecars.status.running": "Running",
"sidecars.status.stopped": "Stopped",
"sidecars.basePath": "Base path",
"sidecars.settings.listTitle": "Configured SideCars",
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
"sidecars.settings.empty": "No SideCars configured yet.",
"sidecars.picker.title": "Open SideCar",
"sidecars.picker.loading": "Loading SideCars...",
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
"sidecars.picker.empty": "No port-based SideCars are available yet.",
"sidecars.picker.close": "Close",
"sidecars.open.errorTitle": "Unable to open SideCar",
"sidecars.open.notFound": "SideCar not found.",
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
"sidecars.back": "Back",
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
} as const

View File

@@ -15,17 +15,17 @@ export const commandMessages = {
"commands.newInstance.description": "Открыть выбор папки для создания нового экземпляра",
"commands.newInstance.keywords": "папка, проект, рабочее пространство",
"commands.closeInstance.label": "Закрыть экземпляр",
"commands.closeInstance.description": "Остановить сервер текущего экземпляра",
"commands.closeInstance.keywords": "остановить, выйти, закрыть",
"commands.closeInstance.label": "Закрыть вкладку",
"commands.closeInstance.description": "Закрыть текущую верхнеуровневую вкладку",
"commands.closeInstance.keywords": "остановить, выйти, закрыть, вкладка",
"commands.nextInstance.label": "Следующий экземпляр",
"commands.nextInstance.description": "Переключиться на следующую вкладку экземпляра",
"commands.nextInstance.keywords": "переключить, навигация",
"commands.nextInstance.label": "Следующая вкладка",
"commands.nextInstance.description": "Переключиться на следующую верхнеуровневую вкладку",
"commands.nextInstance.keywords": "переключить, навигация, вкладка",
"commands.previousInstance.label": "Предыдущий экземпляр",
"commands.previousInstance.description": "Переключиться на предыдущую вкладку экземпляра",
"commands.previousInstance.keywords": "переключить, навигация",
"commands.previousInstance.label": "Предыдущая вкладка",
"commands.previousInstance.description": "Переключиться на предыдущую верхнеуровневую вкладку",
"commands.previousInstance.keywords": "переключить, навигация, вкладка",
"commands.newSession.label": "Новая сессия",
"commands.newSession.description": "Создать новую родительскую сессию",

View File

@@ -69,4 +69,5 @@ export const folderSelectionMessages = {
"folderSelection.servers.dialog.connecting": "Подключение...",
"folderSelection.servers.dialog.errorRequired": "Имя сервера и URL обязательны.",
"folderSelection.servers.dialog.errorConnect": "Не удалось подключиться к удаленному серверу.",
"folderSelection.sidecars.button": "Open SideCar",
} as const

View File

@@ -194,4 +194,40 @@ export const settingsMessages = {
"settings.speech.save.saved": "Сохранено",
"settings.speech.save.unsaved": "Есть несохранённые изменения",
"settings.speech.save.error": "Не удалось сохранить",
"settings.nav.sidecars": "SideCars",
"settings.section.sidecars.eyebrow": "Server services",
"settings.section.sidecars.title": "SideCars",
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
"sidecars.form.name": "Name",
"sidecars.form.validation": "Enter a valid SideCar name and port.",
"sidecars.form.port": "Port",
"sidecars.form.insecure": "Use HTTP",
"sidecars.form.protocol": "Protocol",
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
"sidecars.form.protocol.https": "HTTPS",
"sidecars.form.protocol.http": "HTTP",
"sidecars.form.prefixMode": "Prefix mode",
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
"sidecars.form.prefixMode.strip": "Strip prefix",
"sidecars.form.prefixMode.preserve": "Preserve prefix",
"sidecars.form.add": "Add SideCar",
"sidecars.kind.port": "Port",
"sidecars.status.running": "Running",
"sidecars.status.stopped": "Stopped",
"sidecars.basePath": "Base path",
"sidecars.settings.listTitle": "Configured SideCars",
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
"sidecars.settings.empty": "No SideCars configured yet.",
"sidecars.picker.title": "Open SideCar",
"sidecars.picker.loading": "Loading SideCars...",
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
"sidecars.picker.empty": "No port-based SideCars are available yet.",
"sidecars.picker.close": "Close",
"sidecars.open.errorTitle": "Unable to open SideCar",
"sidecars.open.notFound": "SideCar not found.",
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
"sidecars.back": "Back",
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
} as const

View File

@@ -15,17 +15,17 @@ export const commandMessages = {
"commands.newInstance.description": "打开文件夹选择器以创建新实例",
"commands.newInstance.keywords": "folder, project, workspace, 文件夹, 项目, 工作区",
"commands.closeInstance.label": "关闭实例",
"commands.closeInstance.description": "停止当前实例的服务器",
"commands.closeInstance.keywords": "stop, quit, close, 停止, 退出, 关闭",
"commands.closeInstance.label": "关闭标签页",
"commands.closeInstance.description": "关闭当前顶层标签页",
"commands.closeInstance.keywords": "stop, quit, close, 停止, 退出, 关闭, 标签",
"commands.nextInstance.label": "下一个实例",
"commands.nextInstance.description": "切换到下一个实例标签页",
"commands.nextInstance.keywords": "switch, navigate, 切换, 导航",
"commands.nextInstance.label": "下一个标签页",
"commands.nextInstance.description": "切换到下一个顶层标签页",
"commands.nextInstance.keywords": "switch, navigate, 切换, 导航, 标签",
"commands.previousInstance.label": "上一个实例",
"commands.previousInstance.description": "切换到上一个实例标签页",
"commands.previousInstance.keywords": "switch, navigate, 切换, 导航",
"commands.previousInstance.label": "上一个标签页",
"commands.previousInstance.description": "切换到上一个顶层标签页",
"commands.previousInstance.keywords": "switch, navigate, 切换, 导航, 标签",
"commands.newSession.label": "新建会话",
"commands.newSession.description": "创建新的父会话",

View File

@@ -69,4 +69,5 @@ export const folderSelectionMessages = {
"folderSelection.servers.dialog.connecting": "连接中...",
"folderSelection.servers.dialog.errorRequired": "服务器名称和 URL 为必填项。",
"folderSelection.servers.dialog.errorConnect": "无法连接到远程服务器。",
"folderSelection.sidecars.button": "Open SideCar",
} as const

View File

@@ -194,4 +194,40 @@ export const settingsMessages = {
"settings.speech.save.saved": "已保存",
"settings.speech.save.unsaved": "有未保存的更改",
"settings.speech.save.error": "保存失败",
"settings.nav.sidecars": "SideCars",
"settings.section.sidecars.eyebrow": "Server services",
"settings.section.sidecars.title": "SideCars",
"settings.section.sidecars.subtitle": "Configure local services listening on fixed ports that the server can proxy into tabs.",
"sidecars.form.name": "Name",
"sidecars.form.validation": "Enter a valid SideCar name and port.",
"sidecars.form.port": "Port",
"sidecars.form.insecure": "Use HTTP",
"sidecars.form.protocol": "Protocol",
"sidecars.form.protocol.help": "Choose how the proxy should connect to the local service.",
"sidecars.form.protocol.https": "HTTPS",
"sidecars.form.protocol.http": "HTTP",
"sidecars.form.prefixMode": "Prefix mode",
"sidecars.form.prefixMode.help": "Choose whether the SideCar receives the public /sidecars path prefix.",
"sidecars.form.prefixMode.strip": "Strip prefix",
"sidecars.form.prefixMode.preserve": "Preserve prefix",
"sidecars.form.add": "Add SideCar",
"sidecars.kind.port": "Port",
"sidecars.status.running": "Running",
"sidecars.status.stopped": "Stopped",
"sidecars.basePath": "Base path",
"sidecars.settings.listTitle": "Configured SideCars",
"sidecars.settings.listSubtitle": "Review the port-based SideCars available in the picker.",
"sidecars.settings.empty": "No SideCars configured yet.",
"sidecars.picker.title": "Open SideCar",
"sidecars.picker.loading": "Loading SideCars...",
"sidecars.picker.subtitle": "Choose an available SideCar to open in a new tab.",
"sidecars.picker.empty": "No port-based SideCars are available yet.",
"sidecars.picker.close": "Close",
"sidecars.open.errorTitle": "Unable to open SideCar",
"sidecars.open.notFound": "SideCar not found.",
"sidecars.open.notRunning": "SideCar is not reachable on its configured port.",
"sidecars.back": "Back",
"sidecars.refresh": "Refresh",
"sidecars.path": "Path",
"sidecars.go": "Go",
} as const

View File

@@ -1,11 +1,12 @@
import { instances, activeInstanceId, setActiveInstanceId } from "../stores/instances"
import { activeInstanceId } from "../stores/instances"
import { selectAppTabByIndex } from "../stores/app-tabs"
import { activeSessionId, setActiveSession, getSessions, activeParentSessionId } from "../stores/sessions"
import { keyboardRegistry } from "./keyboard-registry"
import { isMac } from "./keyboard-utils"
export function setupTabKeyboardShortcuts(
handleNewInstance: () => void,
handleCloseInstance: (instanceId: string) => void,
handleCloseActiveTab: () => Promise<void>,
handleNewSession: (instanceId: string) => void,
handleCloseSession: (instanceId: string, sessionId: string) => void,
handleCommandPalette: () => void,
@@ -35,11 +36,7 @@ export function setupTabKeyboardShortcuts(
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key >= "1" && e.key <= "9") {
e.preventDefault()
const index = parseInt(e.key) - 1
const instanceIds = Array.from(instances().keys())
if (instanceIds[index]) {
setActiveInstanceId(instanceIds[index])
}
selectAppTabByIndex(parseInt(e.key) - 1)
}
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key >= "1" && e.key <= "9") {
@@ -67,10 +64,7 @@ export function setupTabKeyboardShortcuts(
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "w") {
e.preventDefault()
const instanceId = activeInstanceId()
if (instanceId) {
handleCloseInstance(instanceId)
}
void handleCloseActiveTab()
}
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "w") {

View File

@@ -1,5 +1,6 @@
import { keyboardRegistry } from "../keyboard-registry"
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
import { activeInstanceId } from "../../stores/instances"
import { selectNextAppTab, selectPreviousAppTab } from "../../stores/app-tabs"
import { activeSessionId, getVisibleSessionIds, setActiveSession, setActiveSessionFromList } from "../../stores/sessions"
export function registerNavigationShortcuts() {
@@ -11,14 +12,8 @@ export function registerNavigationShortcuts() {
id: "instance-prev",
key: "[",
modifiers: { ctrl: !isMac(), meta: isMac() },
handler: () => {
const ids = Array.from(instances().keys())
if (ids.length <= 1) return
const current = ids.indexOf(activeInstanceId() || "")
const prev = current <= 0 ? ids.length - 1 : current - 1
if (ids[prev]) setActiveInstanceId(ids[prev])
},
description: "previous instance",
handler: () => selectPreviousAppTab(),
description: "previous tab",
context: "global",
})
@@ -26,14 +21,8 @@ export function registerNavigationShortcuts() {
id: "instance-next",
key: "]",
modifiers: { ctrl: !isMac(), meta: isMac() },
handler: () => {
const ids = Array.from(instances().keys())
if (ids.length <= 1) return
const current = ids.indexOf(activeInstanceId() || "")
const next = (current + 1) % ids.length
if (ids[next]) setActiveInstanceId(ids[next])
},
description: "next instance",
handler: () => selectNextAppTab(),
description: "next tab",
context: "global",
})