Skip to content

Commit e0bd971

Browse files
authored
feat: Task run deep linking (#337)
1 parent 1e263a2 commit e0bd971

File tree

4 files changed

+88
-35
lines changed

4 files changed

+88
-35
lines changed

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

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,21 @@ export const TaskLinkEvent = {
1212
} as const;
1313

1414
export interface TaskLinkEvents {
15-
[TaskLinkEvent.OpenTask]: { taskId: string };
15+
[TaskLinkEvent.OpenTask]: { taskId: string; taskRunId?: string };
16+
}
17+
18+
export interface PendingDeepLink {
19+
taskId: string;
20+
taskRunId?: string;
1621
}
1722

1823
@injectable()
1924
export class TaskLinkService extends TypedEventEmitter<TaskLinkEvents> {
2025
/**
21-
* Pending task ID that was received before renderer was ready.
26+
* Pending deep link that was received before renderer was ready.
2227
* This handles the case where the app is launched via deep link.
2328
*/
24-
private pendingTaskId: string | null = null;
29+
private pendingDeepLink: PendingDeepLink | null = null;
2530

2631
constructor(
2732
@inject(MAIN_TOKENS.DeepLinkService)
@@ -36,8 +41,12 @@ export class TaskLinkService extends TypedEventEmitter<TaskLinkEvents> {
3641
}
3742

3843
private handleTaskLink(path: string): boolean {
39-
// path is just the taskId (e.g., "abc123" from array://task/abc123)
40-
const taskId = path.split("/")[0];
44+
// path formats:
45+
// "abc123" from array://task/abc123
46+
// "abc123/run/xyz789" from array://task/abc123/run/xyz789
47+
const parts = path.split("/");
48+
const taskId = parts[0];
49+
const taskRunId = parts[1] === "run" ? parts[2] : undefined;
4150

4251
if (!taskId) {
4352
log.warn("Task link missing task ID");
@@ -48,12 +57,16 @@ export class TaskLinkService extends TypedEventEmitter<TaskLinkEvents> {
4857
const hasListeners = this.listenerCount(TaskLinkEvent.OpenTask) > 0;
4958

5059
if (hasListeners) {
51-
log.info(`Emitting task link event: ${taskId}`);
52-
this.emit(TaskLinkEvent.OpenTask, { taskId });
60+
log.info(
61+
`Emitting task link event: taskId=${taskId}, taskRunId=${taskRunId ?? "none"}`,
62+
);
63+
this.emit(TaskLinkEvent.OpenTask, { taskId, taskRunId });
5364
} else {
5465
// Renderer not ready yet - queue it for later
55-
log.info(`Queueing task link (renderer not ready): ${taskId}`);
56-
this.pendingTaskId = taskId;
66+
log.info(
67+
`Queueing task link (renderer not ready): taskId=${taskId}, taskRunId=${taskRunId ?? "none"}`,
68+
);
69+
this.pendingDeepLink = { taskId, taskRunId };
5770
}
5871

5972
// Focus the window
@@ -69,15 +82,17 @@ export class TaskLinkService extends TypedEventEmitter<TaskLinkEvents> {
6982
}
7083

7184
/**
72-
* Get and clear any pending task ID.
85+
* Get and clear any pending deep link.
7386
* Called by renderer on mount to handle deep links that arrived before it was ready.
7487
*/
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}`);
88+
public consumePendingDeepLink(): PendingDeepLink | null {
89+
const pending = this.pendingDeepLink;
90+
this.pendingDeepLink = null;
91+
if (pending) {
92+
log.info(
93+
`Consumed pending task link: taskId=${pending.taskId}, taskRunId=${pending.taskRunId ?? "none"}`,
94+
);
8095
}
81-
return taskId;
96+
return pending;
8297
}
8398
}

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { container } from "../../di/container.js";
22
import { MAIN_TOKENS } from "../../di/tokens.js";
33
import {
4+
type PendingDeepLink,
45
TaskLinkEvent,
56
type TaskLinkService,
67
} from "../../services/task-link/service.js";
@@ -12,7 +13,8 @@ const getService = () =>
1213
export const deepLinkRouter = router({
1314
/**
1415
* Subscribe to task link deep link events.
15-
* Emits task ID when array://task/{taskId} is opened.
16+
* Emits task ID (and optional task run ID) when array://task/{taskId} or
17+
* array://task/{taskId}/run/{taskRunId} is opened.
1618
*/
1719
onOpenTask: publicProcedure.subscription(async function* (opts) {
1820
const service = getService();
@@ -25,11 +27,11 @@ export const deepLinkRouter = router({
2527
}),
2628

2729
/**
28-
* Get any pending task ID that arrived before renderer was ready.
30+
* Get any pending deep link that arrived before renderer was ready.
2931
* This handles the case where the app is launched via deep link.
3032
*/
31-
getPendingTaskId: publicProcedure.query(() => {
33+
getPendingDeepLink: publicProcedure.query((): PendingDeepLink | null => {
3234
const service = getService();
33-
return service.consumePendingTaskId();
35+
return service.consumePendingDeepLink();
3436
}),
3537
});

apps/array/src/renderer/hooks/useTaskDeepLink.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,19 @@ export function useTaskDeepLink() {
3232
const hasFetchedPending = useRef(false);
3333

3434
const handleOpenTask = useCallback(
35-
async (taskId: string) => {
36-
log.info(`Opening task from deep link: ${taskId}`);
35+
async (taskId: string, taskRunId?: string) => {
36+
log.info(
37+
`Opening task from deep link: ${taskId}${taskRunId ? `, run: ${taskRunId}` : ""}`,
38+
);
3739

3840
try {
3941
const taskService = get<TaskService>(RENDERER_TOKENS.TaskService);
40-
const result = await taskService.openTask(taskId);
42+
const result = await taskService.openTask(taskId, taskRunId);
4143

4244
if (!result.success) {
4345
log.error("Failed to open task from deep link", {
4446
taskId,
47+
taskRunId,
4548
error: result.error,
4649
failedStep: result.failedStep,
4750
});
@@ -66,11 +69,12 @@ export function useTaskDeepLink() {
6669
// Invalidate to ensure sync with server
6770
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
6871

69-
// Mark as viewed and navigate
7072
markAsViewed(taskId);
7173
navigateToTask(task);
7274

73-
log.info(`Successfully opened task from deep link: ${taskId}`);
75+
log.info(
76+
`Successfully opened task from deep link: ${taskId}${taskRunId ? `, run: ${taskRunId}` : ""}`,
77+
);
7478
} catch (error) {
7579
log.error("Unexpected error opening task from deep link:", error);
7680
toast.error("Failed to open task");
@@ -86,11 +90,12 @@ export function useTaskDeepLink() {
8690
const fetchPending = async () => {
8791
hasFetchedPending.current = true;
8892
try {
89-
const pendingTaskId =
90-
await trpcVanilla.deepLink.getPendingTaskId.query();
91-
if (pendingTaskId) {
92-
log.info(`Found pending deep link task: ${pendingTaskId}`);
93-
handleOpenTask(pendingTaskId);
93+
const pending = await trpcVanilla.deepLink.getPendingDeepLink.query();
94+
if (pending) {
95+
log.info(
96+
`Found pending deep link: taskId=${pending.taskId}, taskRunId=${pending.taskRunId ?? "none"}`,
97+
);
98+
handleOpenTask(pending.taskId, pending.taskRunId);
9499
}
95100
} catch (error) {
96101
log.error("Failed to check for pending deep link:", error);
@@ -103,9 +108,11 @@ export function useTaskDeepLink() {
103108
// Subscribe to deep link events (for warm start via deep link)
104109
trpcReact.deepLink.onOpenTask.useSubscription(undefined, {
105110
onData: (data) => {
106-
log.info(`Received deep link event for task: ${data.taskId}`);
111+
log.info(
112+
`Received deep link event: taskId=${data.taskId}, taskRunId=${data.taskRunId ?? "none"}`,
113+
);
107114
if (!data?.taskId) return;
108-
handleOpenTask(data.taskId);
115+
handleOpenTask(data.taskId, data.taskRunId);
109116
},
110117
});
111118
}

apps/array/src/renderer/services/task/service.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,15 @@ export class TaskService {
6767
}
6868

6969
/**
70-
* Open an existing task by ID.
70+
* Open an existing task by ID, optionally loading a specific run.
7171
* If the workspace already exists, just fetches task data.
7272
* Otherwise runs the full saga to set up the workspace.
7373
*/
74-
public async openTask(taskId: string): Promise<CreateTaskResult> {
75-
log.info("Opening existing task", { taskId });
74+
public async openTask(
75+
taskId: string,
76+
taskRunId?: string,
77+
): Promise<CreateTaskResult> {
78+
log.info("Opening existing task", { taskId, taskRunId });
7679

7780
const posthogClient = useAuthStore.getState().client;
7881
if (!posthogClient) {
@@ -89,6 +92,14 @@ export class TaskService {
8992
log.info("Workspace already exists, fetching task only", { taskId });
9093
try {
9194
const task = await posthogClient.getTask(taskId);
95+
96+
// If a specific run was requested, fetch and use it
97+
if (taskRunId) {
98+
log.info("Fetching specific task run", { taskId, taskRunId });
99+
const run = await posthogClient.getTaskRun(taskId, taskRunId);
100+
task.latest_run = run;
101+
}
102+
92103
return {
93104
success: true,
94105
data: {
@@ -112,6 +123,24 @@ export class TaskService {
112123

113124
if (result.success) {
114125
this.updateStoresOnSuccess(result.data);
126+
127+
// If a specific run was requested, update the task with that run
128+
if (taskRunId && result.data.task) {
129+
try {
130+
log.info("Fetching specific task run for new workspace", {
131+
taskId,
132+
taskRunId,
133+
});
134+
const run = await posthogClient.getTaskRun(taskId, taskRunId);
135+
result.data.task.latest_run = run;
136+
} catch (error) {
137+
log.warn("Failed to fetch specific task run, using latest", {
138+
taskId,
139+
taskRunId,
140+
error,
141+
});
142+
}
143+
}
115144
}
116145

117146
return result;

0 commit comments

Comments
 (0)