Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/code/src/main/services/workspace/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,15 @@ export const listGitWorktreesInput = z.object({
mainRepoPath: z.string(),
});

export const getWorktreeFileUsageInput = z.object({
mainRepoPath: z.string(),
});

export const getWorktreeFileUsageOutput = z.object({
usesWorktreeLink: z.boolean(),
usesWorktreeInclude: z.boolean(),
});

export const gitWorktreeEntrySchema = z.object({
worktreePath: z.string(),
head: z.string(),
Expand Down
32 changes: 32 additions & 0 deletions apps/code/src/main/services/workspace/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,28 @@ type TaskAssociation =
branchName: string | null;
};

/**
* True if a worktree exclude file (.worktreelink / .worktreeinclude) exists and has at least
* one non-empty, non-comment entry.
*/
async function hasExcludeFileEntries(
mainRepoPath: string,
fileName: string,
): Promise<boolean> {
try {
const contents = await fsPromises.readFile(
path.join(mainRepoPath, fileName),
"utf8",
);
return contents.split("\n").some((line) => {
const trimmed = line.trim();
return trimmed.length > 0 && !trimmed.startsWith("#");
});
} catch {
return false;
}
}

async function hasAnyFiles(repoPath: string): Promise<boolean> {
try {
const entries = await fsPromises.readdir(repoPath);
Expand Down Expand Up @@ -1089,6 +1111,16 @@ export class WorkspaceService extends TypedEventEmitter<WorkspaceServiceEvents>
});
}

async getWorktreeFileUsage(
mainRepoPath: string,
): Promise<{ usesWorktreeLink: boolean; usesWorktreeInclude: boolean }> {
const [usesWorktreeLink, usesWorktreeInclude] = await Promise.all([
hasExcludeFileEntries(mainRepoPath, ".worktreelink"),
hasExcludeFileEntries(mainRepoPath, ".worktreeinclude"),
]);
return { usesWorktreeLink, usesWorktreeInclude };
}

