Skip to content
Open
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: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ pnpm-debug.log*
lerna-debug.log*

node_modules

# Agent Relay runtime files (created when agents communicate)
.agent-relay/
.trajectories/
dist
dist-ssr
dist-electron
Expand Down
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"@anthropic-ai/sdk": "^0.78.0",
"@better-auth/oauth-provider": "1.4.18",
"@durable-streams/client": "^0.2.1",
"@electric-sql/client": "1.5.12",
"@electric-sql/client": "1.5.13",
"@linear/sdk": "^68.1.0",
"@modelcontextprotocol/sdk": "^1.26.0",
"@octokit/app": "^16.1.2",
Expand Down
10 changes: 5 additions & 5 deletions 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.3.2",
"version": "1.4.0",
"main": "./dist/main/index.js",
"resources": "src/resources",
"repository": {
Expand Down Expand Up @@ -65,7 +65,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@durable-streams/client": "^0.2.1",
"@electric-sql/client": "1.5.12",
"@electric-sql/client": "1.5.13",
"@headless-tree/core": "^1.6.3",
"@headless-tree/react": "^1.6.3",
"@hono/node-server": "^1.14.1",
Expand All @@ -92,9 +92,9 @@
"@superset/workspace-client": "workspace:*",
"@superset/workspace-fs": "workspace:*",
"@t3-oss/env-core": "^0.13.8",
"@tanstack/db": "0.5.31",
"@tanstack/electric-db-collection": "0.2.39",
"@tanstack/react-db": "0.1.75",
"@tanstack/db": "0.5.33",
"@tanstack/electric-db-collection": "0.2.41",
"@tanstack/react-db": "0.1.77",
"@tanstack/react-query": "^5.90.19",
"@tanstack/react-router": "^1.147.3",
"@tanstack/react-table": "^8.21.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,30 @@ function resolveCommentsPullRequestTarget({
};
}

function stripGitHubStatusTimestamp(
status: GitHubStatus | null | undefined,
): Omit<GitHubStatus, "lastRefreshed"> | null {
if (!status) {
return null;
}

const { lastRefreshed: _lastRefreshed, ...rest } = status;
return rest;
}

function hasMeaningfulGitHubStatusChange({
current,
next,
}: {
current: GitHubStatus | null | undefined;
next: GitHubStatus;
}): boolean {
return (
JSON.stringify(stripGitHubStatusTimestamp(current)) !==
JSON.stringify(stripGitHubStatusTimestamp(next))
);
}

