Skip to content

Commit eee0ce4

Browse files
committed
Finish task deep link work
1 parent 047eafc commit eee0ce4

File tree

7 files changed

+381
-96
lines changed

7 files changed

+381
-96
lines changed

apps/array/src/main/services/task-link/service.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ export interface TaskLinkEvents {
1717

1818
@injectable()
1919
export class TaskLinkService extends TypedEventEmitter<TaskLinkEvents> {
20+
/**
21+
* Pending task ID that was received before renderer was ready.
22+
* This handles the case where the app is launched via deep link.
23+
*/
24+
private pendingTaskId: string | null = null;
25+
2026
constructor(
2127
@inject(MAIN_TOKENS.DeepLinkService)
2228
private readonly deepLinkService: DeepLinkService,
@@ -38,8 +44,17 @@ export class TaskLinkService extends TypedEventEmitter<TaskLinkEvents> {
3844
return false;
3945
}
4046

41-
log.info(`Emitting task link event: ${taskId}`);
42-
this.emit(TaskLinkEvent.OpenTask, { taskId });
47+
// Check if renderer is ready (has any listeners)
48+
const hasListeners = this.listenerCount(TaskLinkEvent.OpenTask) > 0;
49+
50+
if (hasListeners) {
51+
log.info(`Emitting task link event: ${taskId}`);
52+
this.emit(TaskLinkEvent.OpenTask, { taskId });
53+
} else {
54+
// Renderer not ready yet - queue it for later
55+
log.info(`Queueing task link (renderer not ready): ${taskId}`);
56+
this.pendingTaskId = taskId;
57+
}
4358

4459
// Focus the window
4560
const mainWindow = getMainWindow();
@@ -52,4 +67,17 @@ export class TaskLinkService extends TypedEventEmitter<TaskLinkEvents> {
5267

5368
return true;
5469
}
70+
71+
/**
72+
* Get and clear any pending task ID.
73+
* Called by renderer on mount to handle deep links that arrived before it was ready.
74+
*/
75+
public consumePendingTaskId(): string | null {
76+
const taskId = this.pendingTaskId;
77+
this.pendingTaskId = null;
78+
if (taskId) {
79+
log.info(`Consumed pending task link: ${taskId}`);
80+
}
81+
return taskId;
82+
}
5583
}

apps/array/src/main/services/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
* This file is auto-generated by vite-plugin-auto-services.ts
44
*/
55

6+

apps/array/src/main/trpc/routers/deep-link.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,21 @@ export const deepLinkRouter = router({
1919
onOpenTask: publicProcedure.subscription(async function* (opts) {
2020
const service = getService();
2121
const options = opts.signal ? { signal: opts.signal } : undefined;
22-
for await (const [payload] of on(service, TaskLinkEvent.OpenTask, options)) {
22+
for await (const [payload] of on(
23+
service,
24+
TaskLinkEvent.OpenTask,
25+
options,
26+
)) {
2327
yield payload as TaskLinkEvents[typeof TaskLinkEvent.OpenTask];
2428
}
2529
}),
30+
31+
/**
32+
* Get any pending task ID that arrived before renderer was ready.
33+
* This handles the case where the app is launched via deep link.
34+
*/
35+
getPendingTaskId: publicProcedure.query(() => {
36+
const service = getService();
37+
return service.consumePendingTaskId();
38+
}),
2639
});

apps/array/src/renderer/components/MainLayout.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,21 @@ import { useNavigationStore } from "@stores/navigationStore";
2020
import { useCallback, useEffect, useState } from "react";
2121
import { useHotkeys } from "react-hotkeys-hook";
2222
import { Toaster } from "sonner";
23+
import { useTaskDeepLink } from "../hooks/useTaskDeepLink";
2324

2425
export function MainLayout() {
2526
const { view, toggleSettings, navigateToTaskInput, goBack, goForward } =
2627
useNavigationStore();
2728
const clearAllLayouts = usePanelLayoutStore((state) => state.clearAllLayouts);
2829
const toggleLeftSidebar = useSidebarStore((state) => state.toggle);
2930
const toggleRightSidebar = useRightSidebarStore((state) => state.toggle);
30-
useIntegrations();
31+
3132
const [commandMenuOpen, setCommandMenuOpen] = useState(false);
3233

34+
// Initialize integrations
35+
useIntegrations();
36+
useTaskDeepLink();
37+
3338
const handleOpenSettings = useCallback(() => {
3439
toggleSettings();
3540
}, [toggleSettings]);
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { useAuthStore } from "@features/auth/stores/authStore";
2+
import { useTaskViewedStore } from "@features/sidebar/stores/taskViewedStore";
3+
import { get } from "@renderer/di/container";
4+
import { RENDERER_TOKENS } from "@renderer/di/tokens";
5+
import { logger } from "@renderer/lib/logger";
6+
import type { TaskService } from "@renderer/services/task/service";
7+
import { useNavigationStore } from "@stores/navigationStore";
8+
import { trpcReact, trpcVanilla } from "@renderer/trpc";
9+
import type { Task } from "@shared/types";
10+
import { useQueryClient } from "@tanstack/react-query";
11+
import { useCallback, useEffect, useRef } from "react";
12+
import { toast } from "sonner";
13+
14+
const log = logger.scope("task-deep-link");
15+
16+
const taskKeys = {
17+
all: ["tasks"] as const,
18+
lists: () => [...taskKeys.all, "list"] as const,
19+
list: (filters?: { repository?: string }) =>
20+
[...taskKeys.lists(), filters] as const,
21+
};
22+
23+
/**
24+
* Hook that subscribes to deep link events and handles opening tasks.
25+
* Uses TaskService to fetch task and set up workspace via the saga pattern.
26+
*/
27+
export function useTaskDeepLink() {
28+
const navigateToTask = useNavigationStore((state) => state.navigateToTask);
29+
const markAsViewed = useTaskViewedStore((state) => state.markAsViewed);
30+
const queryClient = useQueryClient();
31+
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
32+
const hasFetchedPending = useRef(false);
33+
34+
const handleOpenTask = useCallback(
35+
async (taskId: string) => {
36+
log.info(`Opening task from deep link: ${taskId}`);
37+
38+
try {
39+
const taskService = get<TaskService>(RENDERER_TOKENS.TaskService);
40+
const result = await taskService.openTask(taskId);
41+
42+
if (!result.success) {
43+
log.error("Failed to open task from deep link", {
44+
taskId,
45+
error: result.error,
46+
failedStep: result.failedStep,
47+
});
48+
toast.error(`Failed to open task: ${result.error}`);
49+
return;
50+
}
51+
52+
const { task } = result.data;
53+
54+
// Add task to query cache so it shows in sidebar
55+
queryClient.setQueryData<Task[]>(taskKeys.list(), (old) => {
56+
if (!old) return [task];
57+
const existingIndex = old.findIndex((t) => t.id === task.id);
58+
if (existingIndex >= 0) {
59+
const updated = [...old];
60+
updated[existingIndex] = task;
61+
return updated;
62+
}
63+
return [task, ...old];
64+
});
65+
66+
// Invalidate to ensure sync with server
67+
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
68+
69+
// Mark as viewed and navigate
70+
markAsViewed(taskId);
71+
navigateToTask(task);
72+
73+
log.info(`Successfully opened task from deep link: ${taskId}`);
74+
} catch (error) {
75+
log.error("Unexpected error opening task from deep link:", error);
76+
toast.error("Failed to open task");
77+
}
78+
},
79+
[navigateToTask, markAsViewed, queryClient],
80+
);
81+
82+
// Check for pending deep link on mount (for cold start via deep link)
83+
useEffect(() => {
84+
if (!isAuthenticated || hasFetchedPending.current) return;
85+
86+
const fetchPending = async () => {
87+
hasFetchedPending.current = true;
88+
try {
89+
const pendingTaskId = await trpcVanilla.deepLink.getPendingTaskId.query();
90+
if (pendingTaskId) {
91+
log.info(`Found pending deep link task: ${pendingTaskId}`);
92+
handleOpenTask(pendingTaskId);
93+
}
94+
} catch (error) {
95+
log.error("Failed to check for pending deep link:", error);
96+
}
97+
};
98+
99+
fetchPending();
100+
}, [isAuthenticated, handleOpenTask]);
101+
102+
// Subscribe to deep link events (for warm start via deep link)
103+
trpcReact.deepLink.onOpenTask.useSubscription(undefined, {
104+
onData: (data) => {
105+
log.info(`Received deep link event for task: ${data.taskId}`);
106+
handleOpenTask(data.taskId);
107+
},
108+
});
109+
}

0 commit comments

Comments
 (0)