feat: enhance file reveal functionality and improve UI interactions
- Added support for revealing files in the editor when requested, enhancing user experience during file navigation. - Implemented a pending file reveal mechanism to ensure files are opened correctly after the editor initializes. - Updated the ChipInteractionLayer to allow users to open files directly from chips, streamlining access to relevant files. - Enhanced the MentionSuggestionList styling for better visual feedback on selection. - Improved mention detection logic to maintain selection state during redundant events, optimizing user interaction.
This commit is contained in:
parent
171773acf1
commit
64b9dc526d
8 changed files with 199 additions and 38 deletions
18
README.md
18
README.md
|
|
@ -28,8 +28,26 @@ A new approach to task management with AI agents.
|
||||||
- **Sit back and watch** — tasks change status on the kanban board while agents handle everything on their own
|
- **Sit back and watch** — tasks change status on the kanban board while agents handle everything on their own
|
||||||
- **Review changes like in Cursor** — see what code each task changed, then approve, reject, or comment
|
- **Review changes like in Cursor** — see what code each task changed, then approve, reject, or comment
|
||||||
- **Full tool visibility** — inspect exactly which tools an agent used to complete each task
|
- **Full tool visibility** — inspect exactly which tools an agent used to complete each task
|
||||||
|
- **Live process dashboard** — see which agents are running processes in dedicated sections (frontend, backend, etc.) and open URLs directly in the browser
|
||||||
- **Stay in control** — send a direct message to any agent or drop a comment on a task whenever you want to clarify something or add new work
|
- **Stay in control** — send a direct message to any agent or drop a comment on a task whenever you want to clarify something or add new work
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>More features</strong></summary>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
- **Recent tasks across projects** — browse the latest completed tasks from all your projects in one place
|
||||||
|
- **Deep session analysis** — detailed breakdown of what happened in each Claude session: bash commands, reasoning, subprocesses
|
||||||
|
- **Smart task-to-log matching** — automatically links Claude session logs to specific tasks based on status change timestamps, even when a task moves back and forth between states
|
||||||
|
- **Zero-setup onboarding** — built-in Claude Code installation and authentication, ready to go out of the box
|
||||||
|
- **Built-in code editor** — edit project files with Git support and other essential features without leaving the app
|
||||||
|
- **Branch strategy control** — choose via prompt whether all agents work on a single branch or each gets its own git worktree
|
||||||
|
- **Team member stats** — global performance statistics for every member of the team
|
||||||
|
- **Attach code context** — reference files or code snippets in your messages, just like in Cursor
|
||||||
|
- **Notification system** — configurable alerts when tasks complete, agents need attention, or errors occur
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,14 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
});
|
});
|
||||||
const [messagesFilterOpen, setMessagesFilterOpen] = useState(false);
|
const [messagesFilterOpen, setMessagesFilterOpen] = useState(false);
|
||||||
|
|
||||||
|
// Open editor overlay when a file reveal is requested (e.g. from chip click)
|
||||||
|
const pendingRevealFile = useStore((s) => s.editorPendingRevealFile);
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingRevealFile && data?.config.projectPath) {
|
||||||
|
setEditorOpen(true);
|
||||||
|
}
|
||||||
|
}, [pendingRevealFile, data?.config.projectPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!teamName) {
|
if (!teamName) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,15 @@ export const EditorFileTree = ({
|
||||||
overscan: !dndReady ? 3 : draggedItem ? 20 : 10,
|
overscan: !dndReady ? 3 : draggedItem ? 20 : 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scroll to file when selectedFilePath changes (e.g. from revealFileInEditor)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedFilePath) return;
|
||||||
|
const idx = flatItems.findIndex((fi) => fi.node.fullPath === selectedFilePath);
|
||||||
|
if (idx >= 0) {
|
||||||
|
virtualizer.scrollToIndex(idx, { align: 'center' });
|
||||||
|
}
|
||||||
|
}, [selectedFilePath, flatItems, virtualizer]);
|
||||||
|
|
||||||
// Git status lookup: absolute path → status type
|
// Git status lookup: absolute path → status type
|
||||||
const gitStatusMap = useMemo(() => {
|
const gitStatusMap = useMemo(() => {
|
||||||
const t0 = performance.now();
|
const t0 = performance.now();
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,9 @@ export const ProjectEditorOverlay = ({
|
||||||
// Active tab save error
|
// Active tab save error
|
||||||
const activeSaveError = activeTabId ? (saveErrors[activeTabId] ?? null) : null;
|
const activeSaveError = activeTabId ? (saveErrors[activeTabId] ?? null) : null;
|
||||||
|
|
||||||
|
const pendingRevealFile = useStore((s) => s.editorPendingRevealFile);
|
||||||
|
const revealAndOpenFile = useStore((s) => s.revealAndOpenFile);
|
||||||
|
|
||||||
// Initialize editor on mount
|
// Initialize editor on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void openEditor(projectPath);
|
void openEditor(projectPath);
|
||||||
|
|
@ -245,6 +248,18 @@ export const ProjectEditorOverlay = ({
|
||||||
};
|
};
|
||||||
}, [projectPath, openEditor, closeEditor]);
|
}, [projectPath, openEditor, closeEditor]);
|
||||||
|
|
||||||
|
// Process pending file reveal after editor initializes.
|
||||||
|
// Guard: wait until the file tree is actually loaded (not null) and not loading.
|
||||||
|
// Without the fileTree check, the effect fires on mount when fileTreeLoading is
|
||||||
|
// still at its initial `false` value — before openEditor sets it to `true`.
|
||||||
|
const fileTreeLoading = useStore((s) => s.editorFileTreeLoading);
|
||||||
|
const fileTreeLoaded = useStore((s) => s.editorFileTree !== null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (pendingRevealFile && !fileTreeLoading && fileTreeLoaded) {
|
||||||
|
void revealAndOpenFile(pendingRevealFile);
|
||||||
|
}
|
||||||
|
}, [pendingRevealFile, fileTreeLoading, fileTreeLoaded, revealAndOpenFile]);
|
||||||
|
|
||||||
// Keep container rect fresh for selection menu positioning (resize, sidebar toggle)
|
// Keep container rect fresh for selection menu positioning (resize, sidebar toggle)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = editorContentRef.current;
|
const el = editorContentRef.current;
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,11 @@ import { EditorState } from '@codemirror/state';
|
||||||
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
import { oneDarkHighlightStyle } from '@codemirror/theme-one-dark';
|
||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
|
||||||
|
import { useStore } from '@renderer/store';
|
||||||
import { chipDisplayLabel } from '@renderer/types/inlineChip';
|
import { chipDisplayLabel } from '@renderer/types/inlineChip';
|
||||||
import { calculateChipPositions } from '@renderer/utils/chipUtils';
|
import { calculateChipPositions } from '@renderer/utils/chipUtils';
|
||||||
import { getSyncLanguageExtension } from '@renderer/utils/codemirrorLanguages';
|
import { getSyncLanguageExtension } from '@renderer/utils/codemirrorLanguages';
|
||||||
import { X } from 'lucide-react';
|
import { ExternalLink, X } from 'lucide-react';
|
||||||
|
|
||||||
import type { InlineChip } from '@renderer/types/inlineChip';
|
import type { InlineChip } from '@renderer/types/inlineChip';
|
||||||
import type { ChipPosition } from '@renderer/utils/chipUtils';
|
import type { ChipPosition } from '@renderer/utils/chipUtils';
|
||||||
|
|
@ -54,13 +55,33 @@ const chipPreviewTheme = EditorView.theme({
|
||||||
const MAX_PREVIEW_LINES = 12;
|
const MAX_PREVIEW_LINES = 12;
|
||||||
|
|
||||||
/** Simple tooltip for file-level mention chips (no code preview). */
|
/** Simple tooltip for file-level mention chips (no code preview). */
|
||||||
const ChipFilePreview = ({ chip }: { chip: InlineChip }): React.JSX.Element => {
|
const ChipFilePreview = ({
|
||||||
|
chip,
|
||||||
|
onOpenInEditor,
|
||||||
|
}: {
|
||||||
|
chip: InlineChip;
|
||||||
|
onOpenInEditor?: (filePath: string) => void;
|
||||||
|
}): React.JSX.Element => {
|
||||||
const displayPath = chip.displayPath ?? chip.filePath;
|
const displayPath = chip.displayPath ?? chip.filePath;
|
||||||
return (
|
return (
|
||||||
<div className="max-w-md overflow-hidden rounded-md">
|
<div className="max-w-md overflow-hidden rounded-md">
|
||||||
<div className="flex items-center gap-2 bg-[var(--code-bg,#1e1e2e)] px-2.5 py-2">
|
<div className="flex items-center gap-2 bg-[var(--code-bg,#1e1e2e)] px-2.5 py-2">
|
||||||
<span className="text-[11px] font-medium text-[var(--color-text)]">{chip.fileName}</span>
|
<span className="text-[11px] font-medium text-[var(--color-text)]">{chip.fileName}</span>
|
||||||
<span className="text-[10px] text-[var(--color-text-muted)]">{displayPath}</span>
|
<span className="flex-1 text-[10px] text-[var(--color-text-muted)]">{displayPath}</span>
|
||||||
|
{onOpenInEditor ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] text-[var(--color-text-muted)] transition-colors hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)]"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onOpenInEditor(chip.filePath);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExternalLink size={10} />
|
||||||
|
Open
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -139,6 +160,7 @@ export const ChipInteractionLayer = ({
|
||||||
onRemove,
|
onRemove,
|
||||||
}: ChipInteractionLayerProps): React.JSX.Element | null => {
|
}: ChipInteractionLayerProps): React.JSX.Element | null => {
|
||||||
const [positions, setPositions] = React.useState<ChipPosition[]>([]);
|
const [positions, setPositions] = React.useState<ChipPosition[]>([]);
|
||||||
|
const revealFileInEditor = useStore((s) => s.revealFileInEditor);
|
||||||
|
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
if (chips.length === 0) {
|
if (chips.length === 0) {
|
||||||
|
|
@ -155,40 +177,52 @@ export const ChipInteractionLayer = ({
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none absolute inset-0 z-20 overflow-hidden">
|
<div className="pointer-events-none absolute inset-0 z-20 overflow-hidden">
|
||||||
<div style={{ transform: `translateY(-${scrollTop}px)` }}>
|
<div style={{ transform: `translateY(-${scrollTop}px)` }}>
|
||||||
{positions.map((pos) => (
|
{positions.map((pos) => {
|
||||||
<Tooltip key={pos.chip.id}>
|
const isFileChip = pos.chip.fromLine == null;
|
||||||
<TooltipTrigger asChild>
|
return (
|
||||||
<div
|
<Tooltip key={pos.chip.id}>
|
||||||
className="group pointer-events-auto absolute cursor-default"
|
<TooltipTrigger asChild>
|
||||||
style={{
|
<div
|
||||||
top: pos.top,
|
className={`group pointer-events-auto absolute ${isFileChip ? 'cursor-pointer' : 'cursor-default'}`}
|
||||||
left: pos.left,
|
style={{
|
||||||
width: pos.width,
|
top: pos.top,
|
||||||
height: pos.height,
|
left: pos.left,
|
||||||
}}
|
width: pos.width,
|
||||||
>
|
height: pos.height,
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="absolute -right-1 -top-1.5 z-30 flex size-3.5 items-center justify-center rounded-full border border-[var(--color-border-emphasis)] bg-[var(--color-surface-raised)] opacity-0 transition-opacity group-hover:opacity-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onRemove(pos.chip.id);
|
|
||||||
}}
|
}}
|
||||||
|
onClick={
|
||||||
|
isFileChip
|
||||||
|
? (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
revealFileInEditor(pos.chip.filePath);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<X size={8} className="text-[var(--color-text-muted)]" />
|
<button
|
||||||
</button>
|
type="button"
|
||||||
</div>
|
className="absolute -right-1 -top-1.5 z-30 flex size-3.5 items-center justify-center rounded-full border border-[var(--color-border-emphasis)] bg-[var(--color-surface-raised)] opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
</TooltipTrigger>
|
onClick={(e) => {
|
||||||
<TooltipContent side="top" className="max-w-md p-0">
|
e.preventDefault();
|
||||||
{pos.chip.fromLine == null ? (
|
e.stopPropagation();
|
||||||
<ChipFilePreview chip={pos.chip} />
|
onRemove(pos.chip.id);
|
||||||
) : (
|
}}
|
||||||
<ChipCodePreview chip={pos.chip} />
|
>
|
||||||
)}
|
<X size={8} className="text-[var(--color-text-muted)]" />
|
||||||
</TooltipContent>
|
</button>
|
||||||
</Tooltip>
|
</div>
|
||||||
))}
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" className="max-w-md p-0">
|
||||||
|
{isFileChip ? (
|
||||||
|
<ChipFilePreview chip={pos.chip} onOpenInEditor={revealFileInEditor} />
|
||||||
|
) : (
|
||||||
|
<ChipCodePreview chip={pos.chip} />
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,9 @@ export const MentionSuggestionList = ({
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={isSelected}
|
aria-selected={isSelected}
|
||||||
data-index={idx}
|
data-index={idx}
|
||||||
className={`flex cursor-pointer items-center gap-2 px-3 py-1.5 text-xs transition-colors ${
|
className={`flex cursor-pointer items-center gap-2 rounded-sm px-3 py-1.5 text-xs transition-colors ${
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)]'
|
? 'bg-[var(--color-accent)]/15 ring-[var(--color-accent)]/30 text-[var(--color-text)] ring-1 ring-inset'
|
||||||
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
|
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-raised)]'
|
||||||
}`}
|
}`}
|
||||||
onMouseDown={(e) => {
|
onMouseDown={(e) => {
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,9 @@ export function useMentionDetection({
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
const [dropdownPosition, setDropdownPosition] = useState<DropdownPosition | null>(null);
|
const [dropdownPosition, setDropdownPosition] = useState<DropdownPosition | null>(null);
|
||||||
const triggerIndexRef = useRef<number>(-1);
|
const triggerIndexRef = useRef<number>(-1);
|
||||||
|
// Track current query in a ref so detectTrigger can avoid resetting selectedIndex
|
||||||
|
// on redundant selectionchange events (e.g. after ArrowDown/Up keyboard navigation)
|
||||||
|
const queryRef = useRef('');
|
||||||
|
|
||||||
const filteredSuggestions = useMemo(() => {
|
const filteredSuggestions = useMemo(() => {
|
||||||
if (!isOpen) return [];
|
if (!isOpen) return [];
|
||||||
|
|
@ -179,6 +182,7 @@ export function useMentionDetection({
|
||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
setDropdownPosition(null);
|
setDropdownPosition(null);
|
||||||
triggerIndexRef.current = -1;
|
triggerIndexRef.current = -1;
|
||||||
|
queryRef.current = '';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const computeDropdownPosition = useCallback(
|
const computeDropdownPosition = useCallback(
|
||||||
|
|
@ -217,14 +221,28 @@ export function useMentionDetection({
|
||||||
[value, query, onValueChange, textareaRef, dismiss]
|
[value, query, onValueChange, textareaRef, dismiss]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects whether cursor is inside an @-trigger region and opens/dismisses the dropdown.
|
||||||
|
*
|
||||||
|
* Called from handleSelect (selectionchange) — must NOT reset selectedIndex when
|
||||||
|
* the trigger is already active with the same query, otherwise ArrowDown/Up navigation
|
||||||
|
* gets immediately undone by the selectionchange event that follows keydown.
|
||||||
|
*/
|
||||||
const detectTrigger = useCallback(
|
const detectTrigger = useCallback(
|
||||||
(cursorPos: number) => {
|
(cursorPos: number) => {
|
||||||
const trigger = findMentionTrigger(value, cursorPos);
|
const trigger = findMentionTrigger(value, cursorPos);
|
||||||
if (trigger && (suggestions.length > 0 || enableTriggerAlways)) {
|
if (trigger && (suggestions.length > 0 || enableTriggerAlways)) {
|
||||||
|
const sameQuery =
|
||||||
|
triggerIndexRef.current === trigger.triggerIndex && queryRef.current === trigger.query;
|
||||||
triggerIndexRef.current = trigger.triggerIndex;
|
triggerIndexRef.current = trigger.triggerIndex;
|
||||||
|
queryRef.current = trigger.query;
|
||||||
setQuery(trigger.query);
|
setQuery(trigger.query);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
setSelectedIndex(0);
|
// Only reset selection when trigger/query actually changed —
|
||||||
|
// preserves keyboard navigation index across redundant selectionchange events
|
||||||
|
if (!sameQuery) {
|
||||||
|
setSelectedIndex(0);
|
||||||
|
}
|
||||||
computeDropdownPosition(trigger.triggerIndex, value);
|
computeDropdownPosition(trigger.triggerIndex, value);
|
||||||
} else {
|
} else {
|
||||||
dismiss();
|
dismiss();
|
||||||
|
|
@ -243,8 +261,10 @@ export function useMentionDetection({
|
||||||
const trigger = findMentionTrigger(newValue, cursorPos);
|
const trigger = findMentionTrigger(newValue, cursorPos);
|
||||||
if (trigger && (suggestions.length > 0 || enableTriggerAlways)) {
|
if (trigger && (suggestions.length > 0 || enableTriggerAlways)) {
|
||||||
triggerIndexRef.current = trigger.triggerIndex;
|
triggerIndexRef.current = trigger.triggerIndex;
|
||||||
|
queryRef.current = trigger.query;
|
||||||
setQuery(trigger.query);
|
setQuery(trigger.query);
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
|
// Text changed — always reset selection to first item
|
||||||
setSelectedIndex(0);
|
setSelectedIndex(0);
|
||||||
computeDropdownPosition(trigger.triggerIndex, newValue);
|
computeDropdownPosition(trigger.triggerIndex, newValue);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,14 @@ export interface EditorSlice {
|
||||||
editorPendingGoToLine: number | null;
|
editorPendingGoToLine: number | null;
|
||||||
setPendingGoToLine: (line: number | null) => void;
|
setPendingGoToLine: (line: number | null) => void;
|
||||||
|
|
||||||
|
/** File path to reveal in editor (opens editor, expands dirs, opens tab, focuses in tree). */
|
||||||
|
editorPendingRevealFile: string | null;
|
||||||
|
/** Request to reveal a file in the editor. Opens editor overlay if needed. */
|
||||||
|
revealFileInEditor: (filePath: string) => void;
|
||||||
|
/** Process the pending reveal: expand parent dirs and open the file tab. */
|
||||||
|
revealAndOpenFile: (filePath: string) => Promise<void>;
|
||||||
|
clearPendingRevealFile: () => void;
|
||||||
|
|
||||||
fetchGitStatus: () => Promise<void>;
|
fetchGitStatus: () => Promise<void>;
|
||||||
toggleWatcher: (enable: boolean) => Promise<void>;
|
toggleWatcher: (enable: boolean) => Promise<void>;
|
||||||
toggleLineWrap: () => void;
|
toggleLineWrap: () => void;
|
||||||
|
|
@ -288,9 +296,58 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
|
||||||
editorFileMtimes: {},
|
editorFileMtimes: {},
|
||||||
editorConflictFile: null,
|
editorConflictFile: null,
|
||||||
editorPendingGoToLine: null,
|
editorPendingGoToLine: null,
|
||||||
|
editorPendingRevealFile: null,
|
||||||
|
|
||||||
setPendingGoToLine: (line: number | null) => set({ editorPendingGoToLine: line }),
|
setPendingGoToLine: (line: number | null) => set({ editorPendingGoToLine: line }),
|
||||||
|
|
||||||
|
revealFileInEditor: (filePath: string) => {
|
||||||
|
set({ editorPendingRevealFile: filePath });
|
||||||
|
},
|
||||||
|
|
||||||
|
clearPendingRevealFile: () => {
|
||||||
|
set({ editorPendingRevealFile: null });
|
||||||
|
},
|
||||||
|
|
||||||
|
revealAndOpenFile: async (filePath: string) => {
|
||||||
|
const { editorProjectPath, editorFileTree, expandDirectory, openFile } = get();
|
||||||
|
if (!editorProjectPath) return;
|
||||||
|
|
||||||
|
// Guard: file tree must be loaded before we can reveal.
|
||||||
|
// If it's still null, bail out WITHOUT clearing pendingRevealFile
|
||||||
|
// so the caller effect can retry after the tree loads.
|
||||||
|
if (!editorFileTree) {
|
||||||
|
log.info('revealAndOpenFile: tree not loaded yet, deferring reveal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute parent directories from projectRoot to the file.
|
||||||
|
// Normalize: strip trailing slash from project path to avoid double-slash.
|
||||||
|
const normalizedRoot = editorProjectPath.endsWith('/')
|
||||||
|
? editorProjectPath.slice(0, -1)
|
||||||
|
: editorProjectPath;
|
||||||
|
const relative = filePath.startsWith(normalizedRoot + '/')
|
||||||
|
? filePath.slice(normalizedRoot.length + 1)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (relative) {
|
||||||
|
const segments = relative.split('/');
|
||||||
|
// Expand each parent directory sequentially (root → child → grandchild).
|
||||||
|
// Skip the last segment (the file name itself).
|
||||||
|
// Each expandDirectory call is awaited so that its children are merged
|
||||||
|
// into the tree before the next level is expanded.
|
||||||
|
let currentDir = normalizedRoot;
|
||||||
|
for (let i = 0; i < segments.length - 1; i++) {
|
||||||
|
currentDir = `${currentDir}/${segments[i]}`;
|
||||||
|
await expandDirectory(currentDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the file as a tab
|
||||||
|
openFile(filePath);
|
||||||
|
// Clear reveal state
|
||||||
|
set({ editorPendingRevealFile: null });
|
||||||
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
// Group 1: File tree actions
|
// Group 1: File tree actions
|
||||||
// ═══════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue