diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index bd9dd25d2..2d5ad9909 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -35,6 +35,7 @@ import * as Linking from "expo-linking"; import * as Notifications from "expo-notifications"; import { LeftSidebar } from "@/components/left-sidebar"; import { DownloadToast } from "@/components/download-toast"; +import { UpdateBanner } from "@/desktop/updates/update-banner"; import { ToastProvider } from "@/contexts/toast-context"; import { usePanelStore } from "@/stores/panel-store"; import { runOnJS, interpolate, Extrapolation, useSharedValue } from "react-native-reanimated"; @@ -379,6 +380,7 @@ function AppContainer({ {isMobile && chromeEnabled && } + @@ -578,7 +580,12 @@ export default function RootLayout() { > - + + `${params?.serverId}:${params?.workspaceId}` + } + /> {})} isDragging={input?.isDragging ?? false} @@ -1147,7 +1148,7 @@ function ProjectBlock({ return } onWorkspacePress?.() - router.replace(buildHostWorkspaceRoute(serverId, flattenedWorkspace.workspaceId) as any) + navigateToWorkspace(serverId, flattenedWorkspace.workspaceId) }} drag={drag} isDragging={isDragging} diff --git a/packages/app/src/components/terminal-emulator.tsx b/packages/app/src/components/terminal-emulator.tsx index 9e41c7e52..b29ba0e8b 100644 --- a/packages/app/src/components/terminal-emulator.tsx +++ b/packages/app/src/components/terminal-emulator.tsx @@ -7,10 +7,6 @@ import type { ITheme } from "@xterm/xterm"; import type { PendingTerminalModifiers } from "../utils/terminal-keys"; import { TerminalEmulatorRuntime } from "../terminal/runtime/terminal-emulator-runtime"; import { focusWithRetries } from "../utils/web-focus"; -import { - summarizeTerminalText, - terminalDebugLog, -} from "../terminal/runtime/terminal-debug"; function buildXtermThemeKey(theme: ITheme): string { const values: Array = [ @@ -287,14 +283,6 @@ export default function TerminalEmulator({ } if (outputChunkSequence <= appliedChunkSequenceRef.current) { - terminalDebugLog({ - scope: "emulator-component", - event: "output:chunk:skip-duplicate", - details: { - sequence: outputChunkSequence, - lastAppliedSequence: appliedChunkSequenceRef.current, - }, - }); onOutputChunkConsumed?.(outputChunkSequence); return; } @@ -307,13 +295,6 @@ export default function TerminalEmulator({ appliedChunkSequenceRef.current = outputChunkSequence; if (outputChunkText.length === 0) { - terminalDebugLog({ - scope: "emulator-component", - event: "output:chunk:clear", - details: { - sequence: outputChunkSequence, - }, - }); runtime.clear({ onCommitted: () => { onOutputChunkConsumed?.(outputChunkSequence); @@ -321,16 +302,6 @@ export default function TerminalEmulator({ }); return; } - terminalDebugLog({ - scope: "emulator-component", - event: "output:chunk:write", - details: { - sequence: outputChunkSequence, - replay: outputChunkReplay, - length: outputChunkText.length, - preview: summarizeTerminalText({ text: outputChunkText, maxChars: 80 }), - }, - }); runtime.write({ text: outputChunkText, suppressInput: outputChunkReplay, @@ -383,10 +354,6 @@ export default function TerminalEmulator({ touchAction: "pan-y", }} onPointerDown={() => { - terminalDebugLog({ - scope: "emulator-component", - event: "surface:pointer-down-focus", - }); runtimeRef.current?.focus(); }} > diff --git a/packages/app/src/components/terminal-pane.tsx b/packages/app/src/components/terminal-pane.tsx index ddec4fa75..f106b17e5 100644 --- a/packages/app/src/components/terminal-pane.tsx +++ b/packages/app/src/components/terminal-pane.tsx @@ -35,10 +35,6 @@ import { TerminalStreamController, type TerminalStreamControllerStatus, } from "@/terminal/runtime/terminal-stream-controller"; -import { - summarizeTerminalText, - terminalDebugLog, -} from "@/terminal/runtime/terminal-debug"; import { usePanelStore } from "@/stores/panel-store"; import { toXtermTheme } from "@/utils/to-xterm-theme"; import TerminalEmulator from "./terminal-emulator"; @@ -366,7 +362,7 @@ export function TerminalPane({ const terminals = terminalsQuery.data?.terminals ?? []; useEffect(() => { - if (!client || !isConnected) { + if (!client || !isConnected || !isScreenFocused) { return; } @@ -716,17 +712,6 @@ export function TerminalPane({ meta: input.meta, }, }; - terminalDebugLog({ - scope: "terminal-pane", - event: "input:key:send", - details: { - key: normalizedKey, - ctrl: input.ctrl, - shift: input.shift, - alt: input.alt, - activeStreamId: getCurrentActiveStreamId(), - }, - }); if (!dispatchTerminalInputEntry(pendingEntry)) { enqueuePendingTerminalInput(pendingEntry); } @@ -745,16 +730,6 @@ export function TerminalPane({ if (data.length === 0) { return; } - const currentStreamId = getCurrentActiveStreamId(); - terminalDebugLog({ - scope: "terminal-pane", - event: "input:data:received", - details: { - length: data.length, - preview: summarizeTerminalText({ text: data, maxChars: 80 }), - activeStreamId: currentStreamId, - }, - }); if (hasPendingTerminalModifiers(modifiers)) { const pendingResolution = resolvePendingModifierDataInput({ @@ -788,15 +763,6 @@ export function TerminalPane({ }); return; } - terminalDebugLog({ - scope: "terminal-pane", - event: "input:data:send", - details: { - length: data.length, - preview: summarizeTerminalText({ text: data, maxChars: 80 }), - activeStreamId: currentStreamId, - }, - }); const pendingEntry: PendingTerminalInput = { type: "data", data, @@ -835,15 +801,6 @@ export function TerminalPane({ return; } lastReportedSizeRef.current = { rows: normalizedRows, cols: normalizedCols }; - terminalDebugLog({ - scope: "terminal-pane", - event: "display:resize:send", - details: { - terminalId: selectedTerminalId, - rows: normalizedRows, - cols: normalizedCols, - }, - }); client.sendTerminalInput(selectedTerminalId, { type: "resize", rows: normalizedRows, @@ -871,13 +828,6 @@ export function TerminalPane({ }, [clearPendingModifiers]); const handleOutputChunkConsumed = useCallback((sequence: number) => { - terminalDebugLog({ - scope: "terminal-pane", - event: "output:chunk:consumed", - details: { - sequence, - }, - }); outputSession.consume({ sequence }); }, [outputSession]); @@ -1040,7 +990,7 @@ export function TerminalPane({ ) : null} - {selectedTerminal ? ( + {selectedTerminal && isScreenFocused ? ( + ) : selectedTerminal ? ( + ) : ( No terminal selected )} - {isAttaching ? ( + {isAttaching && isScreenFocused ? ( | null>(null); + + useEffect(() => { + if (!isDesktop) return; + + void checkForUpdates({ silent: true }); + + intervalRef.current = setInterval(() => { + void checkForUpdates({ silent: true }); + }, CHECK_INTERVAL_MS); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [isDesktop, checkForUpdates]); + + if (!isDesktop) return null; + if (dismissed) return null; + if (status !== "available" && status !== "installed") return null; + + const isInstalled = status === "installed"; + + return ( + + + setDismissed(true)} + hitSlop={8} + style={styles.closeButton} + > + + + + + + {isInstalled ? "Update installed" : "Update available"} + + + {isInstalled + ? "Restart to use the new version." + : `${availableUpdate?.latestVersion ? `v${availableUpdate.latestVersion.replace(/^v/i, "")} is ready` : "A new version is ready"} to install.`} + + + + + void openExternalUrl(CHANGELOG_URL)} + style={({ pressed }) => [ + styles.outlineButton, + pressed && styles.buttonPressed, + ]} + > + What's new + + + {!isInstalled && ( + void installUpdate()} + disabled={isInstalling} + style={({ pressed }) => [ + styles.primaryButton, + pressed && styles.buttonPressed, + isInstalling && styles.buttonDisabled, + ]} + > + + {isInstalling ? "Installing..." : "Install & restart"} + + + )} + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + container: { + position: "absolute", + bottom: theme.spacing[4], + right: theme.spacing[4], + zIndex: 1000, + }, + banner: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[4], + backgroundColor: theme.colors.surface2, + borderRadius: theme.borderRadius.xl, + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.border, + paddingVertical: theme.spacing[3], + paddingLeft: theme.spacing[4], + paddingRight: theme.spacing[3], + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 12, + elevation: 8, + maxWidth: 480, + }, + closeButton: { + position: "absolute", + top: theme.spacing[2], + left: theme.spacing[2], + padding: theme.spacing[1], + zIndex: 1, + }, + textSection: { + flex: 1, + gap: 2, + paddingTop: theme.spacing[1], + }, + title: { + color: theme.colors.foreground, + fontSize: theme.fontSize.sm, + fontWeight: theme.fontWeight.semibold, + }, + subtitle: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + }, + actions: { + flexDirection: "row", + gap: theme.spacing[2], + alignItems: "center", + }, + outlineButton: { + paddingVertical: theme.spacing[2], + paddingHorizontal: theme.spacing[3], + borderRadius: theme.borderRadius.md, + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.border, + }, + outlineButtonText: { + color: theme.colors.foreground, + fontSize: theme.fontSize.xs, + fontWeight: theme.fontWeight.medium, + }, + primaryButton: { + paddingVertical: theme.spacing[2], + paddingHorizontal: theme.spacing[3], + borderRadius: theme.borderRadius.md, + backgroundColor: theme.colors.foreground, + }, + primaryButtonText: { + color: theme.colors.surface0, + fontSize: theme.fontSize.xs, + fontWeight: theme.fontWeight.medium, + }, + buttonPressed: { + opacity: 0.8, + }, + buttonDisabled: { + opacity: 0.5, + }, +})); diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts index 675d30fbf..c566cb86d 100644 --- a/packages/app/src/hooks/use-keyboard-shortcuts.ts +++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts @@ -1,14 +1,14 @@ import { useEffect } from "react"; import { Platform } from "react-native"; -import { usePathname, useRouter } from "expo-router"; +import { usePathname } from "expo-router"; import { getIsTauri } from "@/constants/layout"; import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore"; import { - buildHostWorkspaceRoute, parseHostAgentRouteFromPathname, parseHostWorkspaceRouteFromPathname, } from "@/utils/host-routes"; +import { navigateToWorkspace } from "@/hooks/use-workspace-navigation"; import { type MessageInputKeyboardActionKind, type KeyboardShortcutPayload, @@ -32,7 +32,6 @@ export function useKeyboardShortcuts({ selectedAgentId?: string; toggleFileExplorer?: () => void; }) { - const router = useRouter(); const pathname = usePathname(); const resetModifiers = useKeyboardShortcutsStore((s) => s.resetModifiers); @@ -57,11 +56,7 @@ export function useKeyboardShortcuts({ return false; } - const shouldReplace = - Boolean(parseHostWorkspaceRouteFromPathname(pathname)) || - Boolean(parseHostAgentRouteFromPathname(pathname)); - const navigate = shouldReplace ? router.replace : router.push; - navigate(buildHostWorkspaceRoute(target.serverId, target.workspaceId) as any); + navigateToWorkspace(target.serverId, target.workspaceId); return true; }; const navigateRelativeWorkspace = (delta: 1 | -1): boolean => { @@ -77,7 +72,7 @@ export function useKeyboardShortcuts({ if (!fallback) { return false; } - router.push(buildHostWorkspaceRoute(fallback.serverId, fallback.workspaceId) as any); + navigateToWorkspace(fallback.serverId, fallback.workspaceId); return true; } @@ -92,7 +87,7 @@ export function useKeyboardShortcuts({ if (!target) { return false; } - router.replace(buildHostWorkspaceRoute(target.serverId, target.workspaceId) as any); + navigateToWorkspace(target.serverId, target.workspaceId); return true; }; @@ -134,22 +129,6 @@ export function useKeyboardShortcuts({ return false; } }; - const requestWorkspaceTabAction = (input: - | { kind: "new" | "close-current" } - | { kind: "navigate-index"; index: number } - | { kind: "navigate-relative"; delta: 1 | -1 }): boolean => { - const route = parseHostWorkspaceRouteFromPathname(pathname); - if (!route) { - return false; - } - useKeyboardShortcutsStore.getState().requestWorkspaceTabAction({ - serverId: route.serverId, - workspaceId: route.workspaceId, - ...input, - }); - return true; - }; - const handleAction = (input: { action: string; payload: KeyboardShortcutPayload; @@ -159,23 +138,31 @@ export function useKeyboardShortcuts({ case "agent.new": return openProjectPicker(); case "workspace.tab.new": - return requestWorkspaceTabAction({ kind: "new" }); + return keyboardActionDispatcher.dispatch({ + id: "workspace.tab.new", + scope: "workspace", + }); case "workspace.tab.close.current": - return requestWorkspaceTabAction({ kind: "close-current" }); + return keyboardActionDispatcher.dispatch({ + id: "workspace.tab.close-current", + scope: "workspace", + }); case "workspace.tab.navigate.index": if (!input.payload || typeof input.payload !== "object" || !("index" in input.payload)) { return false; } - return requestWorkspaceTabAction({ - kind: "navigate-index", + return keyboardActionDispatcher.dispatch({ + id: "workspace.tab.navigate-index", + scope: "workspace", index: input.payload.index, }); case "workspace.tab.navigate.relative": if (!input.payload || typeof input.payload !== "object" || !("delta" in input.payload)) { return false; } - return requestWorkspaceTabAction({ - kind: "navigate-relative", + return keyboardActionDispatcher.dispatch({ + id: "workspace.tab.navigate-relative", + scope: "workspace", delta: input.payload.delta, }); case "workspace.navigate.index": @@ -326,7 +313,6 @@ export function useKeyboardShortcuts({ isMobile, pathname, resetModifiers, - router, selectedAgentId, toggleAgentList, toggleFileExplorer, diff --git a/packages/app/src/hooks/use-workspace-navigation.ts b/packages/app/src/hooks/use-workspace-navigation.ts new file mode 100644 index 000000000..09c46c54d --- /dev/null +++ b/packages/app/src/hooks/use-workspace-navigation.ts @@ -0,0 +1,26 @@ +import { useCallback } from "react"; +import { router } from "expo-router"; +import { buildHostWorkspaceRoute } from "@/utils/host-routes"; + +/** + * Navigate to a workspace screen. Uses router.navigate() which, + * combined with getId on the workspace Stack.Screen, ensures: + * - Only one instance of each workspace exists in the stack + * - History is preserved (back button works) + * - No duplicate workspace screens + */ +export function navigateToWorkspace(serverId: string, workspaceId: string) { + const href = buildHostWorkspaceRoute(serverId, workspaceId); + router.navigate(href as any); +} + +export function useWorkspaceNavigation() { + return { + navigateToWorkspace: useCallback( + (serverId: string, workspaceId: string) => { + navigateToWorkspace(serverId, workspaceId); + }, + [] + ), + }; +} diff --git a/packages/app/src/keyboard/keyboard-action-dispatcher.ts b/packages/app/src/keyboard/keyboard-action-dispatcher.ts index 6a8d5b2a1..708c2a03e 100644 --- a/packages/app/src/keyboard/keyboard-action-dispatcher.ts +++ b/packages/app/src/keyboard/keyboard-action-dispatcher.ts @@ -9,12 +9,22 @@ export type KeyboardActionId = | "message-input.dictation-toggle" | "message-input.dictation-cancel" | "message-input.voice-toggle" - | "message-input.voice-mute-toggle"; + | "message-input.voice-mute-toggle" + | "workspace.tab.new" + | "workspace.tab.close-current" + | "workspace.tab.navigate-index" + | "workspace.tab.navigate-relative"; -export type KeyboardActionDefinition = { - id: KeyboardActionId; - scope: KeyboardActionScope; -}; +export type KeyboardActionDefinition = + | { id: "message-input.focus"; scope: KeyboardActionScope } + | { id: "message-input.dictation-toggle"; scope: KeyboardActionScope } + | { id: "message-input.dictation-cancel"; scope: KeyboardActionScope } + | { id: "message-input.voice-toggle"; scope: KeyboardActionScope } + | { id: "message-input.voice-mute-toggle"; scope: KeyboardActionScope } + | { id: "workspace.tab.new"; scope: KeyboardActionScope } + | { id: "workspace.tab.close-current"; scope: KeyboardActionScope } + | { id: "workspace.tab.navigate-index"; scope: KeyboardActionScope; index: number } + | { id: "workspace.tab.navigate-relative"; scope: KeyboardActionScope; delta: 1 | -1 }; export type KeyboardActionHandler = { handlerId: string; diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 1ab584f1b..5228c97c3 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -1,4 +1,5 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useIsFocused } from "@react-navigation/native"; import { ActivityIndicator, BackHandler, @@ -54,7 +55,8 @@ import { buildWorkspaceTabPersistenceKey, useWorkspaceTabsStore, } from "@/stores/workspace-tabs-store"; -import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store"; +import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; +import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher"; import { useCreateFlowStore } from "@/stores/create-flow-store"; import { buildWorkspaceOpenIntentParam, @@ -363,6 +365,7 @@ function WorkspaceScreenContent({ const isDarkMode = useColorScheme() === "dark"; const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0; const toast = useToast(); + const isScreenFocused = useIsFocused(); const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; @@ -651,12 +654,6 @@ function WorkspaceScreenContent({ const retargetWorkspaceTab = useWorkspaceTabsStore((state) => state.retargetTab); const reorderWorkspaceTabs = useWorkspaceTabsStore((state) => state.reorderTabs); const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId); - const workspaceTabActionRequest = useKeyboardShortcutsStore( - (state) => state.workspaceTabActionRequest - ); - const clearWorkspaceTabActionRequest = useKeyboardShortcutsStore( - (state) => state.clearWorkspaceTabActionRequest - ); const consumedOpenIntentsRef = useRef(new Set()); const pendingCloseTabIdsRef = useRef(new Set()); const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState(null); @@ -1386,59 +1383,56 @@ function WorkspaceScreenContent({ [handleBulkCloseTabs, tabs] ); - useEffect(() => { - if (!workspaceTabActionRequest) { - return; - } - if ( - workspaceTabActionRequest.serverId !== normalizedServerId || - workspaceTabActionRequest.workspaceId !== normalizedWorkspaceId - ) { - return; - } - - const request = workspaceTabActionRequest; - clearWorkspaceTabActionRequest(request.id); - - if (request.kind === "new") { - handleCreateDraftTab(); - return; - } - if (request.kind === "close-current") { - if (activeTabId) { - void handleCloseTabById(activeTabId); - } - return; - } - if (request.kind === "navigate-index") { - const next = tabs[request.index - 1] ?? null; - if (next?.tabId) { - navigateToTabId(next.tabId); - } - return; - } - if (request.kind === "navigate-relative") { - if (tabs.length > 0) { - const currentIndex = tabs.findIndex((tab) => tab.tabId === activeTabId); - const fromIndex = currentIndex >= 0 ? currentIndex : 0; - const nextIndex = (fromIndex + request.delta + tabs.length) % tabs.length; - const next = tabs[nextIndex] ?? null; - if (next?.tabId) { - navigateToTabId(next.tabId); + const handleWorkspaceTabAction = useCallback( + (action: KeyboardActionDefinition): boolean => { + switch (action.id) { + case "workspace.tab.new": + handleCreateDraftTab(); + return true; + case "workspace.tab.close-current": + if (activeTabId) { + void handleCloseTabById(activeTabId); + } + return true; + case "workspace.tab.navigate-index": { + const next = tabs[action.index - 1] ?? null; + if (next?.tabId) { + navigateToTabId(next.tabId); + } + return true; } + case "workspace.tab.navigate-relative": { + if (tabs.length > 0) { + const currentIndex = tabs.findIndex((tab) => tab.tabId === activeTabId); + const fromIndex = currentIndex >= 0 ? currentIndex : 0; + const nextIndex = (fromIndex + action.delta + tabs.length) % tabs.length; + const next = tabs[nextIndex] ?? null; + if (next?.tabId) { + navigateToTabId(next.tabId); + } + } + return true; + } + default: + return false; } - } - }, [ - activeTabId, - clearWorkspaceTabActionRequest, - handleCloseTabById, - handleCreateDraftTab, - navigateToTabId, - normalizedServerId, - normalizedWorkspaceId, - tabs, - workspaceTabActionRequest, - ]); + }, + [activeTabId, handleCloseTabById, handleCreateDraftTab, navigateToTabId, tabs] + ); + + useKeyboardActionHandler({ + handlerId: `workspace-tab-actions:${normalizedServerId}:${normalizedWorkspaceId}`, + actions: [ + "workspace.tab.new", + "workspace.tab.close-current", + "workspace.tab.navigate-index", + "workspace.tab.navigate-relative", + ] as const, + enabled: Boolean(normalizedServerId && normalizedWorkspaceId), + priority: 100, + isActive: () => isScreenFocused, + handle: handleWorkspaceTabAction, + }); const renderContent = () => { if ( @@ -1523,7 +1517,7 @@ function WorkspaceScreenContent({ ); } - return ( + return isScreenFocused ? ( + ) : ( + ); }; @@ -2069,6 +2065,11 @@ const styles = StyleSheet.create((theme) => ({ minHeight: 0, backgroundColor: theme.colors.surface0, }, + contentPlaceholder: { + flex: 1, + minHeight: 0, + backgroundColor: theme.colors.surface0, + }, emptyState: { flex: 1, alignItems: "center", diff --git a/packages/app/src/stores/keyboard-shortcuts-store.test.ts b/packages/app/src/stores/keyboard-shortcuts-store.test.ts index 097a8d408..f9680fde9 100644 --- a/packages/app/src/stores/keyboard-shortcuts-store.test.ts +++ b/packages/app/src/stores/keyboard-shortcuts-store.test.ts @@ -9,33 +9,13 @@ beforeEach(() => { cmdOrCtrlDown: false, sidebarShortcutWorkspaceTargets: [], visibleWorkspaceTargets: [], - workspaceTabActionRequest: null, - nextWorkspaceTabActionRequestId: 1, }); }); describe("keyboard-shortcuts-store", () => { - it("queues and clears workspace tab action requests", () => { - useKeyboardShortcutsStore.getState().requestWorkspaceTabAction({ - serverId: "srv-1", - workspaceId: "/repo/main", - kind: "navigate-index", - index: 3, - }); - - const request = useKeyboardShortcutsStore.getState().workspaceTabActionRequest; - expect(request).toEqual({ - id: 1, - serverId: "srv-1", - workspaceId: "/repo/main", - kind: "navigate-index", - index: 3, - }); - - useKeyboardShortcutsStore.getState().clearWorkspaceTabActionRequest(999); - expect(useKeyboardShortcutsStore.getState().workspaceTabActionRequest).not.toBeNull(); - - useKeyboardShortcutsStore.getState().clearWorkspaceTabActionRequest(1); - expect(useKeyboardShortcutsStore.getState().workspaceTabActionRequest).toBeNull(); + it("toggles command center open state", () => { + expect(useKeyboardShortcutsStore.getState().commandCenterOpen).toBe(false); + useKeyboardShortcutsStore.getState().setCommandCenterOpen(true); + expect(useKeyboardShortcutsStore.getState().commandCenterOpen).toBe(true); }); }); diff --git a/packages/app/src/stores/keyboard-shortcuts-store.ts b/packages/app/src/stores/keyboard-shortcuts-store.ts index bddc4f583..d1bd3515e 100644 --- a/packages/app/src/stores/keyboard-shortcuts-store.ts +++ b/packages/app/src/stores/keyboard-shortcuts-store.ts @@ -1,28 +1,6 @@ import { create } from "zustand"; import type { SidebarShortcutWorkspaceTarget } from "@/utils/sidebar-shortcuts"; -export type WorkspaceTabActionRequest = - | { - id: number; - serverId: string; - workspaceId: string; - kind: "new" | "close-current"; - } - | { - id: number; - serverId: string; - workspaceId: string; - kind: "navigate-index"; - index: number; - } - | { - id: number; - serverId: string; - workspaceId: string; - kind: "navigate-relative"; - delta: 1 | -1; - }; - interface KeyboardShortcutsState { commandCenterOpen: boolean; projectPickerOpen: boolean; @@ -33,8 +11,6 @@ interface KeyboardShortcutsState { sidebarShortcutWorkspaceTargets: SidebarShortcutWorkspaceTarget[]; /** All visible workspace targets in top-to-bottom visual order. */ visibleWorkspaceTargets: SidebarShortcutWorkspaceTarget[]; - workspaceTabActionRequest: WorkspaceTabActionRequest | null; - nextWorkspaceTabActionRequestId: number; setCommandCenterOpen: (open: boolean) => void; setProjectPickerOpen: (open: boolean) => void; @@ -44,30 +20,10 @@ interface KeyboardShortcutsState { setSidebarShortcutWorkspaceTargets: (targets: SidebarShortcutWorkspaceTarget[]) => void; setVisibleWorkspaceTargets: (targets: SidebarShortcutWorkspaceTarget[]) => void; resetModifiers: () => void; - - requestWorkspaceTabAction: (input: - | { - serverId: string; - workspaceId: string; - kind: "new" | "close-current"; - } - | { - serverId: string; - workspaceId: string; - kind: "navigate-index"; - index: number; - } - | { - serverId: string; - workspaceId: string; - kind: "navigate-relative"; - delta: 1 | -1; - }) => void; - clearWorkspaceTabActionRequest: (id: number) => void; } export const useKeyboardShortcutsStore = create( - (set, get) => ({ + (set) => ({ commandCenterOpen: false, projectPickerOpen: false, shortcutsDialogOpen: false, @@ -75,8 +31,6 @@ export const useKeyboardShortcutsStore = create( cmdOrCtrlDown: false, sidebarShortcutWorkspaceTargets: [], visibleWorkspaceTargets: [], - workspaceTabActionRequest: null, - nextWorkspaceTabActionRequestId: 1, setCommandCenterOpen: (open) => set({ commandCenterOpen: open }), setProjectPickerOpen: (open) => set({ projectPickerOpen: open }), @@ -87,23 +41,5 @@ export const useKeyboardShortcutsStore = create( set({ sidebarShortcutWorkspaceTargets: targets }), setVisibleWorkspaceTargets: (targets) => set({ visibleWorkspaceTargets: targets }), resetModifiers: () => set({ altDown: false, cmdOrCtrlDown: false }), - - requestWorkspaceTabAction: (input) => { - const id = get().nextWorkspaceTabActionRequestId; - set({ - workspaceTabActionRequest: { - ...input, - id, - } as WorkspaceTabActionRequest, - nextWorkspaceTabActionRequestId: id + 1, - }); - }, - clearWorkspaceTabActionRequest: (id) => { - const current = get().workspaceTabActionRequest; - if (!current || current.id !== id) { - return; - } - set({ workspaceTabActionRequest: null }); - }, }) ); diff --git a/packages/app/src/terminal/runtime/terminal-debug.ts b/packages/app/src/terminal/runtime/terminal-debug.ts deleted file mode 100644 index 8a7db84ad..000000000 --- a/packages/app/src/terminal/runtime/terminal-debug.ts +++ /dev/null @@ -1,77 +0,0 @@ -type TerminalDebugGlobal = { - __PASEO_TERMINAL_DEBUG?: boolean; -}; - -type TerminalDebugLogInput = { - scope: string; - event: string; - details?: Record; -}; - -function resolveGlobalDebugFlag(): boolean | null { - if (typeof globalThis === "undefined") { - return null; - } - - const value = (globalThis as TerminalDebugGlobal).__PASEO_TERMINAL_DEBUG; - if (typeof value === "boolean") { - return value; - } - return null; -} - -export function isTerminalDebugEnabled(): boolean { - const globalFlag = resolveGlobalDebugFlag(); - if (globalFlag !== null) { - return globalFlag; - } - return process.env.NODE_ENV === "development"; -} - -export function terminalDebugLog(input: TerminalDebugLogInput): void { - if (!isTerminalDebugEnabled()) { - return; - } - - const payload = input.details - ? { ...input.details, ts: Date.now() } - : { ts: Date.now() }; - console.log(`[terminal][${input.scope}] ${input.event}`, payload); -} - -function escapeControlBytes(input: { text: string }): string { - let output = ""; - for (const char of input.text) { - const code = char.charCodeAt(0); - if (code === 10) { - output += "\\n"; - continue; - } - if (code === 13) { - output += "\\r"; - continue; - } - if (code === 9) { - output += "\\t"; - continue; - } - if (code < 32 || code === 127) { - output += `\\x${code.toString(16).padStart(2, "0")}`; - continue; - } - output += char; - } - return output; -} - -export function summarizeTerminalText(input: { - text: string; - maxChars?: number; -}): string { - const maxChars = input.maxChars ?? 80; - const escaped = escapeControlBytes({ text: input.text }); - if (escaped.length <= maxChars) { - return escaped; - } - return `${escaped.slice(0, maxChars)}…`; -} diff --git a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts index 2d84d0b4a..3f161d5ca 100644 --- a/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts +++ b/packages/app/src/terminal/runtime/terminal-emulator-runtime.ts @@ -10,7 +10,6 @@ import { normalizeTerminalTransportKey, shouldInterceptDomTerminalKey, } from "@/utils/terminal-keys"; -import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug"; export type TerminalEmulatorRuntimeMountInput = { root: HTMLDivElement; @@ -126,13 +125,6 @@ export class TerminalEmulatorRuntime { } mount(input: TerminalEmulatorRuntimeMountInput): void { - terminalDebugLog({ - scope: "emulator-runtime", - event: "mount:start", - details: { - initialOutputLength: input.initialOutputText.length, - }, - }); this.unmount(); input.host.innerHTML = ""; @@ -219,15 +211,6 @@ export class TerminalEmulatorRuntime { } this.lastSize = { rows: nextRows, cols: nextCols }; - terminalDebugLog({ - scope: "emulator-runtime", - event: "resize:emit", - details: { - rows: nextRows, - cols: nextCols, - force, - }, - }); this.callbacks.onResize?.({ rows: nextRows, cols: nextCols, @@ -239,24 +222,8 @@ export class TerminalEmulatorRuntime { const inputDisposable = terminal.onData((data) => { if (this.suppressInput) { - terminalDebugLog({ - scope: "emulator-runtime", - event: "input:suppressed", - details: { - length: data.length, - preview: summarizeTerminalText({ text: data, maxChars: 64 }), - }, - }); return; } - terminalDebugLog({ - scope: "emulator-runtime", - event: "input:onData", - details: { - length: data.length, - preview: summarizeTerminalText({ text: data, maxChars: 64 }), - }, - }); this.callbacks.onInput?.(data); }); @@ -292,17 +259,6 @@ export class TerminalEmulatorRuntime { key: normalizeTerminalTransportKey(normalizedKey), ...modifiers, }); - terminalDebugLog({ - scope: "emulator-runtime", - event: "key:intercepted", - details: { - key: normalizedKey, - ctrl: modifiers.ctrl, - shift: modifiers.shift, - alt: modifiers.alt, - meta: modifiers.meta, - }, - }); if (this.pendingModifiers.ctrl || this.pendingModifiers.shift || this.pendingModifiers.alt) { this.callbacks.onPendingModifiersConsumed?.(); @@ -368,17 +324,6 @@ export class TerminalEmulatorRuntime { if (input.initialOutputText.length > 0) { this.write({ text: input.initialOutputText, suppressInput: true }); - terminalDebugLog({ - scope: "emulator-runtime", - event: "output:initial-write", - details: { - length: input.initialOutputText.length, - preview: summarizeTerminalText({ - text: input.initialOutputText, - maxChars: 96, - }), - }, - }); } this.processOutputQueue(); @@ -466,15 +411,6 @@ export class TerminalEmulatorRuntime { suppressInput: input.suppressInput ?? false, ...(input.onCommitted ? { onCommitted: input.onCommitted } : {}), }); - terminalDebugLog({ - scope: "emulator-runtime", - event: "output:enqueue", - details: { - chunkLength: input.text.length, - queueLength: this.outputOperations.length, - preview: summarizeTerminalText({ text: input.text, maxChars: 64 }), - }, - }); this.processOutputQueue(); } @@ -485,10 +421,6 @@ export class TerminalEmulatorRuntime { suppressInput: false, ...(input?.onCommitted ? { onCommitted: input.onCommitted } : {}), }); - terminalDebugLog({ - scope: "emulator-runtime", - event: "output:clear", - }); this.processOutputQueue(); } @@ -504,10 +436,6 @@ export class TerminalEmulatorRuntime { try { terminal.options.theme = input.theme; - terminalDebugLog({ - scope: "emulator-runtime", - event: "theme:update", - }); } catch { // ignore return; @@ -525,13 +453,6 @@ export class TerminalEmulatorRuntime { } unmount(): void { - terminalDebugLog({ - scope: "emulator-runtime", - event: "mount:unmount", - details: { - pendingWriteOperations: this.outputOperations.length, - }, - }); this.clearInFlightOutputTimeout(); const inFlightOperation = this.inFlightOutputOperation; this.inFlightOutputOperation = null; @@ -593,14 +514,6 @@ export class TerminalEmulatorRuntime { } const text = operation.text; - terminalDebugLog({ - scope: "emulator-runtime", - event: "output:flush", - details: { - length: text.length, - preview: summarizeTerminalText({ text, maxChars: 96 }), - }, - }); this.inFlightOutputOperationTimeout = setTimeout(() => { finalizeOperation(operation); }, OUTPUT_OPERATION_TIMEOUT_MS); diff --git a/packages/app/src/terminal/runtime/terminal-output-delivery-queue.ts b/packages/app/src/terminal/runtime/terminal-output-delivery-queue.ts index 140188934..51b0c290c 100644 --- a/packages/app/src/terminal/runtime/terminal-output-delivery-queue.ts +++ b/packages/app/src/terminal/runtime/terminal-output-delivery-queue.ts @@ -1,5 +1,3 @@ -import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug"; - export type TerminalOutputDeliveryChunk = { sequence: number; text: string; @@ -37,13 +35,6 @@ export class TerminalOutputDeliveryQueue { if (chunk.text.length === 0) { this.pendingChunks.length = 0; this.pendingChunks.push(chunk); - terminalDebugLog({ - scope: "output-delivery-queue", - event: "enqueue:clear", - details: { - sequence: chunk.sequence, - }, - }); this.tryDeliver(); return; } @@ -59,39 +50,14 @@ export class TerminalOutputDeliveryQueue { } else { this.pendingChunks.push(chunk); } - terminalDebugLog({ - scope: "output-delivery-queue", - event: "enqueue:text", - details: { - sequence: chunk.sequence, - pendingCount: this.pendingChunks.length, - textLength: chunk.text.length, - preview: summarizeTerminalText({ text: chunk.text, maxChars: 80 }), - }, - }); this.tryDeliver(); } consume(input: { sequence: number }): void { if (this.inFlightChunk?.sequence !== input.sequence) { - terminalDebugLog({ - scope: "output-delivery-queue", - event: "consume:ignored", - details: { - sequence: input.sequence, - inFlightSequence: this.inFlightChunk?.sequence ?? null, - }, - }); return; } - terminalDebugLog({ - scope: "output-delivery-queue", - event: "consume", - details: { - sequence: input.sequence, - }, - }); this.clearInFlightTimeout(); this.inFlightChunk = null; this.tryDeliver(); @@ -114,14 +80,6 @@ export class TerminalOutputDeliveryQueue { } this.inFlightChunk = nextChunk; - terminalDebugLog({ - scope: "output-delivery-queue", - event: "deliver:start", - details: { - sequence: nextChunk.sequence, - pendingCount: this.pendingChunks.length, - }, - }); this.deliverInFlightChunk(); } @@ -136,15 +94,6 @@ export class TerminalOutputDeliveryQueue { if (!this.inFlightChunk) { return; } - terminalDebugLog({ - scope: "output-delivery-queue", - event: "deliver:timeout-retry", - details: { - sequence: this.inFlightChunk.sequence, - pendingCount: this.pendingChunks.length, - timeoutMs: this.deliveryTimeoutMs, - }, - }); this.deliverInFlightChunk(); }, this.deliveryTimeoutMs); diff --git a/packages/app/src/terminal/runtime/terminal-output-pump.ts b/packages/app/src/terminal/runtime/terminal-output-pump.ts index f1834005c..22b80abfe 100644 --- a/packages/app/src/terminal/runtime/terminal-output-pump.ts +++ b/packages/app/src/terminal/runtime/terminal-output-pump.ts @@ -4,7 +4,6 @@ import { readTerminalOutputBuffer, type TerminalOutputBuffer, } from "@/utils/terminal-output-buffer"; -import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug"; export type TerminalOutputChunk = { sequence: number; @@ -54,14 +53,6 @@ export class TerminalOutputPump { return; } - terminalDebugLog({ - scope: "output-pump", - event: "selected-terminal:set", - details: { - previousTerminalId: this.selectedTerminalId, - nextTerminalId: input.terminalId, - }, - }); this.clearSelectedChunkFlushTimer(); this.selectedChunkAccumulator = ""; this.selectedChunkAccumulatorReplay = null; @@ -102,17 +93,6 @@ export class TerminalOutputPump { } this.selectedChunkAccumulator += input.text; - terminalDebugLog({ - scope: "output-pump", - event: "selected-terminal:accumulate", - details: { - terminalId: input.terminalId, - appendedLength: input.text.length, - accumulatorLength: this.selectedChunkAccumulator.length, - replay: input.replay, - preview: summarizeTerminalText({ text: input.text, maxChars: 80 }), - }, - }); this.scheduleSelectedChunkFlush(); } @@ -173,16 +153,6 @@ export class TerminalOutputPump { const replay = this.selectedChunkAccumulatorReplay ?? false; this.selectedChunkAccumulator = ""; this.selectedChunkAccumulatorReplay = null; - terminalDebugLog({ - scope: "output-pump", - event: "selected-terminal:flush", - details: { - terminalId: this.selectedTerminalId, - textLength: text.length, - replay, - preview: summarizeTerminalText({ text, maxChars: 96 }), - }, - }); this.emitSelectedChunk({ text, replay }); } diff --git a/packages/app/src/terminal/runtime/terminal-output-session.ts b/packages/app/src/terminal/runtime/terminal-output-session.ts index 5fa31148a..fc09e3d52 100644 --- a/packages/app/src/terminal/runtime/terminal-output-session.ts +++ b/packages/app/src/terminal/runtime/terminal-output-session.ts @@ -6,7 +6,6 @@ import { TerminalOutputDeliveryQueue, type TerminalOutputDeliveryChunk, } from "./terminal-output-delivery-queue"; -import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug"; type TerminalOutputState = { selectedTerminalId: string | null; @@ -75,18 +74,6 @@ export class TerminalOutputSession { chunkSequence: chunk.sequence, chunkReplay: chunk.replay, }; - terminalDebugLog({ - scope: "output-session", - event: "deliver", - details: { - selectedTerminalId: this.state.selectedTerminalId, - sequence: chunk.sequence, - replay: chunk.replay, - chunkLength: chunk.text.length, - snapshotLength: snapshotText.length, - preview: summarizeTerminalText({ text: chunk.text, maxChars: 80 }), - }, - }); this.emit(); }, }); @@ -121,14 +108,6 @@ export class TerminalOutputSession { chunkSequence: 0, chunkReplay: false, }; - terminalDebugLog({ - scope: "output-session", - event: "selected-terminal:set", - details: { - selectedTerminalId: input.terminalId, - snapshotLength: snapshotText.length, - }, - }); this.emit(); } @@ -154,13 +133,6 @@ export class TerminalOutputSession { chunkSequence: 0, chunkReplay: false, }; - terminalDebugLog({ - scope: "output-session", - event: "selected-terminal:clear", - details: { - terminalId: input.terminalId, - }, - }); this.emit(); } @@ -208,14 +180,6 @@ export function releaseTerminalOutputSession(input: { scopeKey: string }): void if (current <= 1) { sessionRefCountByScopeKey.delete(input.scopeKey); sessionsByScopeKey.delete(input.scopeKey); - terminalDebugLog({ - scope: "output-session", - event: "scope:release", - details: { - scopeKey: input.scopeKey, - released: true, - }, - }); return; } diff --git a/packages/app/src/terminal/runtime/terminal-stream-controller.ts b/packages/app/src/terminal/runtime/terminal-stream-controller.ts index 7920863e8..92de68525 100644 --- a/packages/app/src/terminal/runtime/terminal-stream-controller.ts +++ b/packages/app/src/terminal/runtime/terminal-stream-controller.ts @@ -7,7 +7,6 @@ import { waitForDuration, withPromiseTimeout, } from "@/utils/terminal-attach"; -import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug"; export type TerminalStreamControllerAttachPayload = { streamId: number | null; @@ -114,14 +113,6 @@ export class TerminalStreamController { if (this.isDisposed) { return; } - terminalDebugLog({ - scope: "stream-controller", - event: "terminal:set", - details: { - previousTerminalId: this.selectedTerminalId, - nextTerminalId: input.terminalId, - }, - }); const nextTerminalId = input.terminalId; const previousTerminalId = this.selectedTerminalId; @@ -166,14 +157,6 @@ export class TerminalStreamController { if (this.isDisposed) { return; } - terminalDebugLog({ - scope: "stream-controller", - event: "stream:exit", - details: { - terminalId: input.terminalId, - streamId: input.streamId, - }, - }); if (this.selectedTerminalId !== input.terminalId) { return; @@ -256,16 +239,6 @@ export class TerminalStreamController { if (!this.isAttachGenerationCurrent({ generation: input.generation, terminalId: input.terminalId })) { return; } - terminalDebugLog({ - scope: "stream-controller", - event: "attach:attempt", - details: { - terminalId: input.terminalId, - generation: input.generation, - attempt, - maxAttachAttempts, - }, - }); try { const preferredSize = this.options.getPreferredSize(); @@ -299,15 +272,6 @@ export class TerminalStreamController { if (attachPayload.error || typeof attachPayload.streamId !== "number") { lastErrorMessage = attachPayload.error ?? "Unable to attach terminal stream"; - terminalDebugLog({ - scope: "stream-controller", - event: "attach:response-error", - details: { - terminalId: input.terminalId, - attempt, - error: lastErrorMessage, - }, - }); const hasRemainingAttempts = attempt < maxAttachAttempts - 1; if (hasRemainingAttempts && isRetryableError({ message: lastErrorMessage })) { await waitForDelay({ durationMs: getRetryDelayMs({ attempt }) }); @@ -385,19 +349,6 @@ export class TerminalStreamController { } else { unsubscribe(); } - terminalDebugLog({ - scope: "stream-controller", - event: "attach:success", - details: { - terminalId: input.terminalId, - streamId, - replayedFrom: attachPayload.replayedFrom ?? null, - requestedResumeOffset: resumeOffset ?? null, - currentOffset: attachPayload.currentOffset, - reset: attachPayload.reset, - bootstrapReset: shouldResetForReplayBootstrap, - }, - }); this.updateStatus({ terminalId: input.terminalId, streamId, @@ -408,15 +359,6 @@ export class TerminalStreamController { } catch (error) { lastErrorMessage = error instanceof Error ? error.message : "Unable to attach terminal stream"; - terminalDebugLog({ - scope: "stream-controller", - event: "attach:exception", - details: { - terminalId: input.terminalId, - attempt, - error: lastErrorMessage, - }, - }); const hasRemainingAttempts = attempt < maxAttachAttempts - 1; if (hasRemainingAttempts && isRetryableError({ message: lastErrorMessage })) { await waitForDelay({ durationMs: getRetryDelayMs({ attempt }) }); @@ -510,21 +452,6 @@ export class TerminalStreamController { if (text.length === 0) { return; } - terminalDebugLog({ - scope: "stream-controller", - event: "stream:chunk", - details: { - terminalId: input.terminalId, - streamId: input.streamId, - offset: chunkOffset, - endOffset: chunkEndOffset, - replay: Boolean(input.chunk.replay), - byteLength: input.chunk.data.byteLength, - textLength: text.length, - preview: summarizeTerminalText({ text, maxChars: 96 }), - }, - }); - this.options.onChunk({ terminalId: input.terminalId, text, @@ -577,15 +504,6 @@ export class TerminalStreamController { if (!activeStream) { return; } - terminalDebugLog({ - scope: "stream-controller", - event: "stream:detach", - details: { - terminalId: activeStream.terminalId, - streamId: activeStream.streamId, - shouldDetach: input.shouldDetach, - }, - }); this.activeStream = null; try { @@ -633,16 +551,6 @@ export class TerminalStreamController { private updateStatus(status: TerminalStreamControllerStatus): void { this.status = status; - terminalDebugLog({ - scope: "stream-controller", - event: "status:update", - details: { - terminalId: status.terminalId, - streamId: status.streamId, - isAttaching: status.isAttaching, - error: status.error, - }, - }); this.options.onStatusChange?.(status); } diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index b47154956..469c03702 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2629,7 +2629,7 @@ dependencies = [ [[package]] name = "paseo" -version = "0.1.27" +version = "0.1.28" dependencies = [ "base64 0.22.1", "dirs",