async getWorktreeSize(worktreePath: string): Promise<{ sizeBytes: number }> {
try {
const { stdout } = await execFileAsync("du", ["-s", worktreePath]);
Expand Down
9 changes: 9 additions & 0 deletions apps/code/src/main/trpc/routers/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
getTaskTimestampsOutput,
getWorkspaceInfoInput,
getWorkspaceInfoOutput,
getWorktreeFileUsageInput,
getWorktreeFileUsageOutput,
getWorktreeSizeInput,
getWorktreeSizeOutput,
getWorktreeTasksInput,
Expand Down Expand Up @@ -106,6 +108,13 @@ export const workspaceRouter = router({
.output(getWorktreeSizeOutput)
.query(({ input }) => getService().getWorktreeSize(input.worktreePath)),

getWorktreeFileUsage: publicProcedure
.input(getWorktreeFileUsageInput)
.output(getWorktreeFileUsageOutput)
.query(({ input }) =>
getService().getWorktreeFileUsage(input.mainRepoPath),
),

deleteWorktree: publicProcedure
.input(deleteWorktreeInput)
.mutation(({ input }) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ import { useConnectivity } from "@hooks/useConnectivity";
import type { WorkspaceMode } from "@main/services/workspace/schemas";
import { get } from "@renderer/di/container";
import { RENDERER_TOKENS } from "@renderer/di/tokens";
import { trpcClient } from "@renderer/trpc/client";
import { toast } from "@renderer/utils/toast";
import type { ExecutionMode, Task } from "@shared/types";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { useNavigationStore } from "@stores/navigationStore";
import { track } from "@utils/analytics";
import { logger } from "@utils/logger";
import { useCallback, useState } from "react";
import type { TaskCreationInput, TaskService } from "../service/service";
Expand Down Expand Up @@ -104,6 +107,54 @@ function prepareTaskInput(
};
}

async function trackTaskCreated(
input: TaskCreationInput,
selectedDirectory: string,
): Promise<void> {
try {
const workspaceMode = input.workspaceMode ?? "local";

let usesWorktreeLink: boolean | undefined;
let usesWorktreeInclude: boolean | undefined;
if (workspaceMode === "worktree" && selectedDirectory) {
try {
const usage = await trpcClient.workspace.getWorktreeFileUsage.query({
mainRepoPath: selectedDirectory,
});
usesWorktreeLink = usage.usesWorktreeLink;
usesWorktreeInclude = usage.usesWorktreeInclude;
} catch (error) {
log.warn("Failed to read worktree file usage for analytics", {
error,
});
}
}

track(ANALYTICS_EVENTS.TASK_CREATED, {
auto_run: !!input.executionMode,
created_from: "command-menu",
repository_provider: input.repository ? "github" : "none",
workspace_mode: workspaceMode,
has_branch: !!input.branch,
has_environment_setup:
workspaceMode === "worktree" ? !!input.environmentId : undefined,
has_sandbox_environment:
workspaceMode === "cloud" ? !!input.sandboxEnvironmentId : undefined,
cloud_run_source:
workspaceMode === "cloud"
? (input.cloudRunSource ?? "manual")
: undefined,
cloud_pr_authorship_mode:
workspaceMode === "cloud" ? input.cloudPrAuthorshipMode : undefined,
uses_worktree_link: usesWorktreeLink,
uses_worktree_include: usesWorktreeInclude,
adapter: input.adapter,
});
} catch (error) {
log.warn("Failed to track Task created event", { error });
}
}

function getErrorTitle(failedStep: string): string {
const titles: Record<string, string> = {
repo_detection: "Failed to detect repository",
Expand Down Expand Up @@ -202,6 +253,10 @@ export function useTaskCreation({
editor.clear();
});

if (result.success) {
void trackTaskCreated(input, selectedDirectory);
}

if (!result.success) {
const title = getErrorTitle(result.failedStep);
toast.error(title, { description: result.error });
Expand Down
11 changes: 0 additions & 11 deletions apps/code/src/renderer/features/tasks/hooks/useTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { useFocusStore } from "@renderer/stores/focusStore";
import { useNavigationStore } from "@renderer/stores/navigationStore";
import { trpcClient } from "@renderer/trpc/client";
import type { Task } from "@shared/types";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
import { keepPreviousData, useQueryClient } from "@tanstack/react-query";
import { track } from "@utils/analytics";
import { logger } from "@utils/logger";
import { useCallback } from "react";

Expand Down Expand Up @@ -111,15 +109,6 @@ export function useCreateTask() {
repository,
github_integration,
}) as unknown as Promise<Task>,
{
onSuccess: (_task, variables) => {
track(ANALYTICS_EVENTS.TASK_CREATED, {
auto_run: false,
created_from: variables.createdFrom || "cli",
repository_provider: variables.repository ? "github" : "none",
});
},
},
);

return { ...mutation, invalidateTasks };
Expand Down
13 changes: 13 additions & 0 deletions apps/code/src/shared/types/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ export interface TaskCreateProperties {
auto_run: boolean;
created_from: TaskCreatedFrom;
repository_provider?: RepositoryProvider;
workspace_mode?: "local" | "worktree" | "cloud";
has_branch?: boolean;
/** Worktree mode: a project environment with a setup script was selected */
has_environment_setup?: boolean;
/** Cloud mode: a sandbox environment was selected */
has_sandbox_environment?: boolean;
cloud_run_source?: "manual" | "signal_report";
cloud_pr_authorship_mode?: "user" | "bot";
/** Worktree mode: repo has a non-empty .worktreelink file */
uses_worktree_link?: boolean;
/** Worktree mode: repo has a non-empty .worktreeinclude file */
uses_worktree_include?: boolean;
adapter?: "claude" | "codex";
}

export interface TaskViewProperties {
Expand Down
Loading