mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-16 21:53:07 +00:00
Fix feature deep link with project path handling (#834)
* Changes from fix/feature-deeplink-worktree * Update apps/ui/src/components/views/board-view.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -595,12 +595,12 @@ export class EventHookService {
|
||||
if (clickUrl && context.projectPath) {
|
||||
try {
|
||||
const url = new URL(clickUrl);
|
||||
url.pathname = '/board';
|
||||
// Add projectPath so the UI can switch to the correct project
|
||||
url.searchParams.set('projectPath', context.projectPath);
|
||||
// Add featureId as query param for deep linking to board with feature output modal
|
||||
if (context.featureId) {
|
||||
url.pathname = '/board';
|
||||
url.searchParams.set('featureId', context.featureId);
|
||||
} else {
|
||||
url.pathname = '/board';
|
||||
}
|
||||
clickUrl = url.toString();
|
||||
} catch (error) {
|
||||
|
||||
@@ -68,7 +68,13 @@ export function NotificationBell({ projectPath }: NotificationBellProps) {
|
||||
|
||||
// Navigate to the relevant view based on notification type
|
||||
if (notification.featureId) {
|
||||
navigate({ to: '/board', search: { featureId: notification.featureId } });
|
||||
navigate({
|
||||
to: '/board',
|
||||
search: {
|
||||
featureId: notification.featureId,
|
||||
projectPath: notification.projectPath || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleMarkAsRead, setPopoverOpen, navigate]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { useEffect, useState, useCallback, useMemo, useRef, startTransition } from 'react';
|
||||
import { createLogger } from '@automaker/utils/logger';
|
||||
import type { PointerEvent as ReactPointerEvent } from 'react';
|
||||
import {
|
||||
@@ -37,6 +37,7 @@ import type {
|
||||
ReasoningEffort,
|
||||
} from '@automaker/types';
|
||||
import { pathsEqual } from '@/lib/utils';
|
||||
import { initializeProject } from '@/lib/project-init';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
BoardBackgroundModal,
|
||||
@@ -117,9 +118,11 @@ const logger = createLogger('Board');
|
||||
interface BoardViewProps {
|
||||
/** Feature ID from URL parameter - if provided, opens output modal for this feature on load */
|
||||
initialFeatureId?: string;
|
||||
/** Project path from URL parameter - if provided, switches to this project before handling deep link */
|
||||
initialProjectPath?: string;
|
||||
}
|
||||
|
||||
export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
export function BoardView({ initialFeatureId, initialProjectPath }: BoardViewProps) {
|
||||
const {
|
||||
currentProject,
|
||||
defaultSkipTests,
|
||||
@@ -139,6 +142,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
setPipelineConfig,
|
||||
featureTemplates,
|
||||
defaultSortNewestCardOnTop,
|
||||
upsertAndSetCurrentProject,
|
||||
} = useAppStore(
|
||||
useShallow((state) => ({
|
||||
currentProject: state.currentProject,
|
||||
@@ -159,6 +163,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
setPipelineConfig: state.setPipelineConfig,
|
||||
featureTemplates: state.featureTemplates,
|
||||
defaultSortNewestCardOnTop: state.defaultSortNewestCardOnTop,
|
||||
upsertAndSetCurrentProject: state.upsertAndSetCurrentProject,
|
||||
}))
|
||||
);
|
||||
// Also get keyboard shortcuts for the add feature shortcut
|
||||
@@ -305,6 +310,53 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
setFeaturesWithContext,
|
||||
});
|
||||
|
||||
// Handle deep link project switching - if URL includes a projectPath that differs from
|
||||
// the current project, switch to the target project first. The feature/worktree deep link
|
||||
// effect below will fire naturally once the project switch triggers a features reload.
|
||||
const handledProjectPathRef = useRef<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (!initialProjectPath || handledProjectPathRef.current === initialProjectPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're already on the correct project
|
||||
if (currentProject?.path && pathsEqual(currentProject.path, initialProjectPath)) {
|
||||
handledProjectPathRef.current = initialProjectPath;
|
||||
return;
|
||||
}
|
||||
|
||||
handledProjectPathRef.current = initialProjectPath;
|
||||
|
||||
const switchProject = async () => {
|
||||
try {
|
||||
const initResult = await initializeProject(initialProjectPath);
|
||||
if (!initResult.success) {
|
||||
logger.warn(
|
||||
`Deep link: failed to initialize project "${initialProjectPath}":`,
|
||||
initResult.error
|
||||
);
|
||||
toast.error('Failed to open project from link', {
|
||||
description: initResult.error || 'Unknown error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Derive project name from path basename
|
||||
const projectName =
|
||||
initialProjectPath.split(/[/\\]/).filter(Boolean).pop() || initialProjectPath;
|
||||
logger.info(`Deep link: switching to project "${projectName}" at ${initialProjectPath}`);
|
||||
upsertAndSetCurrentProject(initialProjectPath, projectName);
|
||||
} catch (error) {
|
||||
logger.error('Deep link: project switch failed:', error);
|
||||
toast.error('Failed to switch project', {
|
||||
description: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
switchProject();
|
||||
}, [initialProjectPath, currentProject?.path, upsertAndSetCurrentProject]);
|
||||
|
||||
// Handle initial feature ID from URL - switch to the correct worktree and open output modal
|
||||
// Uses a ref to track which featureId has been handled to prevent re-opening
|
||||
// when the component re-renders but initialFeatureId hasn't changed.
|
||||
@@ -325,6 +377,17 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
[currentProject?.path]
|
||||
)
|
||||
);
|
||||
|
||||
// Track how many render cycles we've waited for worktrees during a deep link.
|
||||
// If the Zustand store never gets populated (e.g., WorktreePanel hasn't mounted,
|
||||
// useWorktrees setting is off, or the worktree query failed), we stop waiting
|
||||
// after a threshold and open the modal without switching worktree.
|
||||
const deepLinkRetryCountRef = useRef(0);
|
||||
// Reset retry count when the feature ID changes
|
||||
useEffect(() => {
|
||||
deepLinkRetryCountRef.current = 0;
|
||||
}, [initialFeatureId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!initialFeatureId ||
|
||||
@@ -339,14 +402,43 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
const feature = hookFeatures.find((f) => f.id === initialFeatureId);
|
||||
if (!feature) return;
|
||||
|
||||
// If the feature has a branch, wait for worktrees to load so we can switch
|
||||
if (feature.branchName && deepLinkWorktrees.length === 0) {
|
||||
return; // Worktrees not loaded yet - effect will re-run when they load
|
||||
// Resolve worktrees: prefer the Zustand store (reactive), but fall back to
|
||||
// the React Query cache if the store hasn't been populated yet. The store is
|
||||
// only synced by the WorktreePanel's useWorktrees hook, which may not have
|
||||
// rendered yet during a deep link cold start. Reading the query cache directly
|
||||
// avoids an indefinite wait that hangs the app on the loading screen.
|
||||
let resolvedWorktrees = deepLinkWorktrees;
|
||||
if (resolvedWorktrees.length === 0 && currentProject.path) {
|
||||
const cachedData = queryClient.getQueryData(queryKeys.worktrees.all(currentProject.path)) as
|
||||
| { worktrees?: WorktreeInfo[] }
|
||||
| undefined;
|
||||
if (cachedData?.worktrees && cachedData.worktrees.length > 0) {
|
||||
resolvedWorktrees = cachedData.worktrees as typeof deepLinkWorktrees;
|
||||
}
|
||||
}
|
||||
|
||||
// Switch to the correct worktree based on the feature's branchName
|
||||
if (feature.branchName && deepLinkWorktrees.length > 0) {
|
||||
const targetWorktree = deepLinkWorktrees.find((w) => w.branch === feature.branchName);
|
||||
// If the feature has a branch and worktrees aren't available yet, wait briefly.
|
||||
// After enough retries, proceed without switching worktree to avoid hanging.
|
||||
const MAX_DEEP_LINK_RETRIES = 10;
|
||||
if (feature.branchName && resolvedWorktrees.length === 0) {
|
||||
deepLinkRetryCountRef.current++;
|
||||
if (deepLinkRetryCountRef.current < MAX_DEEP_LINK_RETRIES) {
|
||||
return; // Worktrees not loaded yet - effect will re-run when they load
|
||||
}
|
||||
// Exceeded retry limit — proceed without worktree switch to avoid hanging
|
||||
logger.warn(
|
||||
`Deep link: worktrees not available after ${MAX_DEEP_LINK_RETRIES} retries, ` +
|
||||
`opening feature ${initialFeatureId} without switching worktree`
|
||||
);
|
||||
}
|
||||
|
||||
// Switch to the correct worktree based on the feature's branchName.
|
||||
// IMPORTANT: Wrap in startTransition to batch the Zustand store update with
|
||||
// any concurrent React state updates. Without this, the synchronous store
|
||||
// mutation cascades through useAutoMode → refreshStatus → setAutoModeRunning,
|
||||
// which can trigger React error #185 on mobile Safari/PWA crash loops.
|
||||
if (feature.branchName && resolvedWorktrees.length > 0) {
|
||||
const targetWorktree = resolvedWorktrees.find((w) => w.branch === feature.branchName);
|
||||
if (targetWorktree) {
|
||||
const currentWt = useAppStore.getState().getCurrentWorktree(currentProject.path);
|
||||
const isAlreadySelected = targetWorktree.isMain
|
||||
@@ -356,23 +448,27 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
logger.info(
|
||||
`Deep link: switching to worktree "${targetWorktree.branch}" for feature ${initialFeatureId}`
|
||||
);
|
||||
setCurrentWorktree(
|
||||
currentProject.path,
|
||||
targetWorktree.isMain ? null : targetWorktree.path,
|
||||
targetWorktree.branch
|
||||
);
|
||||
startTransition(() => {
|
||||
setCurrentWorktree(
|
||||
currentProject.path,
|
||||
targetWorktree.isMain ? null : targetWorktree.path,
|
||||
targetWorktree.branch
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (!feature.branchName && deepLinkWorktrees.length > 0) {
|
||||
} else if (!feature.branchName && resolvedWorktrees.length > 0) {
|
||||
// Feature has no branch - should be on the main worktree
|
||||
const currentWt = useAppStore.getState().getCurrentWorktree(currentProject.path);
|
||||
if (currentWt?.path !== null && currentWt !== null) {
|
||||
const mainWorktree = deepLinkWorktrees.find((w) => w.isMain);
|
||||
const mainWorktree = resolvedWorktrees.find((w) => w.isMain);
|
||||
if (mainWorktree) {
|
||||
logger.info(
|
||||
`Deep link: switching to main worktree for unassigned feature ${initialFeatureId}`
|
||||
);
|
||||
setCurrentWorktree(currentProject.path, null, mainWorktree.branch);
|
||||
startTransition(() => {
|
||||
setCurrentWorktree(currentProject.path, null, mainWorktree.branch);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,6 +483,7 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
hookFeatures,
|
||||
currentProject?.path,
|
||||
deepLinkWorktrees,
|
||||
queryClient,
|
||||
setCurrentWorktree,
|
||||
setOutputFeature,
|
||||
setShowOutputModal,
|
||||
@@ -764,11 +861,15 @@ export function BoardView({ initialFeatureId }: BoardViewProps) {
|
||||
|
||||
// Recovery handler for BoardErrorBoundary: reset worktree selection to main
|
||||
// so the board can re-render without the stale worktree state that caused the crash.
|
||||
// Wrapped in startTransition to batch with concurrent React updates and avoid
|
||||
// triggering another cascade during recovery.
|
||||
const handleBoardRecover = useCallback(() => {
|
||||
if (!currentProject) return;
|
||||
const mainWorktree = worktrees.find((w) => w.isMain);
|
||||
const mainBranch = mainWorktree?.branch || 'main';
|
||||
setCurrentWorktree(currentProject.path, null, mainBranch);
|
||||
startTransition(() => {
|
||||
setCurrentWorktree(currentProject.path, null, mainBranch);
|
||||
});
|
||||
}, [currentProject, worktrees, setCurrentWorktree]);
|
||||
|
||||
// Helper function to add and select a worktree
|
||||
|
||||
@@ -14,6 +14,63 @@ import { useFeature, useAgentOutput } from '@/hooks/queries';
|
||||
import { queryKeys } from '@/lib/query-keys';
|
||||
import { getFirstNonEmptySummary } from '@/lib/summary-selection';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { isMobileDevice } from '@/lib/mobile-detect';
|
||||
|
||||
// Global concurrency control for mobile mount staggering.
|
||||
// When many AgentInfoPanel instances mount simultaneously (e.g., worktree switch
|
||||
// with 50+ cards), we spread queries over a wider window and cap how many
|
||||
// panels can be querying concurrently to prevent mobile Safari crashes.
|
||||
//
|
||||
// The mechanism works in two layers:
|
||||
// 1. Random delay (0-6s) - spreads mount times so not all panels try to query at once
|
||||
// 2. Concurrency slots (max 4) - even after the delay, only N panels can query simultaneously
|
||||
//
|
||||
// Instance tracking ensures the queue resets if all panels unmount (e.g., navigation).
|
||||
const MOBILE_MAX_CONCURRENT_QUERIES = 4;
|
||||
const MOBILE_STAGGER_WINDOW_MS = 6000; // 6s window (vs previous 2s)
|
||||
let activeMobileQueryCount = 0;
|
||||
let pendingMobileQueue: Array<() => void> = [];
|
||||
let mountedPanelCount = 0;
|
||||
|
||||
function acquireMobileQuerySlot(): Promise<void> {
|
||||
if (!isMobileDevice) return Promise.resolve();
|
||||
if (activeMobileQueryCount < MOBILE_MAX_CONCURRENT_QUERIES) {
|
||||
activeMobileQueryCount++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
pendingMobileQueue.push(() => {
|
||||
activeMobileQueryCount++;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function releaseMobileQuerySlot(): void {
|
||||
if (!isMobileDevice) return;
|
||||
activeMobileQueryCount = Math.max(0, activeMobileQueryCount - 1);
|
||||
const next = pendingMobileQueue.shift();
|
||||
if (next) next();
|
||||
}
|
||||
|
||||
function trackPanelMount(): void {
|
||||
if (!isMobileDevice) return;
|
||||
mountedPanelCount++;
|
||||
}
|
||||
|
||||
function trackPanelUnmount(): void {
|
||||
if (!isMobileDevice) return;
|
||||
mountedPanelCount = Math.max(0, mountedPanelCount - 1);
|
||||
// If all panels unmounted (e.g., navigated away from board or worktree switch),
|
||||
// reset the queue to prevent stale state from blocking future mounts.
|
||||
if (mountedPanelCount === 0) {
|
||||
activeMobileQueryCount = 0;
|
||||
// Drain any pending callbacks so their Promises resolve (components already unmounted)
|
||||
const pending = pendingMobileQueue;
|
||||
pendingMobileQueue = [];
|
||||
for (const cb of pending) cb();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats thinking level for compact display
|
||||
@@ -66,6 +123,12 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
|
||||
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
|
||||
const [isTodosExpanded, setIsTodosExpanded] = useState(false);
|
||||
|
||||
// Track mounted panel count for global queue reset on full unmount
|
||||
useEffect(() => {
|
||||
trackPanelMount();
|
||||
return () => trackPanelUnmount();
|
||||
}, []);
|
||||
|
||||
// Get providers from store for provider-aware model name display
|
||||
// This allows formatModelName to show provider-specific model names (e.g., "GLM 4.7" instead of "Sonnet 4.5")
|
||||
// when a feature was executed using a Claude-compatible provider
|
||||
@@ -92,6 +155,41 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
|
||||
// Determine if we should poll for updates
|
||||
const shouldFetchData = feature.status !== 'backlog' && feature.status !== 'merge_conflict';
|
||||
|
||||
// On mobile, stagger initial per-card queries to prevent a mount storm.
|
||||
// When a worktree loads with many cards, all AgentInfoPanel instances mount
|
||||
// simultaneously. Without staggering, each card fires useFeature + useAgentOutput
|
||||
// queries at the same time, creating 60-100+ concurrent API calls that crash
|
||||
// mobile Safari. Actively running cards fetch immediately (priority data);
|
||||
// other cards defer by a random delay AND wait for a concurrency slot.
|
||||
// The stagger window is 6s (vs previous 2s) to spread load for worktrees
|
||||
// with 50+ features. The concurrency limiter caps active queries to 4 at a time,
|
||||
// preventing the burst that overwhelms mobile Safari's connection handling.
|
||||
const [mountReady, setMountReady] = useState(!isMobileDevice || !!isActivelyRunning);
|
||||
useEffect(() => {
|
||||
if (mountReady) return;
|
||||
let cancelled = false;
|
||||
const delay = Math.random() * MOBILE_STAGGER_WINDOW_MS;
|
||||
const timer = setTimeout(() => {
|
||||
// After the random delay, also wait for a concurrency slot
|
||||
acquireMobileQuerySlot().then(() => {
|
||||
if (!cancelled) {
|
||||
setMountReady(true);
|
||||
// Release the slot after a brief window to let the initial queries fire
|
||||
// and return, preventing all slots from being held indefinitely
|
||||
setTimeout(releaseMobileQuerySlot, 3000);
|
||||
} else {
|
||||
releaseMobileQuerySlot();
|
||||
}
|
||||
});
|
||||
}, delay);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [mountReady]);
|
||||
|
||||
const queryEnabled = shouldFetchData && mountReady;
|
||||
|
||||
// Track whether we're receiving WebSocket events (within threshold)
|
||||
// Use a state to trigger re-renders when the WebSocket connection becomes stale
|
||||
const [isReceivingWsEvents, setIsReceivingWsEvents] = useState(false);
|
||||
@@ -142,34 +240,72 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
|
||||
|
||||
// Fetch fresh feature data for planSpec (store data can be stale for task progress)
|
||||
const { data: freshFeature } = useFeature(projectPath, feature.id, {
|
||||
enabled: shouldFetchData && !contextContent,
|
||||
enabled: queryEnabled && !contextContent,
|
||||
pollingInterval,
|
||||
});
|
||||
|
||||
// Fetch agent output for parsing
|
||||
const { data: agentOutputContent } = useAgentOutput(projectPath, feature.id, {
|
||||
enabled: shouldFetchData && !contextContent,
|
||||
enabled: queryEnabled && !contextContent,
|
||||
pollingInterval,
|
||||
});
|
||||
|
||||
// On mount, ensure feature and agent output queries are fresh.
|
||||
// This handles the worktree switch scenario where cards unmount when filtered out
|
||||
// and remount when the user switches back. Without this, the React Query cache
|
||||
// may serve stale data (or no data) for the individual feature query, causing
|
||||
// the todo list to appear empty until the next polling cycle.
|
||||
// may serve stale data for the individual feature query, causing the todo list
|
||||
// to appear empty until the next polling cycle.
|
||||
//
|
||||
// IMPORTANT: Only invalidate if the cached data EXISTS and is STALE.
|
||||
// During worktree switches, ALL cards in the new worktree remount simultaneously.
|
||||
// If every card fires invalidateQueries(), it creates a query storm (40-100+
|
||||
// concurrent invalidations) that overwhelms React's rendering pipeline on mobile
|
||||
// Safari/PWA, causing crashes. The key insight: if a query has NEVER been fetched
|
||||
// (no dataUpdatedAt), there's nothing stale to invalidate — the useFeature/
|
||||
// useAgentOutput hooks will fetch fresh data when their `enabled` flag is true.
|
||||
// We only need to invalidate when cached data exists but is outdated.
|
||||
//
|
||||
// On mobile, skip mount-time invalidation entirely. The staggered useFeature/
|
||||
// useAgentOutput queries already fetch fresh data — invalidation is redundant
|
||||
// and creates the exact query storm we're trying to prevent. The stale threshold
|
||||
// is also higher on mobile (30s vs 10s) to further reduce unnecessary refetches
|
||||
// during the settling period after a worktree switch.
|
||||
useEffect(() => {
|
||||
if (shouldFetchData && projectPath && feature.id && !contextContent) {
|
||||
// Invalidate both the single feature and agent output queries to trigger immediate refetch
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.features.single(projectPath, feature.id),
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.features.agentOutput(projectPath, feature.id),
|
||||
});
|
||||
if (queryEnabled && projectPath && feature.id && !contextContent) {
|
||||
// On mobile, skip mount-time invalidation — the useFeature/useAgentOutput
|
||||
// hooks will handle the initial fetch after the stagger delay.
|
||||
if (isMobileDevice) return;
|
||||
|
||||
const MOUNT_STALE_THRESHOLD = 10_000; // 10s — skip invalidation if data is fresh
|
||||
const now = Date.now();
|
||||
|
||||
const featureQuery = queryClient.getQueryState(
|
||||
queryKeys.features.single(projectPath, feature.id)
|
||||
);
|
||||
const agentOutputQuery = queryClient.getQueryState(
|
||||
queryKeys.features.agentOutput(projectPath, feature.id)
|
||||
);
|
||||
|
||||
// Only invalidate queries that have cached data AND are stale.
|
||||
// Skip if the query has never been fetched (dataUpdatedAt is undefined) —
|
||||
// the useFeature/useAgentOutput hooks will handle the initial fetch.
|
||||
if (featureQuery?.dataUpdatedAt && now - featureQuery.dataUpdatedAt > MOUNT_STALE_THRESHOLD) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.features.single(projectPath, feature.id),
|
||||
});
|
||||
}
|
||||
if (
|
||||
agentOutputQuery?.dataUpdatedAt &&
|
||||
now - agentOutputQuery.dataUpdatedAt > MOUNT_STALE_THRESHOLD
|
||||
) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.features.agentOutput(projectPath, feature.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Only run on mount (feature.id and projectPath identify this specific card instance)
|
||||
// Runs when mount staggering completes (queryEnabled becomes true) or on initial mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [feature.id, projectPath]);
|
||||
}, [queryEnabled, feature.id, projectPath]);
|
||||
|
||||
// Parse agent output into agentInfo
|
||||
const agentInfo = useMemo(() => {
|
||||
|
||||
@@ -94,8 +94,14 @@ export function NotificationsView() {
|
||||
|
||||
// Navigate to the relevant view based on notification type
|
||||
if (notification.featureId) {
|
||||
// Navigate to board view with feature ID to show output
|
||||
navigate({ to: '/board', search: { featureId: notification.featureId } });
|
||||
// Navigate to board view with feature ID and project path to show output
|
||||
navigate({
|
||||
to: '/board',
|
||||
search: {
|
||||
featureId: notification.featureId,
|
||||
projectPath: notification.projectPath || undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[handleMarkAsRead, navigate]
|
||||
|
||||
@@ -136,8 +136,14 @@ export function RecentActivityFeed({ activities, maxItems = 10 }: RecentActivity
|
||||
upsertAndSetCurrentProject(projectPath, projectName);
|
||||
|
||||
if (activity.featureId) {
|
||||
// Navigate to the specific feature
|
||||
navigate({ to: '/board', search: { featureId: activity.featureId } });
|
||||
// Navigate to the specific feature with project path for deep link handling
|
||||
navigate({
|
||||
to: '/board',
|
||||
search: {
|
||||
featureId: activity.featureId,
|
||||
projectPath: projectPath || undefined,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
navigate({ to: '/board' });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ export const Route = createLazyFileRoute('/board')({
|
||||
});
|
||||
|
||||
function BoardRouteComponent() {
|
||||
const { featureId } = useSearch({ from: '/board' });
|
||||
return <BoardView initialFeatureId={featureId} />;
|
||||
const { featureId, projectPath } = useSearch({ from: '/board' });
|
||||
return <BoardView initialFeatureId={featureId} initialProjectPath={projectPath} />;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
||||
// Search params schema for board route
|
||||
const boardSearchSchema = z.object({
|
||||
featureId: z.string().optional(),
|
||||
projectPath: z.string().optional(),
|
||||
});
|
||||
|
||||
// Component is lazy-loaded via board.lazy.tsx for code splitting.
|
||||
|
||||
Reference in New Issue
Block a user