mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Update files
This commit is contained in:
@@ -123,6 +123,7 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
||||
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
|
||||
const openAgentList = usePanelStore((state) => state.openAgentList);
|
||||
const toggleAgentList = usePanelStore((state) => state.toggleAgentList);
|
||||
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
|
||||
const horizontalScroll = useHorizontalScrollOptional();
|
||||
|
||||
const isMobile =
|
||||
@@ -134,7 +135,7 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
||||
: desktopAgentListOpen
|
||||
: false;
|
||||
|
||||
// Cmd+B to toggle sidebar (web only)
|
||||
// Cmd+B to toggle agent list sidebar, Cmd+E to toggle explorer sidebar (web only)
|
||||
useEffect(() => {
|
||||
if (!chromeEnabled) return;
|
||||
if (Platform.OS !== "web") return;
|
||||
@@ -142,11 +143,20 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === "b") {
|
||||
event.preventDefault();
|
||||
toggleAgentList();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
selectedAgentId &&
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
(event.code === "KeyE" || event.key.toLowerCase() === "e")
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleFileExplorer();
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [chromeEnabled, toggleAgentList]);
|
||||
}, [chromeEnabled, selectedAgentId, toggleAgentList, toggleFileExplorer]);
|
||||
const {
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
|
||||
@@ -95,7 +95,8 @@ export function AgentStreamView({
|
||||
state.sessions[resolvedServerId]?.agentStreamHead?.get(agentId)
|
||||
);
|
||||
|
||||
const { requestDirectoryListing, requestFilePreview } = useFileExplorerActions(resolvedServerId);
|
||||
const { requestDirectoryListing, requestFilePreview, selectExplorerEntry } =
|
||||
useFileExplorerActions(resolvedServerId);
|
||||
// Keep entry/exit animations off on Android due to RN dispatchDraw crashes
|
||||
// tracked in react-native-reanimated#8422.
|
||||
const shouldDisableEntryExitAnimations = Platform.OS === "android";
|
||||
@@ -123,8 +124,12 @@ export function AgentStreamView({
|
||||
return;
|
||||
}
|
||||
|
||||
requestDirectoryListing(agentId, normalized.directory);
|
||||
requestDirectoryListing(agentId, normalized.directory, {
|
||||
recordHistory: false,
|
||||
setCurrentPath: false,
|
||||
});
|
||||
if (normalized.file) {
|
||||
selectExplorerEntry(agentId, normalized.file);
|
||||
requestFilePreview(agentId, normalized.file);
|
||||
}
|
||||
|
||||
@@ -136,6 +141,7 @@ export function AgentStreamView({
|
||||
agentId,
|
||||
requestDirectoryListing,
|
||||
requestFilePreview,
|
||||
selectExplorerEntry,
|
||||
setExplorerTab,
|
||||
openFileExplorer,
|
||||
]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { View, Text, Pressable, Platform } from "react-native";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { View, Text, Pressable, Platform, useWindowDimensions } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
@@ -8,12 +8,11 @@ import Animated, {
|
||||
} from "react-native-reanimated";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||
import { X, LayoutGrid, List as ListIcon } from "lucide-react-native";
|
||||
import { X } from "lucide-react-native";
|
||||
import {
|
||||
usePanelStore,
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
MAX_EXPLORER_SIDEBAR_WIDTH,
|
||||
type ViewMode,
|
||||
type ExplorerTab,
|
||||
} from "@/stores/panel-store";
|
||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||
@@ -22,6 +21,8 @@ import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query";
|
||||
import { GitDiffPane } from "./git-diff-pane";
|
||||
import { FileExplorerPane } from "./file-explorer-pane";
|
||||
|
||||
const MIN_CHAT_WIDTH = 400;
|
||||
|
||||
interface ExplorerSidebarProps {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
@@ -38,10 +39,22 @@ export function ExplorerSidebar({ serverId, agentId, cwd }: ExplorerSidebarProps
|
||||
const closeToAgent = usePanelStore((state) => state.closeToAgent);
|
||||
const explorerTab = usePanelStore((state) => state.explorerTab);
|
||||
const explorerWidth = usePanelStore((state) => state.explorerWidth);
|
||||
const explorerViewMode = usePanelStore((state) => state.explorerViewMode);
|
||||
const setExplorerTab = usePanelStore((state) => state.setExplorerTab);
|
||||
const setExplorerWidth = usePanelStore((state) => state.setExplorerWidth);
|
||||
const setExplorerViewMode = usePanelStore((state) => state.setExplorerViewMode);
|
||||
const { width: viewportWidth } = useWindowDimensions();
|
||||
|
||||
useEffect(() => {
|
||||
if (isMobile) {
|
||||
return;
|
||||
}
|
||||
const maxWidth = Math.max(
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_EXPLORER_SIDEBAR_WIDTH, viewportWidth - MIN_CHAT_WIDTH)
|
||||
);
|
||||
if (explorerWidth > maxWidth) {
|
||||
setExplorerWidth(maxWidth);
|
||||
}
|
||||
}, [explorerWidth, isMobile, setExplorerWidth, viewportWidth]);
|
||||
|
||||
// Derive isOpen from the unified panel state
|
||||
const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
|
||||
@@ -133,16 +146,20 @@ export function ExplorerSidebar({ serverId, agentId, cwd }: ExplorerSidebarProps
|
||||
.onUpdate((event) => {
|
||||
// Dragging left (negative translationX) increases width
|
||||
const newWidth = startWidthRef.current - event.translationX;
|
||||
const maxWidth = Math.max(
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_EXPLORER_SIDEBAR_WIDTH, viewportWidth - MIN_CHAT_WIDTH)
|
||||
);
|
||||
const clampedWidth = Math.max(
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_EXPLORER_SIDEBAR_WIDTH, newWidth)
|
||||
Math.min(maxWidth, newWidth)
|
||||
);
|
||||
resizeWidth.value = clampedWidth;
|
||||
})
|
||||
.onEnd(() => {
|
||||
runOnJS(setExplorerWidth)(resizeWidth.value);
|
||||
}),
|
||||
[isMobile, explorerWidth, resizeWidth, setExplorerWidth]
|
||||
[isMobile, explorerWidth, resizeWidth, setExplorerWidth, viewportWidth]
|
||||
);
|
||||
|
||||
const sidebarAnimatedStyle = useAnimatedStyle(() => ({
|
||||
@@ -185,8 +202,6 @@ export function ExplorerSidebar({ serverId, agentId, cwd }: ExplorerSidebarProps
|
||||
serverId={serverId}
|
||||
agentId={agentId}
|
||||
cwd={cwd}
|
||||
fileViewMode={explorerViewMode}
|
||||
onFileViewModeChange={setExplorerViewMode}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
</Animated.View>
|
||||
@@ -219,8 +234,6 @@ export function ExplorerSidebar({ serverId, agentId, cwd }: ExplorerSidebarProps
|
||||
serverId={serverId}
|
||||
agentId={agentId}
|
||||
cwd={cwd}
|
||||
fileViewMode={explorerViewMode}
|
||||
onFileViewModeChange={setExplorerViewMode}
|
||||
isMobile={false}
|
||||
/>
|
||||
</Animated.View>
|
||||
@@ -234,8 +247,6 @@ interface SidebarContentProps {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
cwd: string;
|
||||
fileViewMode: ViewMode;
|
||||
onFileViewModeChange: (mode: ViewMode) => void;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
@@ -246,8 +257,6 @@ function SidebarContent({
|
||||
serverId,
|
||||
agentId,
|
||||
cwd,
|
||||
fileViewMode,
|
||||
onFileViewModeChange,
|
||||
isMobile,
|
||||
}: SidebarContentProps) {
|
||||
const { theme } = useUnistyles();
|
||||
@@ -292,9 +301,6 @@ function SidebarContent({
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={styles.headerRightSection}>
|
||||
{effectiveTab === "files" && (
|
||||
<ViewToggle viewMode={fileViewMode} onChange={onFileViewModeChange} />
|
||||
)}
|
||||
{isMobile && (
|
||||
<Pressable onPress={onClose} style={styles.closeButton}>
|
||||
<X size={18} color={theme.colors.foregroundMuted} />
|
||||
@@ -316,33 +322,6 @@ function SidebarContent({
|
||||
);
|
||||
}
|
||||
|
||||
function ViewToggle({
|
||||
viewMode,
|
||||
onChange,
|
||||
}: {
|
||||
viewMode: ViewMode;
|
||||
onChange: (mode: ViewMode) => void;
|
||||
}) {
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
return (
|
||||
<View style={styles.viewToggleContainer}>
|
||||
<Pressable
|
||||
style={[styles.viewToggleButton, viewMode === "list" && styles.viewToggleActive]}
|
||||
onPress={() => onChange("list")}
|
||||
>
|
||||
<ListIcon size={14} color={theme.colors.foreground} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.viewToggleButton, viewMode === "grid" && styles.viewToggleActive]}
|
||||
onPress={() => onChange("grid")}
|
||||
>
|
||||
<LayoutGrid size={14} color={theme.colors.foreground} />
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
@@ -423,17 +402,4 @@ const styles = StyleSheet.create((theme) => ({
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
},
|
||||
viewToggleContainer: {
|
||||
flexDirection: "row",
|
||||
borderRadius: theme.borderRadius.md,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
overflow: "hidden",
|
||||
},
|
||||
viewToggleButton: {
|
||||
padding: theme.spacing[2],
|
||||
},
|
||||
viewToggleActive: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
}));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -121,6 +121,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
const { theme } = useUnistyles();
|
||||
const voice = useVoiceOptional();
|
||||
const toggleAgentList = usePanelStore((state) => state.toggleAgentList);
|
||||
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
|
||||
const [inputHeight, setInputHeight] = useState(MIN_INPUT_HEIGHT);
|
||||
const textInputRef = useRef<
|
||||
TextInput | (TextInput & { getNativeRef?: () => unknown }) | null
|
||||
@@ -442,6 +443,13 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
return;
|
||||
}
|
||||
|
||||
// Cmd+E or Ctrl+E: toggle explorer sidebar
|
||||
if ((metaKey || ctrlKey) && key === "e") {
|
||||
event.preventDefault();
|
||||
toggleFileExplorer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cmd+D or Ctrl+D: start dictation or submit if already dictating
|
||||
if ((metaKey || ctrlKey) && key === "d") {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -54,6 +54,8 @@ import * as Clipboard from "expo-clipboard";
|
||||
import type { TodoEntry } from "@/types/stream";
|
||||
import { extractPrincipalParam } from "@/utils/tool-call-parsers";
|
||||
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
||||
import { parseInlinePathToken, type InlinePathTarget } from "@/utils/inline-path";
|
||||
export type { InlinePathTarget } from "@/utils/inline-path";
|
||||
import { resolveToolCallPreview } from "./tool-call-preview";
|
||||
import { useToolCallSheet } from "./tool-call-sheet";
|
||||
import {
|
||||
@@ -213,13 +215,6 @@ export const UserMessage = memo(function UserMessage({
|
||||
);
|
||||
});
|
||||
|
||||
export interface InlinePathTarget {
|
||||
raw: string;
|
||||
path: string;
|
||||
lineStart?: number;
|
||||
lineEnd?: number;
|
||||
}
|
||||
|
||||
interface AssistantMessageProps {
|
||||
message: string;
|
||||
timestamp: number;
|
||||
@@ -405,96 +400,6 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
function isLikelyPathToken(value: string): boolean {
|
||||
if (!value || value.length > 300) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/\s/.test(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasSlash = value.includes("/") || value.includes("\\");
|
||||
const hasExtension = /\.[a-zA-Z0-9]{1,8}$/.test(value);
|
||||
|
||||
if (!hasSlash && !hasExtension) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const looksLikeDir =
|
||||
value.endsWith("/") || value.startsWith("./") || value.startsWith("../");
|
||||
|
||||
return hasExtension || looksLikeDir || value.includes("/");
|
||||
}
|
||||
|
||||
function normalizeInlinePathValue(value: string): string | null {
|
||||
const trimmed = value
|
||||
.trim()
|
||||
.replace(/^['"`]/, "")
|
||||
.replace(/['"`]$/, "");
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function parseInlinePathToken(
|
||||
value: string,
|
||||
lastPathRef: React.MutableRefObject<string | null>
|
||||
): InlinePathTarget | null {
|
||||
const rawValue = value ?? "";
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rangeOnlyMatch = trimmed.match(/^:([0-9]+)(?:-([0-9]+))?$/);
|
||||
if (rangeOnlyMatch) {
|
||||
const basePath = lastPathRef.current;
|
||||
if (!basePath) {
|
||||
return null;
|
||||
}
|
||||
const lineStart = parseInt(rangeOnlyMatch[1], 10);
|
||||
const lineEnd = rangeOnlyMatch[2]
|
||||
? parseInt(rangeOnlyMatch[2], 10)
|
||||
: undefined;
|
||||
return {
|
||||
raw: rawValue,
|
||||
path: basePath,
|
||||
lineStart,
|
||||
lineEnd,
|
||||
};
|
||||
}
|
||||
|
||||
const pathMatch = trimmed.match(/^(.*?)(?::([0-9]+)(?:-([0-9]+))?)?$/);
|
||||
if (!pathMatch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const basePath = pathMatch[1]?.trim();
|
||||
if (!basePath || !isLikelyPathToken(basePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizeInlinePathValue(basePath);
|
||||
if (!normalizedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
lastPathRef.current = normalizedPath;
|
||||
|
||||
const lineStart = pathMatch[2] ? parseInt(pathMatch[2], 10) : undefined;
|
||||
const lineEnd = pathMatch[3] ? parseInt(pathMatch[3], 10) : undefined;
|
||||
|
||||
return {
|
||||
raw: rawValue,
|
||||
path: normalizedPath,
|
||||
lineStart,
|
||||
lineEnd,
|
||||
};
|
||||
}
|
||||
|
||||
export const AssistantMessage = memo(function AssistantMessage({
|
||||
message,
|
||||
timestamp,
|
||||
@@ -504,7 +409,6 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
const { theme } = useUnistyles();
|
||||
const resolvedDisableOuterSpacing =
|
||||
useDisableOuterSpacing(disableOuterSpacing);
|
||||
const lastPathRef = useRef<string | null>(null);
|
||||
|
||||
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
||||
|
||||
@@ -579,7 +483,7 @@ export const AssistantMessage = memo(function AssistantMessage({
|
||||
) => {
|
||||
const content = node.content ?? "";
|
||||
const parsed = onInlinePathPress
|
||||
? parseInlinePathToken(content, lastPathRef)
|
||||
? parseInlinePathToken(content)
|
||||
: null;
|
||||
|
||||
if (!parsed) {
|
||||
|
||||
@@ -241,6 +241,7 @@ const createExplorerState = () => ({
|
||||
currentPath: ".",
|
||||
history: ["."],
|
||||
lastVisitedPath: ".",
|
||||
selectedEntryPath: null,
|
||||
});
|
||||
|
||||
const pushHistory = (history: string[], path: string): string[] => {
|
||||
|
||||
@@ -127,14 +127,6 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
detectionGracePeriod: 200,
|
||||
});
|
||||
|
||||
// Update voice detection flags whenever they change
|
||||
useEffect(() => {
|
||||
activeSession?.methods?.setVoiceDetectionFlags(
|
||||
realtimeAudio.isDetecting,
|
||||
realtimeAudio.isSpeaking
|
||||
);
|
||||
}, [activeSession?.methods, realtimeAudio.isDetecting, realtimeAudio.isSpeaking]);
|
||||
|
||||
useEffect(() => {
|
||||
realtimeSessionRef.current = activeSession;
|
||||
}, [activeSession]);
|
||||
@@ -196,7 +188,6 @@ export function VoiceProvider({ children }: VoiceProviderProps) {
|
||||
const session = realtimeSessionRef.current;
|
||||
session?.audioPlayer?.stop();
|
||||
await realtimeAudio.stop();
|
||||
session?.methods?.setVoiceDetectionFlags(false, false);
|
||||
setIsVoiceMode(false);
|
||||
setActiveServerId(null);
|
||||
console.log("[Voice] Mode disabled");
|
||||
|
||||
@@ -11,6 +11,7 @@ function createExplorerState(): AgentFileExplorerState {
|
||||
currentPath: ".",
|
||||
history: ["."],
|
||||
lastVisitedPath: ".",
|
||||
selectedEntryPath: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -40,18 +41,30 @@ export function useFileExplorerActions(serverId: string) {
|
||||
);
|
||||
|
||||
const requestDirectoryListing = useCallback(
|
||||
(agentId: string, path: string, options?: { recordHistory?: boolean }) => {
|
||||
(
|
||||
agentId: string,
|
||||
path: string,
|
||||
options?: { recordHistory?: boolean; setCurrentPath?: boolean }
|
||||
) => {
|
||||
const normalizedPath = path && path.length > 0 ? path : ".";
|
||||
const shouldRecordHistory = options?.recordHistory ?? true;
|
||||
const shouldSetCurrentPath = options?.setCurrentPath ?? true;
|
||||
const shouldRecordHistory =
|
||||
options?.recordHistory ?? (shouldSetCurrentPath ? true : false);
|
||||
|
||||
updateExplorerState(agentId, (state) => ({
|
||||
...state,
|
||||
isLoading: true,
|
||||
lastError: null,
|
||||
pendingRequest: { path: normalizedPath, mode: "list" },
|
||||
currentPath: normalizedPath,
|
||||
history: shouldRecordHistory ? pushHistory(state.history, normalizedPath) : state.history,
|
||||
lastVisitedPath: normalizedPath,
|
||||
...(shouldSetCurrentPath
|
||||
? {
|
||||
currentPath: normalizedPath,
|
||||
history: shouldRecordHistory
|
||||
? pushHistory(state.history, normalizedPath)
|
||||
: state.history,
|
||||
lastVisitedPath: normalizedPath,
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
|
||||
if (!client) {
|
||||
@@ -104,6 +117,7 @@ export function useFileExplorerActions(serverId: string) {
|
||||
updateExplorerState(agentId, (state) => ({
|
||||
...state,
|
||||
isLoading: true,
|
||||
lastError: null,
|
||||
pendingRequest: { path: normalizedPath, mode: "file" },
|
||||
}));
|
||||
|
||||
@@ -165,42 +179,20 @@ export function useFileExplorerActions(serverId: string) {
|
||||
[client]
|
||||
);
|
||||
|
||||
const navigateExplorerBack = useCallback(
|
||||
(agentId: string) => {
|
||||
let targetPath: string | null = null;
|
||||
|
||||
updateExplorerState(agentId, (state) => {
|
||||
if (state.history.length <= 1) {
|
||||
return state;
|
||||
}
|
||||
const nextHistory = state.history.slice(0, -1);
|
||||
targetPath = nextHistory[nextHistory.length - 1] ?? ".";
|
||||
return {
|
||||
...state,
|
||||
isLoading: true,
|
||||
lastError: null,
|
||||
pendingRequest: { path: targetPath, mode: "list" },
|
||||
currentPath: targetPath,
|
||||
history: nextHistory,
|
||||
lastVisitedPath: targetPath,
|
||||
};
|
||||
});
|
||||
|
||||
if (!targetPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
requestDirectoryListing(agentId, targetPath, { recordHistory: false });
|
||||
return targetPath;
|
||||
const selectExplorerEntry = useCallback(
|
||||
(agentId: string, path: string | null) => {
|
||||
updateExplorerState(agentId, (state) => ({
|
||||
...state,
|
||||
selectedEntryPath: path,
|
||||
}));
|
||||
},
|
||||
[requestDirectoryListing, updateExplorerState]
|
||||
[updateExplorerState]
|
||||
);
|
||||
|
||||
return {
|
||||
requestDirectoryListing,
|
||||
requestFilePreview,
|
||||
requestFileDownloadToken,
|
||||
navigateExplorerBack,
|
||||
selectExplorerEntry,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@ interface DesktopSidebarState {
|
||||
}
|
||||
|
||||
export type ExplorerTab = "changes" | "files";
|
||||
export type ViewMode = "list" | "grid";
|
||||
export type SortOption = "name" | "modified" | "size";
|
||||
|
||||
export const DEFAULT_EXPLORER_SIDEBAR_WIDTH = 400;
|
||||
export const MIN_EXPLORER_SIDEBAR_WIDTH = 280;
|
||||
export const MAX_EXPLORER_SIDEBAR_WIDTH = 800;
|
||||
// Upper bound is intentionally generous; desktop resizing enforces a min-chat-width constraint.
|
||||
export const MAX_EXPLORER_SIDEBAR_WIDTH = 2000;
|
||||
|
||||
interface PanelState {
|
||||
// Mobile: which panel is currently shown
|
||||
@@ -45,7 +45,6 @@ interface PanelState {
|
||||
// File explorer settings (shared between mobile/desktop)
|
||||
explorerTab: ExplorerTab;
|
||||
explorerWidth: number;
|
||||
explorerViewMode: ViewMode;
|
||||
explorerSortOption: SortOption;
|
||||
|
||||
// Actions
|
||||
@@ -58,7 +57,6 @@ interface PanelState {
|
||||
// File explorer settings actions
|
||||
setExplorerTab: (tab: ExplorerTab) => void;
|
||||
setExplorerWidth: (width: number) => void;
|
||||
setExplorerViewMode: (mode: ViewMode) => void;
|
||||
setExplorerSortOption: (option: SortOption) => void;
|
||||
}
|
||||
|
||||
@@ -83,7 +81,6 @@ export const usePanelStore = create<PanelState>()(
|
||||
// File explorer defaults
|
||||
explorerTab: "changes",
|
||||
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
||||
explorerViewMode: "list",
|
||||
explorerSortOption: "name",
|
||||
|
||||
openAgentList: () =>
|
||||
@@ -139,7 +136,6 @@ export const usePanelStore = create<PanelState>()(
|
||||
|
||||
setExplorerTab: (tab) => set({ explorerTab: tab }),
|
||||
setExplorerWidth: (width) => set({ explorerWidth: clampWidth(width) }),
|
||||
setExplorerViewMode: (mode) => set({ explorerViewMode: mode }),
|
||||
setExplorerSortOption: (option) => set({ explorerSortOption: option }),
|
||||
}),
|
||||
{
|
||||
@@ -150,7 +146,6 @@ export const usePanelStore = create<PanelState>()(
|
||||
desktop: state.desktop,
|
||||
explorerTab: state.explorerTab,
|
||||
explorerWidth: state.explorerWidth,
|
||||
explorerViewMode: state.explorerViewMode,
|
||||
explorerSortOption: state.explorerSortOption,
|
||||
}),
|
||||
}
|
||||
@@ -181,11 +176,9 @@ export function usePanelState(isMobile: boolean) {
|
||||
// Explorer settings
|
||||
explorerTab: store.explorerTab,
|
||||
explorerWidth: store.explorerWidth,
|
||||
explorerViewMode: store.explorerViewMode,
|
||||
explorerSortOption: store.explorerSortOption,
|
||||
setExplorerTab: store.setExplorerTab,
|
||||
setExplorerWidth: store.setExplorerWidth,
|
||||
setExplorerViewMode: store.setExplorerViewMode,
|
||||
setExplorerSortOption: store.setExplorerSortOption,
|
||||
};
|
||||
}
|
||||
@@ -209,11 +202,9 @@ export function usePanelState(isMobile: boolean) {
|
||||
// Explorer settings
|
||||
explorerTab: store.explorerTab,
|
||||
explorerWidth: store.explorerWidth,
|
||||
explorerViewMode: store.explorerViewMode,
|
||||
explorerSortOption: store.explorerSortOption,
|
||||
setExplorerTab: store.setExplorerTab,
|
||||
setExplorerWidth: store.setExplorerWidth,
|
||||
setExplorerViewMode: store.setExplorerViewMode,
|
||||
setExplorerSortOption: store.setExplorerSortOption,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ export interface AgentFileExplorerState {
|
||||
currentPath: string;
|
||||
history: string[];
|
||||
lastVisitedPath: string;
|
||||
selectedEntryPath: string | null;
|
||||
}
|
||||
|
||||
export interface DaemonConnectionSnapshot {
|
||||
|
||||
33
packages/app/src/utils/inline-path.test.ts
Normal file
33
packages/app/src/utils/inline-path.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseInlinePathToken } from "./inline-path";
|
||||
|
||||
describe("parseInlinePathToken", () => {
|
||||
it("returns null for plain paths (no line)", () => {
|
||||
expect(parseInlinePathToken("src/app.ts")).toBeNull();
|
||||
expect(parseInlinePathToken("README.md")).toBeNull();
|
||||
});
|
||||
|
||||
it("parses filename:line", () => {
|
||||
expect(parseInlinePathToken("src/app.ts:12")).toEqual({
|
||||
raw: "src/app.ts:12",
|
||||
path: "src/app.ts",
|
||||
lineStart: 12,
|
||||
lineEnd: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses filename:lineStart-lineEnd", () => {
|
||||
expect(parseInlinePathToken("src/app.ts:12-20")).toEqual({
|
||||
raw: "src/app.ts:12-20",
|
||||
path: "src/app.ts",
|
||||
lineStart: 12,
|
||||
lineEnd: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects range-only :line tokens", () => {
|
||||
expect(parseInlinePathToken(":12")).toBeNull();
|
||||
expect(parseInlinePathToken(":12-20")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
81
packages/app/src/utils/inline-path.ts
Normal file
81
packages/app/src/utils/inline-path.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
export interface InlinePathTarget {
|
||||
raw: string;
|
||||
path: string;
|
||||
lineStart?: number;
|
||||
lineEnd?: number;
|
||||
}
|
||||
|
||||
function normalizePathToken(value: string): string | null {
|
||||
const trimmed = value
|
||||
.trim()
|
||||
.replace(/^['"`]/, "")
|
||||
.replace(/['"`]$/, "");
|
||||
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict VSCode-style markers only.
|
||||
*
|
||||
* Supported:
|
||||
* - `filename:linenumber`
|
||||
* - `filename:lineStart-lineEnd`
|
||||
*
|
||||
* Not supported (by design):
|
||||
* - plain `filename` (no line)
|
||||
* - `:linenumber` (range-only)
|
||||
*/
|
||||
export function parseInlinePathToken(value: string): InlinePathTarget | null {
|
||||
const rawValue = value ?? "";
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = trimmed.match(/^(.+?):([0-9]+)(?:-([0-9]+))?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const basePathRaw = match[1]?.trim();
|
||||
if (!basePathRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid accidentally treating URLs as file paths.
|
||||
if (basePathRaw.includes("://")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizePathToken(basePathRaw);
|
||||
if (!normalizedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineStart = parseInt(match[2], 10);
|
||||
if (!Number.isFinite(lineStart) || lineStart <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineEnd = match[3] ? parseInt(match[3], 10) : undefined;
|
||||
if (lineEnd !== undefined) {
|
||||
if (!Number.isFinite(lineEnd) || lineEnd <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (lineEnd < lineStart) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
raw: rawValue,
|
||||
path: normalizedPath,
|
||||
lineStart,
|
||||
lineEnd,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user