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 * as Notifications from "expo-notifications";
|
||||||
import { LeftSidebar } from "@/components/left-sidebar";
|
import { LeftSidebar } from "@/components/left-sidebar";
|
||||||
import { DownloadToast } from "@/components/download-toast";
|
import { DownloadToast } from "@/components/download-toast";
|
||||||
|
import { UpdateBanner } from "@/desktop/updates/update-banner";
|
||||||
import { ToastProvider } from "@/contexts/toast-context";
|
import { ToastProvider } from "@/contexts/toast-context";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { runOnJS, interpolate, Extrapolation, useSharedValue } from "react-native-reanimated";
|
import { runOnJS, interpolate, Extrapolation, useSharedValue } from "react-native-reanimated";
|
||||||
@@ -379,6 +380,7 @@ function AppContainer({
|
|||||||
</View>
|
</View>
|
||||||
{isMobile && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
|
{isMobile && chromeEnabled && <LeftSidebar selectedAgentId={selectedAgentId} />}
|
||||||
<DownloadToast />
|
<DownloadToast />
|
||||||
|
<UpdateBanner />
|
||||||
<CommandCenter />
|
<CommandCenter />
|
||||||
<ProjectPickerModal />
|
<ProjectPickerModal />
|
||||||
<KeyboardShortcutsDialog />
|
<KeyboardShortcutsDialog />
|
||||||
@@ -578,7 +580,12 @@ export default function RootLayout() {
|
|||||||
>
|
>
|
||||||
<Stack.Screen name="index" />
|
<Stack.Screen name="index" />
|
||||||
<Stack.Screen name="settings" />
|
<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
|
<Stack.Screen
|
||||||
name="h/[serverId]/agent/[agentId]"
|
name="h/[serverId]/agent/[agentId]"
|
||||||
options={{ gestureEnabled: false }}
|
options={{ gestureEnabled: false }}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
type MutableRefObject,
|
type MutableRefObject,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { router, usePathname } from 'expo-router'
|
import { router, usePathname } from 'expo-router'
|
||||||
|
import { navigateToWorkspace } from '@/hooks/use-workspace-navigation'
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
||||||
import { type GestureType } from 'react-native-gesture-handler'
|
import { type GestureType } from 'react-native-gesture-handler'
|
||||||
import * as Clipboard from 'expo-clipboard'
|
import * as Clipboard from 'expo-clipboard'
|
||||||
@@ -1093,7 +1094,7 @@ function ProjectBlock({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
onWorkspacePress?.()
|
onWorkspacePress?.()
|
||||||
router.replace(workspaceRoute as any)
|
navigateToWorkspace(serverId, item.workspaceId)
|
||||||
}}
|
}}
|
||||||
drag={input?.drag ?? (() => {})}
|
drag={input?.drag ?? (() => {})}
|
||||||
isDragging={input?.isDragging ?? false}
|
isDragging={input?.isDragging ?? false}
|
||||||
@@ -1147,7 +1148,7 @@ function ProjectBlock({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
onWorkspacePress?.()
|
onWorkspacePress?.()
|
||||||
router.replace(buildHostWorkspaceRoute(serverId, flattenedWorkspace.workspaceId) as any)
|
navigateToWorkspace(serverId, flattenedWorkspace.workspaceId)
|
||||||
}}
|
}}
|
||||||
drag={drag}
|
drag={drag}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ import type { ITheme } from "@xterm/xterm";
|
|||||||
import type { PendingTerminalModifiers } from "../utils/terminal-keys";
|
import type { PendingTerminalModifiers } from "../utils/terminal-keys";
|
||||||
import { TerminalEmulatorRuntime } from "../terminal/runtime/terminal-emulator-runtime";
|
import { TerminalEmulatorRuntime } from "../terminal/runtime/terminal-emulator-runtime";
|
||||||
import { focusWithRetries } from "../utils/web-focus";
|
import { focusWithRetries } from "../utils/web-focus";
|
||||||
import {
|
|
||||||
summarizeTerminalText,
|
|
||||||
terminalDebugLog,
|
|
||||||
} from "../terminal/runtime/terminal-debug";
|
|
||||||
|
|
||||||
function buildXtermThemeKey(theme: ITheme): string {
|
function buildXtermThemeKey(theme: ITheme): string {
|
||||||
const values: Array<string> = [
|
const values: Array<string> = [
|
||||||
@@ -287,14 +283,6 @@ export default function TerminalEmulator({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (outputChunkSequence <= appliedChunkSequenceRef.current) {
|
if (outputChunkSequence <= appliedChunkSequenceRef.current) {
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-component",
|
|
||||||
event: "output:chunk:skip-duplicate",
|
|
||||||
details: {
|
|
||||||
sequence: outputChunkSequence,
|
|
||||||
lastAppliedSequence: appliedChunkSequenceRef.current,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
onOutputChunkConsumed?.(outputChunkSequence);
|
onOutputChunkConsumed?.(outputChunkSequence);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -307,13 +295,6 @@ export default function TerminalEmulator({
|
|||||||
appliedChunkSequenceRef.current = outputChunkSequence;
|
appliedChunkSequenceRef.current = outputChunkSequence;
|
||||||
|
|
||||||
if (outputChunkText.length === 0) {
|
if (outputChunkText.length === 0) {
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-component",
|
|
||||||
event: "output:chunk:clear",
|
|
||||||
details: {
|
|
||||||
sequence: outputChunkSequence,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
runtime.clear({
|
runtime.clear({
|
||||||
onCommitted: () => {
|
onCommitted: () => {
|
||||||
onOutputChunkConsumed?.(outputChunkSequence);
|
onOutputChunkConsumed?.(outputChunkSequence);
|
||||||
@@ -321,16 +302,6 @@ export default function TerminalEmulator({
|
|||||||
});
|
});
|
||||||
return;
|
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({
|
runtime.write({
|
||||||
text: outputChunkText,
|
text: outputChunkText,
|
||||||
suppressInput: outputChunkReplay,
|
suppressInput: outputChunkReplay,
|
||||||
@@ -383,10 +354,6 @@ export default function TerminalEmulator({
|
|||||||
touchAction: "pan-y",
|
touchAction: "pan-y",
|
||||||
}}
|
}}
|
||||||
onPointerDown={() => {
|
onPointerDown={() => {
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-component",
|
|
||||||
event: "surface:pointer-down-focus",
|
|
||||||
});
|
|
||||||
runtimeRef.current?.focus();
|
runtimeRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -35,10 +35,6 @@ import {
|
|||||||
TerminalStreamController,
|
TerminalStreamController,
|
||||||
type TerminalStreamControllerStatus,
|
type TerminalStreamControllerStatus,
|
||||||
} from "@/terminal/runtime/terminal-stream-controller";
|
} from "@/terminal/runtime/terminal-stream-controller";
|
||||||
import {
|
|
||||||
summarizeTerminalText,
|
|
||||||
terminalDebugLog,
|
|
||||||
} from "@/terminal/runtime/terminal-debug";
|
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { toXtermTheme } from "@/utils/to-xterm-theme";
|
import { toXtermTheme } from "@/utils/to-xterm-theme";
|
||||||
import TerminalEmulator from "./terminal-emulator";
|
import TerminalEmulator from "./terminal-emulator";
|
||||||
@@ -366,7 +362,7 @@ export function TerminalPane({
|
|||||||
const terminals = terminalsQuery.data?.terminals ?? [];
|
const terminals = terminalsQuery.data?.terminals ?? [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!client || !isConnected) {
|
if (!client || !isConnected || !isScreenFocused) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -716,17 +712,6 @@ export function TerminalPane({
|
|||||||
meta: input.meta,
|
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)) {
|
if (!dispatchTerminalInputEntry(pendingEntry)) {
|
||||||
enqueuePendingTerminalInput(pendingEntry);
|
enqueuePendingTerminalInput(pendingEntry);
|
||||||
}
|
}
|
||||||
@@ -745,16 +730,6 @@ export function TerminalPane({
|
|||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return;
|
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)) {
|
if (hasPendingTerminalModifiers(modifiers)) {
|
||||||
const pendingResolution = resolvePendingModifierDataInput({
|
const pendingResolution = resolvePendingModifierDataInput({
|
||||||
@@ -788,15 +763,6 @@ export function TerminalPane({
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminalDebugLog({
|
|
||||||
scope: "terminal-pane",
|
|
||||||
event: "input:data:send",
|
|
||||||
details: {
|
|
||||||
length: data.length,
|
|
||||||
preview: summarizeTerminalText({ text: data, maxChars: 80 }),
|
|
||||||
activeStreamId: currentStreamId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const pendingEntry: PendingTerminalInput = {
|
const pendingEntry: PendingTerminalInput = {
|
||||||
type: "data",
|
type: "data",
|
||||||
data,
|
data,
|
||||||
@@ -835,15 +801,6 @@ export function TerminalPane({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastReportedSizeRef.current = { rows: normalizedRows, cols: normalizedCols };
|
lastReportedSizeRef.current = { rows: normalizedRows, cols: normalizedCols };
|
||||||
terminalDebugLog({
|
|
||||||
scope: "terminal-pane",
|
|
||||||
event: "display:resize:send",
|
|
||||||
details: {
|
|
||||||
terminalId: selectedTerminalId,
|
|
||||||
rows: normalizedRows,
|
|
||||||
cols: normalizedCols,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
client.sendTerminalInput(selectedTerminalId, {
|
client.sendTerminalInput(selectedTerminalId, {
|
||||||
type: "resize",
|
type: "resize",
|
||||||
rows: normalizedRows,
|
rows: normalizedRows,
|
||||||
@@ -871,13 +828,6 @@ export function TerminalPane({
|
|||||||
}, [clearPendingModifiers]);
|
}, [clearPendingModifiers]);
|
||||||
|
|
||||||
const handleOutputChunkConsumed = useCallback((sequence: number) => {
|
const handleOutputChunkConsumed = useCallback((sequence: number) => {
|
||||||
terminalDebugLog({
|
|
||||||
scope: "terminal-pane",
|
|
||||||
event: "output:chunk:consumed",
|
|
||||||
details: {
|
|
||||||
sequence,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
outputSession.consume({ sequence });
|
outputSession.consume({ sequence });
|
||||||
}, [outputSession]);
|
}, [outputSession]);
|
||||||
|
|
||||||
@@ -1040,7 +990,7 @@ export function TerminalPane({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View style={styles.outputContainer}>
|
<View style={styles.outputContainer}>
|
||||||
{selectedTerminal ? (
|
{selectedTerminal && isScreenFocused ? (
|
||||||
<View style={styles.terminalGestureContainer}>
|
<View style={styles.terminalGestureContainer}>
|
||||||
<TerminalEmulator
|
<TerminalEmulator
|
||||||
dom={{
|
dom={{
|
||||||
@@ -1084,13 +1034,15 @@ export function TerminalPane({
|
|||||||
resizeRequestToken={resizeRequestToken}
|
resizeRequestToken={resizeRequestToken}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
) : selectedTerminal ? (
|
||||||
|
<View style={styles.terminalGestureContainer} />
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
<Text style={styles.stateText}>No terminal selected</Text>
|
<Text style={styles.stateText}>No terminal selected</Text>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAttaching ? (
|
{isAttaching && isScreenFocused ? (
|
||||||
<View
|
<View
|
||||||
style={styles.attachOverlay}
|
style={styles.attachOverlay}
|
||||||
pointerEvents="none"
|
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 { useEffect } from "react";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
import { usePathname, useRouter } from "expo-router";
|
import { usePathname } from "expo-router";
|
||||||
import { getIsTauri } from "@/constants/layout";
|
import { getIsTauri } from "@/constants/layout";
|
||||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||||
import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
|
import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
|
||||||
import {
|
import {
|
||||||
buildHostWorkspaceRoute,
|
|
||||||
parseHostAgentRouteFromPathname,
|
parseHostAgentRouteFromPathname,
|
||||||
parseHostWorkspaceRouteFromPathname,
|
parseHostWorkspaceRouteFromPathname,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
|
import { navigateToWorkspace } from "@/hooks/use-workspace-navigation";
|
||||||
import {
|
import {
|
||||||
type MessageInputKeyboardActionKind,
|
type MessageInputKeyboardActionKind,
|
||||||
type KeyboardShortcutPayload,
|
type KeyboardShortcutPayload,
|
||||||
@@ -32,7 +32,6 @@ export function useKeyboardShortcuts({
|
|||||||
selectedAgentId?: string;
|
selectedAgentId?: string;
|
||||||
toggleFileExplorer?: () => void;
|
toggleFileExplorer?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const resetModifiers = useKeyboardShortcutsStore((s) => s.resetModifiers);
|
const resetModifiers = useKeyboardShortcutsStore((s) => s.resetModifiers);
|
||||||
|
|
||||||
@@ -57,11 +56,7 @@ export function useKeyboardShortcuts({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldReplace =
|
navigateToWorkspace(target.serverId, target.workspaceId);
|
||||||
Boolean(parseHostWorkspaceRouteFromPathname(pathname)) ||
|
|
||||||
Boolean(parseHostAgentRouteFromPathname(pathname));
|
|
||||||
const navigate = shouldReplace ? router.replace : router.push;
|
|
||||||
navigate(buildHostWorkspaceRoute(target.serverId, target.workspaceId) as any);
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const navigateRelativeWorkspace = (delta: 1 | -1): boolean => {
|
const navigateRelativeWorkspace = (delta: 1 | -1): boolean => {
|
||||||
@@ -77,7 +72,7 @@ export function useKeyboardShortcuts({
|
|||||||
if (!fallback) {
|
if (!fallback) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
router.push(buildHostWorkspaceRoute(fallback.serverId, fallback.workspaceId) as any);
|
navigateToWorkspace(fallback.serverId, fallback.workspaceId);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +87,7 @@ export function useKeyboardShortcuts({
|
|||||||
if (!target) {
|
if (!target) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
router.replace(buildHostWorkspaceRoute(target.serverId, target.workspaceId) as any);
|
navigateToWorkspace(target.serverId, target.workspaceId);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,22 +129,6 @@ export function useKeyboardShortcuts({
|
|||||||
return false;
|
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: {
|
const handleAction = (input: {
|
||||||
action: string;
|
action: string;
|
||||||
payload: KeyboardShortcutPayload;
|
payload: KeyboardShortcutPayload;
|
||||||
@@ -159,23 +138,31 @@ export function useKeyboardShortcuts({
|
|||||||
case "agent.new":
|
case "agent.new":
|
||||||
return openProjectPicker();
|
return openProjectPicker();
|
||||||
case "workspace.tab.new":
|
case "workspace.tab.new":
|
||||||
return requestWorkspaceTabAction({ kind: "new" });
|
return keyboardActionDispatcher.dispatch({
|
||||||
|
id: "workspace.tab.new",
|
||||||
|
scope: "workspace",
|
||||||
|
});
|
||||||
case "workspace.tab.close.current":
|
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":
|
case "workspace.tab.navigate.index":
|
||||||
if (!input.payload || typeof input.payload !== "object" || !("index" in input.payload)) {
|
if (!input.payload || typeof input.payload !== "object" || !("index" in input.payload)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return requestWorkspaceTabAction({
|
return keyboardActionDispatcher.dispatch({
|
||||||
kind: "navigate-index",
|
id: "workspace.tab.navigate-index",
|
||||||
|
scope: "workspace",
|
||||||
index: input.payload.index,
|
index: input.payload.index,
|
||||||
});
|
});
|
||||||
case "workspace.tab.navigate.relative":
|
case "workspace.tab.navigate.relative":
|
||||||
if (!input.payload || typeof input.payload !== "object" || !("delta" in input.payload)) {
|
if (!input.payload || typeof input.payload !== "object" || !("delta" in input.payload)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return requestWorkspaceTabAction({
|
return keyboardActionDispatcher.dispatch({
|
||||||
kind: "navigate-relative",
|
id: "workspace.tab.navigate-relative",
|
||||||
|
scope: "workspace",
|
||||||
delta: input.payload.delta,
|
delta: input.payload.delta,
|
||||||
});
|
});
|
||||||
case "workspace.navigate.index":
|
case "workspace.navigate.index":
|
||||||
@@ -326,7 +313,6 @@ export function useKeyboardShortcuts({
|
|||||||
isMobile,
|
isMobile,
|
||||||
pathname,
|
pathname,
|
||||||
resetModifiers,
|
resetModifiers,
|
||||||
router,
|
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
toggleAgentList,
|
toggleAgentList,
|
||||||
toggleFileExplorer,
|
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-toggle"
|
||||||
| "message-input.dictation-cancel"
|
| "message-input.dictation-cancel"
|
||||||
| "message-input.voice-toggle"
|
| "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 = {
|
export type KeyboardActionDefinition =
|
||||||
id: KeyboardActionId;
|
| { id: "message-input.focus"; scope: KeyboardActionScope }
|
||||||
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 = {
|
export type KeyboardActionHandler = {
|
||||||
handlerId: string;
|
handlerId: string;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { useIsFocused } from "@react-navigation/native";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
@@ -54,7 +55,8 @@ import {
|
|||||||
buildWorkspaceTabPersistenceKey,
|
buildWorkspaceTabPersistenceKey,
|
||||||
useWorkspaceTabsStore,
|
useWorkspaceTabsStore,
|
||||||
} from "@/stores/workspace-tabs-store";
|
} 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 { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||||
import {
|
import {
|
||||||
buildWorkspaceOpenIntentParam,
|
buildWorkspaceOpenIntentParam,
|
||||||
@@ -363,6 +365,7 @@ function WorkspaceScreenContent({
|
|||||||
const isDarkMode = useColorScheme() === "dark";
|
const isDarkMode = useColorScheme() === "dark";
|
||||||
const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0;
|
const mainBackgroundColor = isDarkMode ? theme.colors.surface1 : theme.colors.surface0;
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const isScreenFocused = useIsFocused();
|
||||||
const isMobile =
|
const isMobile =
|
||||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
|
|
||||||
@@ -651,12 +654,6 @@ function WorkspaceScreenContent({
|
|||||||
const retargetWorkspaceTab = useWorkspaceTabsStore((state) => state.retargetTab);
|
const retargetWorkspaceTab = useWorkspaceTabsStore((state) => state.retargetTab);
|
||||||
const reorderWorkspaceTabs = useWorkspaceTabsStore((state) => state.reorderTabs);
|
const reorderWorkspaceTabs = useWorkspaceTabsStore((state) => state.reorderTabs);
|
||||||
const pendingByDraftId = useCreateFlowStore((state) => state.pendingByDraftId);
|
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 consumedOpenIntentsRef = useRef(new Set<string>());
|
||||||
const pendingCloseTabIdsRef = useRef(new Set<string>());
|
const pendingCloseTabIdsRef = useRef(new Set<string>());
|
||||||
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
|
const [resolvedOpenIntentKey, setResolvedOpenIntentKey] = useState<string | null>(null);
|
||||||
@@ -1386,59 +1383,56 @@ function WorkspaceScreenContent({
|
|||||||
[handleBulkCloseTabs, tabs]
|
[handleBulkCloseTabs, tabs]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleWorkspaceTabAction = useCallback(
|
||||||
if (!workspaceTabActionRequest) {
|
(action: KeyboardActionDefinition): boolean => {
|
||||||
return;
|
switch (action.id) {
|
||||||
}
|
case "workspace.tab.new":
|
||||||
if (
|
handleCreateDraftTab();
|
||||||
workspaceTabActionRequest.serverId !== normalizedServerId ||
|
return true;
|
||||||
workspaceTabActionRequest.workspaceId !== normalizedWorkspaceId
|
case "workspace.tab.close-current":
|
||||||
) {
|
if (activeTabId) {
|
||||||
return;
|
void handleCloseTabById(activeTabId);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
const request = workspaceTabActionRequest;
|
case "workspace.tab.navigate-index": {
|
||||||
clearWorkspaceTabActionRequest(request.id);
|
const next = tabs[action.index - 1] ?? null;
|
||||||
|
if (next?.tabId) {
|
||||||
if (request.kind === "new") {
|
navigateToTabId(next.tabId);
|
||||||
handleCreateDraftTab();
|
}
|
||||||
return;
|
return true;
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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, handleCloseTabById, handleCreateDraftTab, navigateToTabId, tabs]
|
||||||
activeTabId,
|
);
|
||||||
clearWorkspaceTabActionRequest,
|
|
||||||
handleCloseTabById,
|
useKeyboardActionHandler({
|
||||||
handleCreateDraftTab,
|
handlerId: `workspace-tab-actions:${normalizedServerId}:${normalizedWorkspaceId}`,
|
||||||
navigateToTabId,
|
actions: [
|
||||||
normalizedServerId,
|
"workspace.tab.new",
|
||||||
normalizedWorkspaceId,
|
"workspace.tab.close-current",
|
||||||
tabs,
|
"workspace.tab.navigate-index",
|
||||||
workspaceTabActionRequest,
|
"workspace.tab.navigate-relative",
|
||||||
]);
|
] as const,
|
||||||
|
enabled: Boolean(normalizedServerId && normalizedWorkspaceId),
|
||||||
|
priority: 100,
|
||||||
|
isActive: () => isScreenFocused,
|
||||||
|
handle: handleWorkspaceTabAction,
|
||||||
|
});
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (
|
if (
|
||||||
@@ -1523,7 +1517,7 @@ function WorkspaceScreenContent({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return isScreenFocused ? (
|
||||||
<TerminalPane
|
<TerminalPane
|
||||||
serverId={normalizedServerId}
|
serverId={normalizedServerId}
|
||||||
cwd={normalizedWorkspaceId}
|
cwd={normalizedWorkspaceId}
|
||||||
@@ -1544,6 +1538,8 @@ function WorkspaceScreenContent({
|
|||||||
hideHeader
|
hideHeader
|
||||||
manageTerminalDirectorySubscription={false}
|
manageTerminalDirectorySubscription={false}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={styles.contentPlaceholder} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2069,6 +2065,11 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
},
|
},
|
||||||
|
contentPlaceholder: {
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
backgroundColor: theme.colors.surface0,
|
||||||
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -9,33 +9,13 @@ beforeEach(() => {
|
|||||||
cmdOrCtrlDown: false,
|
cmdOrCtrlDown: false,
|
||||||
sidebarShortcutWorkspaceTargets: [],
|
sidebarShortcutWorkspaceTargets: [],
|
||||||
visibleWorkspaceTargets: [],
|
visibleWorkspaceTargets: [],
|
||||||
workspaceTabActionRequest: null,
|
|
||||||
nextWorkspaceTabActionRequestId: 1,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("keyboard-shortcuts-store", () => {
|
describe("keyboard-shortcuts-store", () => {
|
||||||
it("queues and clears workspace tab action requests", () => {
|
it("toggles command center open state", () => {
|
||||||
useKeyboardShortcutsStore.getState().requestWorkspaceTabAction({
|
expect(useKeyboardShortcutsStore.getState().commandCenterOpen).toBe(false);
|
||||||
serverId: "srv-1",
|
useKeyboardShortcutsStore.getState().setCommandCenterOpen(true);
|
||||||
workspaceId: "/repo/main",
|
expect(useKeyboardShortcutsStore.getState().commandCenterOpen).toBe(true);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,6 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import type { SidebarShortcutWorkspaceTarget } from "@/utils/sidebar-shortcuts";
|
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 {
|
interface KeyboardShortcutsState {
|
||||||
commandCenterOpen: boolean;
|
commandCenterOpen: boolean;
|
||||||
projectPickerOpen: boolean;
|
projectPickerOpen: boolean;
|
||||||
@@ -33,8 +11,6 @@ interface KeyboardShortcutsState {
|
|||||||
sidebarShortcutWorkspaceTargets: SidebarShortcutWorkspaceTarget[];
|
sidebarShortcutWorkspaceTargets: SidebarShortcutWorkspaceTarget[];
|
||||||
/** All visible workspace targets in top-to-bottom visual order. */
|
/** All visible workspace targets in top-to-bottom visual order. */
|
||||||
visibleWorkspaceTargets: SidebarShortcutWorkspaceTarget[];
|
visibleWorkspaceTargets: SidebarShortcutWorkspaceTarget[];
|
||||||
workspaceTabActionRequest: WorkspaceTabActionRequest | null;
|
|
||||||
nextWorkspaceTabActionRequestId: number;
|
|
||||||
|
|
||||||
setCommandCenterOpen: (open: boolean) => void;
|
setCommandCenterOpen: (open: boolean) => void;
|
||||||
setProjectPickerOpen: (open: boolean) => void;
|
setProjectPickerOpen: (open: boolean) => void;
|
||||||
@@ -44,30 +20,10 @@ interface KeyboardShortcutsState {
|
|||||||
setSidebarShortcutWorkspaceTargets: (targets: SidebarShortcutWorkspaceTarget[]) => void;
|
setSidebarShortcutWorkspaceTargets: (targets: SidebarShortcutWorkspaceTarget[]) => void;
|
||||||
setVisibleWorkspaceTargets: (targets: SidebarShortcutWorkspaceTarget[]) => void;
|
setVisibleWorkspaceTargets: (targets: SidebarShortcutWorkspaceTarget[]) => void;
|
||||||
resetModifiers: () => 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>(
|
export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>(
|
||||||
(set, get) => ({
|
(set) => ({
|
||||||
commandCenterOpen: false,
|
commandCenterOpen: false,
|
||||||
projectPickerOpen: false,
|
projectPickerOpen: false,
|
||||||
shortcutsDialogOpen: false,
|
shortcutsDialogOpen: false,
|
||||||
@@ -75,8 +31,6 @@ export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>(
|
|||||||
cmdOrCtrlDown: false,
|
cmdOrCtrlDown: false,
|
||||||
sidebarShortcutWorkspaceTargets: [],
|
sidebarShortcutWorkspaceTargets: [],
|
||||||
visibleWorkspaceTargets: [],
|
visibleWorkspaceTargets: [],
|
||||||
workspaceTabActionRequest: null,
|
|
||||||
nextWorkspaceTabActionRequestId: 1,
|
|
||||||
|
|
||||||
setCommandCenterOpen: (open) => set({ commandCenterOpen: open }),
|
setCommandCenterOpen: (open) => set({ commandCenterOpen: open }),
|
||||||
setProjectPickerOpen: (open) => set({ projectPickerOpen: open }),
|
setProjectPickerOpen: (open) => set({ projectPickerOpen: open }),
|
||||||
@@ -87,23 +41,5 @@ export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>(
|
|||||||
set({ sidebarShortcutWorkspaceTargets: targets }),
|
set({ sidebarShortcutWorkspaceTargets: targets }),
|
||||||
setVisibleWorkspaceTargets: (targets) => set({ visibleWorkspaceTargets: targets }),
|
setVisibleWorkspaceTargets: (targets) => set({ visibleWorkspaceTargets: targets }),
|
||||||
resetModifiers: () => set({ altDown: false, cmdOrCtrlDown: false }),
|
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,
|
normalizeTerminalTransportKey,
|
||||||
shouldInterceptDomTerminalKey,
|
shouldInterceptDomTerminalKey,
|
||||||
} from "@/utils/terminal-keys";
|
} from "@/utils/terminal-keys";
|
||||||
import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug";
|
|
||||||
|
|
||||||
export type TerminalEmulatorRuntimeMountInput = {
|
export type TerminalEmulatorRuntimeMountInput = {
|
||||||
root: HTMLDivElement;
|
root: HTMLDivElement;
|
||||||
@@ -126,13 +125,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mount(input: TerminalEmulatorRuntimeMountInput): void {
|
mount(input: TerminalEmulatorRuntimeMountInput): void {
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-runtime",
|
|
||||||
event: "mount:start",
|
|
||||||
details: {
|
|
||||||
initialOutputLength: input.initialOutputText.length,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.unmount();
|
this.unmount();
|
||||||
|
|
||||||
input.host.innerHTML = "";
|
input.host.innerHTML = "";
|
||||||
@@ -219,15 +211,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.lastSize = { rows: nextRows, cols: nextCols };
|
this.lastSize = { rows: nextRows, cols: nextCols };
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-runtime",
|
|
||||||
event: "resize:emit",
|
|
||||||
details: {
|
|
||||||
rows: nextRows,
|
|
||||||
cols: nextCols,
|
|
||||||
force,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.callbacks.onResize?.({
|
this.callbacks.onResize?.({
|
||||||
rows: nextRows,
|
rows: nextRows,
|
||||||
cols: nextCols,
|
cols: nextCols,
|
||||||
@@ -239,24 +222,8 @@ export class TerminalEmulatorRuntime {
|
|||||||
|
|
||||||
const inputDisposable = terminal.onData((data) => {
|
const inputDisposable = terminal.onData((data) => {
|
||||||
if (this.suppressInput) {
|
if (this.suppressInput) {
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-runtime",
|
|
||||||
event: "input:suppressed",
|
|
||||||
details: {
|
|
||||||
length: data.length,
|
|
||||||
preview: summarizeTerminalText({ text: data, maxChars: 64 }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-runtime",
|
|
||||||
event: "input:onData",
|
|
||||||
details: {
|
|
||||||
length: data.length,
|
|
||||||
preview: summarizeTerminalText({ text: data, maxChars: 64 }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.callbacks.onInput?.(data);
|
this.callbacks.onInput?.(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -292,17 +259,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
key: normalizeTerminalTransportKey(normalizedKey),
|
key: normalizeTerminalTransportKey(normalizedKey),
|
||||||
...modifiers,
|
...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) {
|
if (this.pendingModifiers.ctrl || this.pendingModifiers.shift || this.pendingModifiers.alt) {
|
||||||
this.callbacks.onPendingModifiersConsumed?.();
|
this.callbacks.onPendingModifiersConsumed?.();
|
||||||
@@ -368,17 +324,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
|
|
||||||
if (input.initialOutputText.length > 0) {
|
if (input.initialOutputText.length > 0) {
|
||||||
this.write({ text: input.initialOutputText, suppressInput: true });
|
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();
|
this.processOutputQueue();
|
||||||
@@ -466,15 +411,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
suppressInput: input.suppressInput ?? false,
|
suppressInput: input.suppressInput ?? false,
|
||||||
...(input.onCommitted ? { onCommitted: input.onCommitted } : {}),
|
...(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();
|
this.processOutputQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,10 +421,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
suppressInput: false,
|
suppressInput: false,
|
||||||
...(input?.onCommitted ? { onCommitted: input.onCommitted } : {}),
|
...(input?.onCommitted ? { onCommitted: input.onCommitted } : {}),
|
||||||
});
|
});
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-runtime",
|
|
||||||
event: "output:clear",
|
|
||||||
});
|
|
||||||
this.processOutputQueue();
|
this.processOutputQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,10 +436,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
terminal.options.theme = input.theme;
|
terminal.options.theme = input.theme;
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-runtime",
|
|
||||||
event: "theme:update",
|
|
||||||
});
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
return;
|
return;
|
||||||
@@ -525,13 +453,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unmount(): void {
|
unmount(): void {
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-runtime",
|
|
||||||
event: "mount:unmount",
|
|
||||||
details: {
|
|
||||||
pendingWriteOperations: this.outputOperations.length,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.clearInFlightOutputTimeout();
|
this.clearInFlightOutputTimeout();
|
||||||
const inFlightOperation = this.inFlightOutputOperation;
|
const inFlightOperation = this.inFlightOutputOperation;
|
||||||
this.inFlightOutputOperation = null;
|
this.inFlightOutputOperation = null;
|
||||||
@@ -593,14 +514,6 @@ export class TerminalEmulatorRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const text = operation.text;
|
const text = operation.text;
|
||||||
terminalDebugLog({
|
|
||||||
scope: "emulator-runtime",
|
|
||||||
event: "output:flush",
|
|
||||||
details: {
|
|
||||||
length: text.length,
|
|
||||||
preview: summarizeTerminalText({ text, maxChars: 96 }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.inFlightOutputOperationTimeout = setTimeout(() => {
|
this.inFlightOutputOperationTimeout = setTimeout(() => {
|
||||||
finalizeOperation(operation);
|
finalizeOperation(operation);
|
||||||
}, OUTPUT_OPERATION_TIMEOUT_MS);
|
}, OUTPUT_OPERATION_TIMEOUT_MS);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug";
|
|
||||||
|
|
||||||
export type TerminalOutputDeliveryChunk = {
|
export type TerminalOutputDeliveryChunk = {
|
||||||
sequence: number;
|
sequence: number;
|
||||||
text: string;
|
text: string;
|
||||||
@@ -37,13 +35,6 @@ export class TerminalOutputDeliveryQueue {
|
|||||||
if (chunk.text.length === 0) {
|
if (chunk.text.length === 0) {
|
||||||
this.pendingChunks.length = 0;
|
this.pendingChunks.length = 0;
|
||||||
this.pendingChunks.push(chunk);
|
this.pendingChunks.push(chunk);
|
||||||
terminalDebugLog({
|
|
||||||
scope: "output-delivery-queue",
|
|
||||||
event: "enqueue:clear",
|
|
||||||
details: {
|
|
||||||
sequence: chunk.sequence,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.tryDeliver();
|
this.tryDeliver();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -59,39 +50,14 @@ export class TerminalOutputDeliveryQueue {
|
|||||||
} else {
|
} else {
|
||||||
this.pendingChunks.push(chunk);
|
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();
|
this.tryDeliver();
|
||||||
}
|
}
|
||||||
|
|
||||||
consume(input: { sequence: number }): void {
|
consume(input: { sequence: number }): void {
|
||||||
if (this.inFlightChunk?.sequence !== input.sequence) {
|
if (this.inFlightChunk?.sequence !== input.sequence) {
|
||||||
terminalDebugLog({
|
|
||||||
scope: "output-delivery-queue",
|
|
||||||
event: "consume:ignored",
|
|
||||||
details: {
|
|
||||||
sequence: input.sequence,
|
|
||||||
inFlightSequence: this.inFlightChunk?.sequence ?? null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
terminalDebugLog({
|
|
||||||
scope: "output-delivery-queue",
|
|
||||||
event: "consume",
|
|
||||||
details: {
|
|
||||||
sequence: input.sequence,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.clearInFlightTimeout();
|
this.clearInFlightTimeout();
|
||||||
this.inFlightChunk = null;
|
this.inFlightChunk = null;
|
||||||
this.tryDeliver();
|
this.tryDeliver();
|
||||||
@@ -114,14 +80,6 @@ export class TerminalOutputDeliveryQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.inFlightChunk = nextChunk;
|
this.inFlightChunk = nextChunk;
|
||||||
terminalDebugLog({
|
|
||||||
scope: "output-delivery-queue",
|
|
||||||
event: "deliver:start",
|
|
||||||
details: {
|
|
||||||
sequence: nextChunk.sequence,
|
|
||||||
pendingCount: this.pendingChunks.length,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.deliverInFlightChunk();
|
this.deliverInFlightChunk();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,15 +94,6 @@ export class TerminalOutputDeliveryQueue {
|
|||||||
if (!this.inFlightChunk) {
|
if (!this.inFlightChunk) {
|
||||||
return;
|
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.deliverInFlightChunk();
|
||||||
}, this.deliveryTimeoutMs);
|
}, this.deliveryTimeoutMs);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
readTerminalOutputBuffer,
|
readTerminalOutputBuffer,
|
||||||
type TerminalOutputBuffer,
|
type TerminalOutputBuffer,
|
||||||
} from "@/utils/terminal-output-buffer";
|
} from "@/utils/terminal-output-buffer";
|
||||||
import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug";
|
|
||||||
|
|
||||||
export type TerminalOutputChunk = {
|
export type TerminalOutputChunk = {
|
||||||
sequence: number;
|
sequence: number;
|
||||||
@@ -54,14 +53,6 @@ export class TerminalOutputPump {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
terminalDebugLog({
|
|
||||||
scope: "output-pump",
|
|
||||||
event: "selected-terminal:set",
|
|
||||||
details: {
|
|
||||||
previousTerminalId: this.selectedTerminalId,
|
|
||||||
nextTerminalId: input.terminalId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.clearSelectedChunkFlushTimer();
|
this.clearSelectedChunkFlushTimer();
|
||||||
this.selectedChunkAccumulator = "";
|
this.selectedChunkAccumulator = "";
|
||||||
this.selectedChunkAccumulatorReplay = null;
|
this.selectedChunkAccumulatorReplay = null;
|
||||||
@@ -102,17 +93,6 @@ export class TerminalOutputPump {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.selectedChunkAccumulator += input.text;
|
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();
|
this.scheduleSelectedChunkFlush();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,16 +153,6 @@ export class TerminalOutputPump {
|
|||||||
const replay = this.selectedChunkAccumulatorReplay ?? false;
|
const replay = this.selectedChunkAccumulatorReplay ?? false;
|
||||||
this.selectedChunkAccumulator = "";
|
this.selectedChunkAccumulator = "";
|
||||||
this.selectedChunkAccumulatorReplay = null;
|
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 });
|
this.emitSelectedChunk({ text, replay });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
TerminalOutputDeliveryQueue,
|
TerminalOutputDeliveryQueue,
|
||||||
type TerminalOutputDeliveryChunk,
|
type TerminalOutputDeliveryChunk,
|
||||||
} from "./terminal-output-delivery-queue";
|
} from "./terminal-output-delivery-queue";
|
||||||
import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug";
|
|
||||||
|
|
||||||
type TerminalOutputState = {
|
type TerminalOutputState = {
|
||||||
selectedTerminalId: string | null;
|
selectedTerminalId: string | null;
|
||||||
@@ -75,18 +74,6 @@ export class TerminalOutputSession {
|
|||||||
chunkSequence: chunk.sequence,
|
chunkSequence: chunk.sequence,
|
||||||
chunkReplay: chunk.replay,
|
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();
|
this.emit();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -121,14 +108,6 @@ export class TerminalOutputSession {
|
|||||||
chunkSequence: 0,
|
chunkSequence: 0,
|
||||||
chunkReplay: false,
|
chunkReplay: false,
|
||||||
};
|
};
|
||||||
terminalDebugLog({
|
|
||||||
scope: "output-session",
|
|
||||||
event: "selected-terminal:set",
|
|
||||||
details: {
|
|
||||||
selectedTerminalId: input.terminalId,
|
|
||||||
snapshotLength: snapshotText.length,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.emit();
|
this.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,13 +133,6 @@ export class TerminalOutputSession {
|
|||||||
chunkSequence: 0,
|
chunkSequence: 0,
|
||||||
chunkReplay: false,
|
chunkReplay: false,
|
||||||
};
|
};
|
||||||
terminalDebugLog({
|
|
||||||
scope: "output-session",
|
|
||||||
event: "selected-terminal:clear",
|
|
||||||
details: {
|
|
||||||
terminalId: input.terminalId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.emit();
|
this.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,14 +180,6 @@ export function releaseTerminalOutputSession(input: { scopeKey: string }): void
|
|||||||
if (current <= 1) {
|
if (current <= 1) {
|
||||||
sessionRefCountByScopeKey.delete(input.scopeKey);
|
sessionRefCountByScopeKey.delete(input.scopeKey);
|
||||||
sessionsByScopeKey.delete(input.scopeKey);
|
sessionsByScopeKey.delete(input.scopeKey);
|
||||||
terminalDebugLog({
|
|
||||||
scope: "output-session",
|
|
||||||
event: "scope:release",
|
|
||||||
details: {
|
|
||||||
scopeKey: input.scopeKey,
|
|
||||||
released: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
waitForDuration,
|
waitForDuration,
|
||||||
withPromiseTimeout,
|
withPromiseTimeout,
|
||||||
} from "@/utils/terminal-attach";
|
} from "@/utils/terminal-attach";
|
||||||
import { summarizeTerminalText, terminalDebugLog } from "./terminal-debug";
|
|
||||||
|
|
||||||
export type TerminalStreamControllerAttachPayload = {
|
export type TerminalStreamControllerAttachPayload = {
|
||||||
streamId: number | null;
|
streamId: number | null;
|
||||||
@@ -114,14 +113,6 @@ export class TerminalStreamController {
|
|||||||
if (this.isDisposed) {
|
if (this.isDisposed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminalDebugLog({
|
|
||||||
scope: "stream-controller",
|
|
||||||
event: "terminal:set",
|
|
||||||
details: {
|
|
||||||
previousTerminalId: this.selectedTerminalId,
|
|
||||||
nextTerminalId: input.terminalId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextTerminalId = input.terminalId;
|
const nextTerminalId = input.terminalId;
|
||||||
const previousTerminalId = this.selectedTerminalId;
|
const previousTerminalId = this.selectedTerminalId;
|
||||||
@@ -166,14 +157,6 @@ export class TerminalStreamController {
|
|||||||
if (this.isDisposed) {
|
if (this.isDisposed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminalDebugLog({
|
|
||||||
scope: "stream-controller",
|
|
||||||
event: "stream:exit",
|
|
||||||
details: {
|
|
||||||
terminalId: input.terminalId,
|
|
||||||
streamId: input.streamId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.selectedTerminalId !== input.terminalId) {
|
if (this.selectedTerminalId !== input.terminalId) {
|
||||||
return;
|
return;
|
||||||
@@ -256,16 +239,6 @@ export class TerminalStreamController {
|
|||||||
if (!this.isAttachGenerationCurrent({ generation: input.generation, terminalId: input.terminalId })) {
|
if (!this.isAttachGenerationCurrent({ generation: input.generation, terminalId: input.terminalId })) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminalDebugLog({
|
|
||||||
scope: "stream-controller",
|
|
||||||
event: "attach:attempt",
|
|
||||||
details: {
|
|
||||||
terminalId: input.terminalId,
|
|
||||||
generation: input.generation,
|
|
||||||
attempt,
|
|
||||||
maxAttachAttempts,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const preferredSize = this.options.getPreferredSize();
|
const preferredSize = this.options.getPreferredSize();
|
||||||
@@ -299,15 +272,6 @@ export class TerminalStreamController {
|
|||||||
|
|
||||||
if (attachPayload.error || typeof attachPayload.streamId !== "number") {
|
if (attachPayload.error || typeof attachPayload.streamId !== "number") {
|
||||||
lastErrorMessage = attachPayload.error ?? "Unable to attach terminal stream";
|
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;
|
const hasRemainingAttempts = attempt < maxAttachAttempts - 1;
|
||||||
if (hasRemainingAttempts && isRetryableError({ message: lastErrorMessage })) {
|
if (hasRemainingAttempts && isRetryableError({ message: lastErrorMessage })) {
|
||||||
await waitForDelay({ durationMs: getRetryDelayMs({ attempt }) });
|
await waitForDelay({ durationMs: getRetryDelayMs({ attempt }) });
|
||||||
@@ -385,19 +349,6 @@ export class TerminalStreamController {
|
|||||||
} else {
|
} else {
|
||||||
unsubscribe();
|
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({
|
this.updateStatus({
|
||||||
terminalId: input.terminalId,
|
terminalId: input.terminalId,
|
||||||
streamId,
|
streamId,
|
||||||
@@ -408,15 +359,6 @@ export class TerminalStreamController {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastErrorMessage =
|
lastErrorMessage =
|
||||||
error instanceof Error ? error.message : "Unable to attach terminal stream";
|
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;
|
const hasRemainingAttempts = attempt < maxAttachAttempts - 1;
|
||||||
if (hasRemainingAttempts && isRetryableError({ message: lastErrorMessage })) {
|
if (hasRemainingAttempts && isRetryableError({ message: lastErrorMessage })) {
|
||||||
await waitForDelay({ durationMs: getRetryDelayMs({ attempt }) });
|
await waitForDelay({ durationMs: getRetryDelayMs({ attempt }) });
|
||||||
@@ -510,21 +452,6 @@ export class TerminalStreamController {
|
|||||||
if (text.length === 0) {
|
if (text.length === 0) {
|
||||||
return;
|
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({
|
this.options.onChunk({
|
||||||
terminalId: input.terminalId,
|
terminalId: input.terminalId,
|
||||||
text,
|
text,
|
||||||
@@ -577,15 +504,6 @@ export class TerminalStreamController {
|
|||||||
if (!activeStream) {
|
if (!activeStream) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
terminalDebugLog({
|
|
||||||
scope: "stream-controller",
|
|
||||||
event: "stream:detach",
|
|
||||||
details: {
|
|
||||||
terminalId: activeStream.terminalId,
|
|
||||||
streamId: activeStream.streamId,
|
|
||||||
shouldDetach: input.shouldDetach,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.activeStream = null;
|
this.activeStream = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -633,16 +551,6 @@ export class TerminalStreamController {
|
|||||||
|
|
||||||
private updateStatus(status: TerminalStreamControllerStatus): void {
|
private updateStatus(status: TerminalStreamControllerStatus): void {
|
||||||
this.status = status;
|
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);
|
this.options.onStatusChange?.(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -2629,7 +2629,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "paseo"
|
name = "paseo"
|
||||||
version = "0.1.27"
|
version = "0.1.28"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
|||||||
Reference in New Issue
Block a user