export const createGitStatusProcedures = () => {
return router({
refreshGitStatus: publicProcedure
Expand Down Expand Up @@ -165,7 +189,13 @@ export const createGitStatusProcedures = () => {

const freshStatus = await fetchGitHubPRStatus(worktree.path);

if (freshStatus) {
if (
freshStatus &&
hasMeaningfulGitHubStatusChange({
current: worktree.githubStatus,
next: freshStatus,
})
) {
localDb
.update(worktrees)
.set({ githubStatus: freshStatus })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import {
getPRHeadBranchCandidates,
prMatchesLocalBranch,
} from "./pr-resolution";
import { getPullRequestRepoArgs } from "./repo-context";
import {
getPullRequestRepoArgs,
shouldRefreshCachedRepoContext,
} from "./repo-context";

describe("branchMatchesPR", () => {
test("matches same-repo branch exactly", () => {
Expand Down Expand Up @@ -70,6 +73,69 @@ describe("getPullRequestRepoArgs", () => {
});
});

describe("shouldRefreshCachedRepoContext", () => {
test("returns false when no cached repo context exists", () => {
expect(
shouldRefreshCachedRepoContext({
originUrl: "https://github.com/superset-sh/superset",
cachedRepoContext: null,
}),
).toBe(false);
});

test("returns false when the cached repo still matches origin", () => {
expect(
shouldRefreshCachedRepoContext({
originUrl: "https://github.com/superset-sh/superset",
cachedRepoContext: {
repoUrl: "https://github.com/superset-sh/superset",
upstreamUrl: "https://github.com/superset-sh/superset",
isFork: false,
},
}),
).toBe(false);
});

test("returns false when origin is missing", () => {
expect(
shouldRefreshCachedRepoContext({
originUrl: null,
cachedRepoContext: {
repoUrl: "https://github.com/superset-sh/superset",
upstreamUrl: "https://github.com/superset-sh/superset",
isFork: false,
},
}),
).toBe(false);
});

test("treats SSH and HTTPS forms of the same repo as equal", () => {
expect(
shouldRefreshCachedRepoContext({
originUrl: "git@github.com:Superset-Sh/superset.git",
cachedRepoContext: {
repoUrl: "https://github.com/superset-sh/superset",
upstreamUrl: "https://github.com/superset-sh/superset",
isFork: false,
},
}),
).toBe(false);
});

test("returns true when origin no longer matches the cached repo", () => {
expect(
shouldRefreshCachedRepoContext({
originUrl: "https://github.com/Kitenite/superset",
cachedRepoContext: {
repoUrl: "https://github.com/superset-sh/superset",
upstreamUrl: "https://github.com/superset-sh/superset",
isFork: false,
},
}),
).toBe(true);
});
});

describe("parseReviewThreadCommentsResponse", () => {
test("normalizes inline review-thread comments with file metadata", () => {
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function getPullRequestCommentsRepoNameWithOwner(
async function resolvePullRequestCommentsTarget(
worktreePath: string,
): Promise<PullRequestCommentsTarget | null> {
const repoContext = await getRepoContext(worktreePath, { forceFresh: true });
const repoContext = await getRepoContext(worktreePath);
if (!repoContext) {
return null;
}
Expand Down Expand Up @@ -86,9 +86,7 @@ async function refreshGitHubPRStatus(
worktreePath: string,
): Promise<GitHubStatus | null> {
try {
const repoContext = await getRepoContext(worktreePath, {
forceFresh: true,
});
const repoContext = await getRepoContext(worktreePath);
if (!repoContext) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execGitWithShellPath } from "../git-client";
import { execWithShellEnv } from "../shell-env";
import { readCachedRepoContext } from "./cache";
import { getCachedRepoContextState, readCachedRepoContext } from "./cache";
import { GHRepoResponseSchema, type RepoContext } from "./types";

async function refreshRepoContext(
Expand Down Expand Up @@ -65,15 +65,50 @@ export async function getRepoContext(
forceFresh?: boolean;
},
): Promise<RepoContext | null> {
const originUrl = await getOriginUrl(worktreePath);
const cachedRepoContext =
getCachedRepoContextState(worktreePath)?.value ?? null;
const forceFresh =
Boolean(options?.forceFresh) ||
shouldRefreshCachedRepoContext({
originUrl,
cachedRepoContext,
});

return readCachedRepoContext(
worktreePath,
() => refreshRepoContext(worktreePath),
{
forceFresh: options?.forceFresh,
forceFresh,
},
);
}

export function shouldRefreshCachedRepoContext({
originUrl,
cachedRepoContext,
}: {
originUrl: string | null;
cachedRepoContext: RepoContext | null;
}): boolean {
if (!cachedRepoContext) {
return false;
}

const normalizedOriginUrl = normalizeGitHubUrl(
originUrl ?? "",
)?.toLowerCase();
const normalizedCachedRepoUrl = normalizeGitHubUrl(
cachedRepoContext.repoUrl,
)?.toLowerCase();

if (!normalizedOriginUrl || !normalizedCachedRepoUrl) {
return false;
}

return normalizedCachedRepoUrl !== normalizedOriginUrl;
}

async function getOriginUrl(worktreePath: string): Promise<string | null> {
try {
const { stdout } = await execGitWithShellPath(
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
PROTOCOL_SCHEME,
} from "shared/constants";
import { setupAgentHooks } from "./lib/agent-setup";
import { ensureRelayApiKey } from "./lib/terminal/env";
import { initAppState } from "./lib/app-state";
import { requestAppleEventsAccess } from "./lib/apple-events-permission";
import { setupAutoUpdater } from "./lib/auto-updater";
Expand Down Expand Up @@ -335,6 +336,14 @@ if (!gotTheLock) {

await loadWebviewBrowserExtension();

// Initialize shared relay workspace for agent-to-agent communication
// Must run before terminals start so all agents share the same workspace
try {
await ensureRelayApiKey();
} catch (error) {
console.warn("[main] Failed to initialize relay workspace:", error);
}

// Must happen before renderer restore runs
await reconcileDaemonSessions();
prewarmTerminalRuntime();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {
buildRelayWrapExecLine,
buildWrapperScript,
createWrapper,
escapeForSingleQuotedShell,
isSupersetManagedHookCommand,
resolveRelayBrokerPath,
writeFileIfChanged,
} from "./agent-wrappers-common";
import { getNotifyScriptPath, NOTIFY_SCRIPT_NAME } from "./notify-hook";
Expand Down Expand Up @@ -271,7 +274,10 @@ export function createClaudeWrapper(): void {
// createClaudeSettingsJson(), so the wrapper is a plain pass-through.
// We still create the wrapper so SUPERSET_* env vars flow through
// and the notify script can identify the Superset terminal context.
const script = buildWrapperScript("claude", `exec "$REAL_BIN" "$@"`);
const script = buildWrapperScript(
"claude",
buildRelayWrapExecLine("claude", 'exec "$REAL_BIN" "$@"'),
);
createWrapper("claude", script);
}

Expand All @@ -292,7 +298,18 @@ export function createCodexWrapper(): void {
*/
export function buildCodexWrapperExecLine(notifyPath: string): string {
const template = fs.readFileSync(CODEX_WRAPPER_EXEC_TEMPLATE_PATH, "utf-8");
return template.replaceAll("{{NOTIFY_PATH}}", notifyPath);
const notifyConfig = `notify=${JSON.stringify(["bash", notifyPath])}`;
const brokerPath = resolveRelayBrokerPath() ?? "";
return template
.replaceAll("{{NOTIFY_PATH}}", notifyPath)
.replaceAll(
"{{NOTIFY_CONFIG_SHELL_ARG}}",
`'${escapeForSingleQuotedShell(notifyConfig)}'`,
)
.replaceAll(
"{{RELAY_BROKER_PATH_SHELL_ARG}}",
`'${escapeForSingleQuotedShell(brokerPath)}'`,
);
}

// ---------------------------------------------------------------------------
Expand Down
42 changes: 42 additions & 0 deletions apps/desktop/src/main/lib/agent-setup/agent-wrappers-common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { getBrokerBinaryPath } from "@agent-relay/sdk/broker-path";
import { BIN_DIR } from "./paths";

export const WRAPPER_MARKER = "# Superset agent-wrapper v1";
Expand Down Expand Up @@ -84,6 +85,19 @@ export function reconcileManagedEntries<T>({
return { entries, replacedManagedEntries };
}

export function escapeForSingleQuotedShell(value: string): string {
return value.replaceAll("'", `'"'"'`);
}

function buildRelayBrokerResolutionShell(): string {
const brokerPath = resolveRelayBrokerPath();
const brokerFallback = brokerPath
? `'${escapeForSingleQuotedShell(brokerPath)}'`
: "''";

return `_RELAY_BROKER="$(command -v agent-relay-broker 2>/dev/null || printf '%s\\n' ${brokerFallback})"`;
}

function buildRealBinaryResolver(): string {
return `find_real_binary() {
local name="$1"
Expand All @@ -104,6 +118,34 @@ function buildRealBinaryResolver(): string {
`;
}

/**
* Resolve the agent-relay-broker binary path at wrapper generation time.
* Returns the absolute path if found, or null if not installed.
*/
export function resolveRelayBrokerPath(): string | null {
return getBrokerBinaryPath();
}

/**
* Build the relay broker wrapper block for a given CLI name.
* If the broker is not found, returns a plain exec of $REAL_BIN.
*/
export function buildRelayWrapExecLine(
cliName: string,
execFallback: string,
): string {
return `${buildRelayBrokerResolutionShell()}
if [ -n "$_RELAY_BROKER" ] && [ -x "$_RELAY_BROKER" ]; then
export RELAY_AGENT_NAME="\${RELAY_AGENT_NAME:-\${SUPERSET_TAB_ID:-${cliName}-$$}}"
export RELAY_CHANNELS="general"
export RUST_LOG="\${RUST_LOG:-error}"
export RELAY_SKIP_PROMPT=1
exec "$_RELAY_BROKER" wrap "$REAL_BIN" -- "$@"
else
${execFallback}
fi`;
}

function getMissingBinaryMessage(name: string): string {
return `Superset: ${name} not found in PATH. Install it and ensure it is on PATH, then retry.`;
}
Expand Down
Loading