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
4 changes: 2 additions & 2 deletions apps/desktop/electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ export default defineConfig({
"terminal-host": resolve("src/main/terminal-host/index.ts"),
// PTY subprocess - spawned by terminal-host for each terminal
"pty-subprocess": resolve("src/main/terminal-host/pty-subprocess.ts"),
// Worker-thread entrypoint for heavy git/status computations
"git-task-worker": resolve("src/main/git-task-worker.ts"),
},
output: {
dir: resolve(devPath, "main"),
Expand All @@ -112,8 +114,6 @@ export default defineConfig({
"pg-native",
"@ast-grep/napi",
"libsql",
"bufferutil",
"utf-8-validate",
],
plugins: [sentryPlugin].filter(Boolean),
},
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@superset/desktop",
"productName": "Superset",
"description": "The last developer tool you'll ever need",
"version": "1.0.5",
"version": "1.0.6",
"main": "./dist/main/index.js",
"resources": "src/resources",
"repository": {
Expand Down Expand Up @@ -35,6 +35,7 @@
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.43",
"@ai-sdk/openai": "3.0.36",
"@ai-sdk/react": "^3.0.0",
"@ast-grep/napi": "^0.41.0",
"@better-auth/stripe": "1.4.18",
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/changes/branches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const createBranchesRouter = () => {
defaultBranch: string;
checkedOutBranches: Record<string, string>;
worktreeBaseBranch: string | null;
currentBranch: string | null;
}> => {
assertRegisteredWorktree(input.worktreePath);

Expand Down Expand Up @@ -83,6 +84,7 @@ export const createBranchesRouter = () => {
defaultBranch,
checkedOutBranches,
worktreeBaseBranch: configuredBaseBranch ?? persistedBaseBranch,
currentBranch,
};
},
),
Expand Down
218 changes: 41 additions & 177 deletions apps/desktop/src/lib/trpc/routers/changes/status.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import { TRPCError } from "@trpc/server";
import type { ChangedFile, GitChangesStatus } from "shared/changes-types";
import type { StatusResult } from "simple-git";
import simpleGit from "simple-git";
import { z } from "zod";
import { publicProcedure, router } from "../..";
import { getStatusNoLock, NotGitRepoError } from "../workspaces/utils/git";
import { assertRegisteredWorktree, secureFs } from "./security";
import { applyNumstatToFiles } from "./utils/apply-numstat";
import {
parseGitLog,
parseGitStatus,
parseNameStatus,
} from "./utils/parse-status";
import { assertRegisteredWorktree } from "./security";
import {
clearInFlightStatus,
getCachedStatus,
Expand All @@ -20,6 +11,7 @@ import {
setCachedStatus,
setInFlightStatus,
} from "./utils/status-cache";
import { runGitTask } from "./workers/git-task-runner";

export const createStatusRouter = () => {
return router({
Expand Down Expand Up @@ -47,54 +39,32 @@ export const createStatusRouter = () => {

let statusPromise!: Promise<GitChangesStatus>;
statusPromise = (async (): Promise<GitChangesStatus> => {
const git = simpleGit(input.worktreePath);

let status: StatusResult;
try {
status = await getStatusNoLock(input.worktreePath);
const result = await runGitTask(
"getStatus",
{
worktreePath: input.worktreePath,
defaultBranch,
},
{
timeoutMs: 45_000,
},
);

// Guard against stale in-flight completion after explicit invalidation.
if (getInFlightStatus(cacheKey) === statusPromise) {
setCachedStatus(cacheKey, result);
}
return result;
} catch (error) {
if (error instanceof NotGitRepoError) {
if (error instanceof Error && error.name === "NotGitRepoError") {
throw new TRPCError({
code: "BAD_REQUEST",
message: error.message,
});
}
throw error;
}
const parsed = parseGitStatus(status);

const [branchComparison, trackingStatus] = await Promise.all([
getBranchComparison(git, defaultBranch),
getTrackingBranchStatus(git),
applyNumstatToFiles(git, parsed.staged, [
"diff",
"--cached",
"--numstat",
]),
applyNumstatToFiles(git, parsed.unstaged, ["diff", "--numstat"]),
applyUntrackedLineCount(input.worktreePath, parsed.untracked),
]);

const result: GitChangesStatus = {
branch: parsed.branch,
defaultBranch,
againstBase: branchComparison.againstBase,
commits: branchComparison.commits,
staged: parsed.staged,
unstaged: parsed.unstaged,
untracked: parsed.untracked,
ahead: branchComparison.ahead,
behind: branchComparison.behind,
pushCount: trackingStatus.pushCount,
pullCount: trackingStatus.pullCount,
hasUpstream: trackingStatus.hasUpstream,
};

// Guard against stale in-flight completion after explicit invalidation.
if (getInFlightStatus(cacheKey) === statusPromise) {
setCachedStatus(cacheKey, result);
}
return result;
})();

setInFlightStatus(cacheKey, statusPromise);
Expand All @@ -117,134 +87,28 @@ export const createStatusRouter = () => {
.query(async ({ input }): Promise<ChangedFile[]> => {
assertRegisteredWorktree(input.worktreePath);

const git = simpleGit(input.worktreePath);

const nameStatus = await git.raw([
"diff-tree",
"--no-commit-id",
"--name-status",
"-r",
input.commitHash,
]);
const files = parseNameStatus(nameStatus);

await applyNumstatToFiles(git, files, [
"diff-tree",
"--no-commit-id",
"--numstat",
"-r",
input.commitHash,
]);

return files;
try {
return await runGitTask(
"getCommitFiles",
{
worktreePath: input.worktreePath,
commitHash: input.commitHash,
},
{
dedupeKey: `${input.worktreePath}:${input.commitHash}`,
strategy: "coalesce",
timeoutMs: 30_000,
},
);
} catch (error) {
if (error instanceof Error && error.name === "NotGitRepoError") {
throw new TRPCError({
code: "BAD_REQUEST",
message: error.message,
});
}
throw error;
}
}),
});
};

interface BranchComparison {
commits: GitChangesStatus["commits"];
againstBase: ChangedFile[];
ahead: number;
behind: number;
}

async function getBranchComparison(
git: ReturnType<typeof simpleGit>,
defaultBranch: string,
): Promise<BranchComparison> {
let commits: GitChangesStatus["commits"] = [];
let againstBase: ChangedFile[] = [];
let ahead = 0;
let behind = 0;

try {
const tracking = await git.raw([
"rev-list",
"--left-right",
"--count",
`origin/${defaultBranch}...HEAD`,
]);
const [behindStr, aheadStr] = tracking.trim().split(/\s+/);
behind = Number.parseInt(behindStr || "0", 10);
ahead = Number.parseInt(aheadStr || "0", 10);

const logOutput = await git.raw([
"log",
`origin/${defaultBranch}..HEAD`,
"--format=%H|%h|%s|%an|%aI",
]);
commits = parseGitLog(logOutput);

if (ahead > 0) {
const nameStatus = await git.raw([
"diff",
"--name-status",
`origin/${defaultBranch}...HEAD`,
]);
againstBase = parseNameStatus(nameStatus);

await applyNumstatToFiles(git, againstBase, [
"diff",
"--numstat",
`origin/${defaultBranch}...HEAD`,
]);
}
} catch {}

return { commits, againstBase, ahead, behind };
}

const MAX_LINE_COUNT_SIZE = 1 * 1024 * 1024;

async function applyUntrackedLineCount(
worktreePath: string,
untracked: ChangedFile[],
): Promise<void> {
for (const file of untracked) {
try {
const stats = await secureFs.stat(worktreePath, file.path);
if (stats.size > MAX_LINE_COUNT_SIZE) continue;

const content = await secureFs.readFile(worktreePath, file.path);
const lineCount = content.split("\n").length;
file.additions = lineCount;
file.deletions = 0;
} catch {}
}
}

interface TrackingStatus {
pushCount: number;
pullCount: number;
hasUpstream: boolean;
}

async function getTrackingBranchStatus(
git: ReturnType<typeof simpleGit>,
): Promise<TrackingStatus> {
try {
const upstream = await git.raw([
"rev-parse",
"--abbrev-ref",
"@{upstream}",
]);
if (!upstream.trim()) {
return { pushCount: 0, pullCount: 0, hasUpstream: false };
}

const tracking = await git.raw([
"rev-list",
"--left-right",
"--count",
"@{upstream}...HEAD",
]);
const [pullStr, pushStr] = tracking.trim().split(/\s+/);
return {
pushCount: Number.parseInt(pushStr || "0", 10),
pullCount: Number.parseInt(pullStr || "0", 10),
hasUpstream: true,
};
} catch {
return { pushCount: 0, pullCount: 0, hasUpstream: false };
}
}
9 changes: 9 additions & 0 deletions apps/desktop/src/lib/trpc/routers/external/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ describe("getAppCommand", () => {
},
]);
});

test("returns Linux command candidates on Linux", () => {
const result = getAppCommand("intellij", "/path/to/file", "linux");
expect(result).toEqual([
{ command: "idea", args: ["/path/to/file"] },
{ command: "intellij-idea-ultimate", args: ["/path/to/file"] },
{ command: "intellij-idea-community", args: ["/path/to/file"] },
]);
});
});

describe("resolvePath", () => {
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/lib/trpc/routers/external/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ const LINUX_CLI_CANDIDATES: Partial<Record<ExternalApp, string[]>> = {
export function getAppCommand(
app: ExternalApp,
targetPath: string,
platform: NodeJS.Platform = process.platform,
): { command: string; args: string[] }[] | null {
if (process.platform === "darwin") {
if (platform === "darwin") {
const bundleIds = BUNDLE_ID_CANDIDATES[app];
if (bundleIds) {
return bundleIds.map((id) => ({
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
workspaces,
} from "@superset/local-db";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, inArray, isNull, not } from "drizzle-orm";
import { and, desc, eq, inArray, isNotNull, isNull, not } from "drizzle-orm";
import type { BrowserWindow } from "electron";
import { dialog } from "electron";
import { track } from "main/lib/analytics";
Expand Down Expand Up @@ -287,6 +287,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
return localDb
.select()
.from(projects)
.where(isNotNull(projects.tabOrder))
.orderBy(desc(projects.lastOpenedAt))
.all();
}),
Expand Down
32 changes: 29 additions & 3 deletions apps/desktop/src/lib/trpc/routers/resource-metrics.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
import { collectResourceMetrics } from "main/lib/resource-metrics";
import { z } from "zod";
import { publicProcedure, router } from "..";
import {
resourceMetricsSnapshotSchema,
validateResourceMetricsSnapshot,
} from "./resource-metrics.schema";

const getSnapshotInputSchema = z
.object({
mode: z.enum(["interactive", "idle"]).optional(),
force: z.boolean().optional(),
})
.optional();

export const createResourceMetricsRouter = () => {
return router({
getSnapshot: publicProcedure.query(async () => {
return collectResourceMetrics();
}),
getSnapshot: publicProcedure
.input(getSnapshotInputSchema)
.output(resourceMetricsSnapshotSchema)
.query(async ({ input }) => {
const snapshot = await collectResourceMetrics({
mode: input?.mode,
force: input?.force,
});
const validation = validateResourceMetricsSnapshot(snapshot);
if (!validation.isValid) {
console.warn(
"[resource-metrics] Invalid snapshot payload; returning fallback snapshot",
validation.issues,
);
}
return validation.snapshot;
}),
});
};
Loading
Loading