refactor: migrate workspace tab actions to keyboard action dispatcher
This commit is contained in:
@@ -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({
|
||||
</View>
|
||||
{isMobile && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
|
||||
<DownloadToast />
|
||||
<UpdateBanner />
|
||||
<CommandCenter />
|
||||
<ProjectPickerModal />
|
||||
<KeyboardShortcutsDialog />
|
||||
@@ -578,7 +580,12 @@ export default function RootLayout() {
|
||||
>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="settings" />
|
||||
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
|
||||
<Stack.Screen
|
||||
name="h/[serverId]/workspace/[workspaceId]"
|
||||
getId={({ params }) =>
|
||||
`${params?.serverId}:${params?.workspaceId}`
|
||||
}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="h/[serverId]/agent/[agentId]"
|
||||
options={{ gestureEnabled: false }}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type MutableRefObject,
|
||||
} from 'react'
|
||||
import { router, usePathname } from 'expo-router'
|
||||
import { navigateToWorkspace } from '@/hooks/use-workspace-navigation'
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
||||
import { type GestureType } from 'react-native-gesture-handler'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
@@ -1093,7 +1094,7 @@ function ProjectBlock({
|
||||
return
|
||||
}
|
||||
onWorkspacePress?.()
|
||||
router.replace(workspaceRoute as any)
|
||||
navigateToWorkspace(serverId, item.workspaceId)
|
||||
}}
|
||||
drag={input?.drag ?? (() => {})}
|
||||
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}
|
||||
|
||||
@@ -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<string> = [
|
||||
@@ -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();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
<View style={styles.outputContainer}>
|
||||
{selectedTerminal ? (
|
||||
{selectedTerminal && isScreenFocused ? (
|
||||
<View style={styles.terminalGestureContainer}>
|
||||
<TerminalEmulator
|
||||
dom={{
|
||||
@@ -1084,13 +1034,15 @@ export function TerminalPane({
|
||||
resizeRequestToken={resizeRequestToken}
|
||||
/>
|
||||
</View>
|
||||
) : selectedTerminal ? (
|
||||
<View style={styles.terminalGestureContainer} />
|
||||
) : (
|
||||
<View style={styles.centerState}>
|
||||
<Text style={styles.stateText}>No terminal selected</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isAttaching ? (
|
||||
{isAttaching && isScreenFocused ? (
|
||||
<View
|
||||
style={styles.attachOverlay}
|
||||
pointerEvents="none"
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { X } from "lucide-react-native";
|
||||
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
|
||||
import { openExternalUrl } from "@/utils/open-external-url";
|
||||
|
||||
const CHECK_INTERVAL_MS = 30 * 60 * 1000;
|
||||
const CHANGELOG_URL = "https://paseo.sh/changelog";
|
||||
|
||||
export function UpdateBanner() {
|
||||
const { theme } = useUnistyles();
|
||||
const {
|
||||
isDesktop,
|
||||
status,
|
||||
availableUpdate,
|
||||
checkForUpdates,
|
||||
installUpdate,
|
||||
isInstalling,
|
||||
} = useDesktopAppUpdater();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | 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 (
|
||||
<View style={styles.container} pointerEvents="box-none">
|
||||
<View style={styles.banner}>
|
||||
<Pressable
|
||||
onPress={() => setDismissed(true)}
|
||||
hitSlop={8}
|
||||
style={styles.closeButton}
|
||||
>
|
||||
<X size={14} color={theme.colors.foregroundMuted} />
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.textSection}>
|
||||
<Text style={styles.title}>
|
||||
{isInstalled ? "Update installed" : "Update available"}
|
||||
</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{isInstalled
|
||||
? "Restart to use the new version."
|
||||
: `${availableUpdate?.latestVersion ? `v${availableUpdate.latestVersion.replace(/^v/i, "")} is ready` : "A new version is ready"} to install.`}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<Pressable
|
||||
onPress={() => void openExternalUrl(CHANGELOG_URL)}
|
||||
style={({ pressed }) => [
|
||||
styles.outlineButton,
|
||||
pressed && styles.buttonPressed,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.outlineButtonText}>What's new</Text>
|
||||
</Pressable>
|
||||
|
||||
{!isInstalled && (
|
||||
<Pressable
|
||||
onPress={() => void installUpdate()}
|
||||
disabled={isInstalling}
|
||||
style={({ pressed }) => [
|
||||
styles.primaryButton,
|
||||
pressed && styles.buttonPressed,
|
||||
isInstalling && styles.buttonDisabled,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.primaryButtonText}>
|
||||
{isInstalling ? "Installing..." : "Install & restart"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}));
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
[]
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string>());
|
||||
const pendingCloseTabIdsRef = useRef(new Set<string>());
|
||||
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(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 ? (
|
||||
<TerminalPane
|
||||
serverId={normalizedServerId}
|
||||
cwd={normalizedWorkspaceId}
|
||||
@@ -1544,6 +1538,8 @@ function WorkspaceScreenContent({
|
||||
hideHeader
|
||||
manageTerminalDirectorySubscription={false}
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.contentPlaceholder} />
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<KeyboardShortcutsState>(
|
||||
(set, get) => ({
|
||||
(set) => ({
|
||||
commandCenterOpen: false,
|
||||
projectPickerOpen: false,
|
||||
shortcutsDialogOpen: false,
|
||||
@@ -75,8 +31,6 @@ export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>(
|
||||
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<KeyboardShortcutsState>(
|
||||
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 });
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
type TerminalDebugGlobal = {
|
||||
__PASEO_TERMINAL_DEBUG?: boolean;
|
||||
};
|
||||
|
||||
type TerminalDebugLogInput = {
|
||||
scope: string;
|
||||
event: string;
|
||||
details?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
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)}…`;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -2629,7 +2629,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "paseo"
|
||||
version = "0.1.27"
|
||||
version = "0.1.28"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"dirs",
|
||||
|
||||
Reference in New Issue
Block a user