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
2 changes: 1 addition & 1 deletion apps/desktop/docs/EXTERNAL_FILES.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ its hook entries into these files while preserving user-defined entries:
| File | Purpose |
|------|---------|
| `~/.claude/settings.json` | Claude Code hook registration merge |
| `~/.codex/hooks.json` | Codex hook registration merge (`SessionStart`, `UserPromptSubmit`, `PreToolUse`, `PostToolUse`, `Stop`) |
| `~/.codex/hooks.json` | Codex hook registration merge (`SessionStart`, `UserPromptSubmit`, `Stop`) |
| `~/.factory/settings.json` | Factory Droid hook registration (`UserPromptSubmit`, `Notification`, `PostToolUse`, `Stop`) |

For Codex specifically, Superset now relies on native `~/.codex/hooks.json`
Expand Down
2 changes: 1 addition & 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.4.6",
"version": "1.4.7",
"main": "./dist/main/index.js",
"resources": "src/resources",
"repository": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,12 +385,7 @@ export function getCodexGlobalHooksJsonContent(
}

const managedEvents: Array<{
eventName:
| "SessionStart"
| "UserPromptSubmit"
| "PreToolUse"
| "PostToolUse"
| "Stop";
eventName: "SessionStart" | "UserPromptSubmit" | "Stop";
definition: ClaudeHookDefinition;
}> = [
{
Expand All @@ -405,20 +400,6 @@ export function getCodexGlobalHooksJsonContent(
hooks: [{ type: "command", command: notifyScriptPath }],
},
},
{
eventName: "PreToolUse",
definition: {
matcher: "*",
hooks: [{ type: "command", command: notifyScriptPath }],
},
},
{
eventName: "PostToolUse",
definition: {
matcher: "*",
hooks: [{ type: "command", command: notifyScriptPath }],
},
},
{
eventName: "Stop",
definition: {
Expand Down
85 changes: 17 additions & 68 deletions apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,7 +888,7 @@ describe("agent-wrappers codex hooks.json", () => {
rmSync(TEST_ROOT, { recursive: true, force: true });
});

it("creates Codex hooks.json with prompt and tool lifecycle hooks when no file exists", () => {
it("creates Codex hooks.json with prompt and lifecycle hooks when no file exists", () => {
const notifyPath = "/tmp/.superset/hooks/notify.sh";
const content = getCodexGlobalHooksJsonContent(notifyPath);
expect(content).not.toBeNull();
Expand All @@ -907,8 +907,6 @@ describe("agent-wrappers codex hooks.json", () => {
for (const eventName of [
"SessionStart",
"UserPromptSubmit",
"PreToolUse",
"PostToolUse",
"Stop",
] as const) {
const hooks = parsed.hooks[eventName];
Expand All @@ -920,12 +918,8 @@ describe("agent-wrappers codex hooks.json", () => {
).toBe(true);
}

expect(parsed.hooks.PreToolUse?.every((def) => def.matcher === "*")).toBe(
true,
);
expect(parsed.hooks.PostToolUse?.every((def) => def.matcher === "*")).toBe(
true,
);
expect(parsed.hooks.PreToolUse).toBeUndefined();
expect(parsed.hooks.PostToolUse).toBeUndefined();
});

it("preserves user hooks when merging", () => {
Expand Down Expand Up @@ -987,7 +981,7 @@ describe("agent-wrappers codex hooks.json", () => {

const parsed = JSON.parse(content);

// Preserves user hook
// Preserves user hooks (including PreToolUse/PostToolUse which we don't manage)
expect(
parsed.hooks.Stop.some((def: { hooks: Array<{ command: string }> }) =>
def.hooks.some(
Expand Down Expand Up @@ -1024,78 +1018,35 @@ describe("agent-wrappers codex hooks.json", () => {
),
).toBe(true);

// Adds managed hook
expect(
parsed.hooks.Stop.some((def: { hooks: Array<{ command: string }> }) =>
def.hooks.some(
(hook: { command: string }) => hook.command === notifyPath,
// Adds managed hooks for SessionStart, UserPromptSubmit, Stop
for (const eventName of ["SessionStart", "UserPromptSubmit", "Stop"]) {
expect(
parsed.hooks[eventName].some(
(def: { hooks: Array<{ command: string }> }) =>
def.hooks.some(
(hook: { command: string }) => hook.command === notifyPath,
),
),
),
).toBe(true);
).toBe(true);
}

// Also creates prompt + start hooks
expect(
parsed.hooks.SessionStart.some(
(def: { hooks: Array<{ command: string }> }) =>
def.hooks.some(
(hook: { command: string }) => hook.command === notifyPath,
),
),
).toBe(true);
expect(
parsed.hooks.UserPromptSubmit.some(
(def: { hooks: Array<{ command: string }> }) =>
def.hooks.some(
(hook: { command: string }) => hook.command === notifyPath,
),
),
).toBe(true);
// Does NOT inject managed hooks for PreToolUse/PostToolUse
expect(
parsed.hooks.PreToolUse.some(
(def: { hooks: Array<{ command: string }> }) =>
def.hooks.some(
(hook: { command: string }) => hook.command === notifyPath,
),
),
).toBe(true);
).toBe(false);
expect(
parsed.hooks.PostToolUse.some(
(def: { hooks: Array<{ command: string }> }) =>
def.hooks.some(
(hook: { command: string }) => hook.command === notifyPath,
),
),
).toBe(true);
});

it("adds UserPromptSubmit, PreToolUse, and PostToolUse to the Codex hooks.json merge", () => {
const notifyPath = "/tmp/.superset/hooks/notify.sh";
const content = getCodexGlobalHooksJsonContent(notifyPath);
expect(content).not.toBeNull();
if (content === null) throw new Error("Expected content");

const parsed = JSON.parse(content) as {
hooks: Record<
string,
Array<{
matcher?: string;
hooks: Array<{ type: string; command: string }>;
}>
>;
};

for (const eventName of [
"UserPromptSubmit",
"PreToolUse",
"PostToolUse",
] as const) {
expect(parsed.hooks[eventName]).toBeDefined();
expect(
parsed.hooks[eventName]?.some((def) =>
def.hooks.some((hook) => hook.command === notifyPath),
),
).toBe(true);
}
).toBe(false);
});

it("replaces stale Codex hook commands from old superset paths", () => {
Expand Down Expand Up @@ -1150,8 +1101,6 @@ describe("agent-wrappers codex hooks.json", () => {
for (const eventName of [
"SessionStart",
"UserPromptSubmit",
"PreToolUse",
"PostToolUse",
"Stop",
] as const) {
const hooks = parsed.hooks[eventName];
Expand Down
51 changes: 0 additions & 51 deletions apps/desktop/src/main/terminal-host/session-shell-ready.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,57 +301,6 @@ describe("Session shell-ready: kill/exit before readiness", () => {
});
});

/** Wait for the emulator write queue to drain (uses setImmediate internally). */
function waitForEmulatorFlush(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 50));
}

describe("Session shell-ready: DA1 query response forwarding (#3028)", () => {
it("forwards headless emulator DA1 response to subprocess during pending state", async () => {
// Fish shell sends DA1 (ESC[c) at startup to detect terminal capabilities.
// The headless emulator generates a response (ESC[?1;2c) via xterm.js.
// This response MUST be forwarded to the subprocess even during the
// "pending" shell-ready state, otherwise fish waits 10s and then
// disables optional features like cursor shape and reflow detection.
const { session, proc } = createTestSession("/usr/local/bin/fish");
spawnAndReady(session, proc);

// Simulate PTY output containing a DA1 query from fish.
// When the headless emulator processes this, xterm.js generates
// a DA1 response via its onData callback.
sendData(proc, "\x1b[c");

// The emulator write queue processes via setImmediate, so we need
// to let the event loop tick for xterm to process the query.
await waitForEmulatorFlush();

// The emulator's DA1 response should have been forwarded to the
// subprocess (written to stdin) even though shell is still pending.
const writes = getWrittenData(proc);

// The response should contain a DA1 reply (ESC[?...c format)
expect(writes.length).toBeGreaterThan(0);
const da1Response = writes.join("");
// biome-ignore lint/suspicious/noControlCharactersInRegex: matching ESC in terminal protocol data
expect(da1Response).toMatch(/\x1b\[\?[\d;]+c/);
});

it("forwards DSR response to subprocess during pending state", async () => {
const { session, proc } = createTestSession("/bin/zsh");
spawnAndReady(session, proc);

// DSR (Device Status Report): ESC[5n → ESC[0n (terminal OK)
sendData(proc, "\x1b[5n");

await waitForEmulatorFlush();

const writes = getWrittenData(proc);
expect(writes.length).toBeGreaterThan(0);
const response = writes.join("");
expect(response).toContain("\x1b[0n");
});
});

describe("Session shell-ready: supported shells", () => {
for (const shell of [
"/bin/zsh",
Expand Down
14 changes: 5 additions & 9 deletions apps/desktop/src/main/terminal-host/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,20 +225,16 @@ export class Session {
this.emulator.setCwd(options.cwd);

// The headless emulator responds to terminal queries (e.g. DA1,
// DSR) when no renderer client is attached. These responses must
// be forwarded to the subprocess immediately — even during shell
// init — because shells like fish send DA1 at startup and wait
// up to 10 seconds for a reply before disabling optional features.
// DSR). These responses must be forwarded to the subprocess
// regardless of whether renderer clients are attached, because
// shells like fish send DA1 at startup and wait up to 10 seconds
// for a reply before disabling optional features.
// Unlike renderer-generated responses (which go through write()
// and are correctly dropped during init to avoid appearing as
// typed text), headless emulator responses are written directly
// to the PTY and consumed by the shell as protocol data.
this.emulator.onData((data) => {
if (
this.attachedClients.size === 0 &&
this.subprocess &&
this.subprocessReady
) {
if (this.subprocess && this.subprocessReady) {
this.sendWriteToSubprocess(data);
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
.default-markdown p {
margin-top: 0;
margin-bottom: 1rem;
line-height: 1.7;
line-height: 1.5;
}

.default-markdown ul,
Expand All @@ -60,7 +60,7 @@

.default-markdown li {
margin-bottom: 0.25rem;
line-height: 1.6;
line-height: 1.5;
}

.default-markdown ul {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.tufte-markdown {
font-family: Georgia, "Times New Roman", serif;
font-size: 1.1rem;
line-height: 1.8;
line-height: 1.6;
color: var(--foreground);
}

Expand Down Expand Up @@ -84,7 +84,7 @@
/* Body text */
.tufte-markdown p {
margin-top: 0;
margin-bottom: 1.4rem;
margin-bottom: 1.1rem;
text-align: justify;
-webkit-hyphens: auto;
hyphens: auto;
Expand All @@ -101,7 +101,7 @@
/* Lists */
.tufte-markdown ul,
.tufte-markdown ol {
margin: 1.4rem 0;
margin: 1.1rem 0;
padding-left: 1.5rem;
list-style-position: outside;
}
Expand All @@ -115,7 +115,8 @@
}

.tufte-markdown li {
margin-bottom: 0.5rem;
margin-bottom: 0.35rem;
line-height: 1.5;
}

/* Links */
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { FaSlack } from "react-icons/fa";
import { HiArrowPath, HiCheck } from "react-icons/hi2";
import { SiGithub, SiLinear } from "react-icons/si";
import { SiLinear } from "react-icons/si";

const SYNCED_ITEMS = [
{ id: "1", type: "issue", name: "SUP-142: Fix auth flow", status: "synced" },
{ id: "2", type: "pr", name: "PR #89: Add workspace sync", status: "synced" },
{
id: "2",
type: "message",
name: "#eng: Add workspace sync",
status: "synced",
},
{
id: "3",
type: "issue",
Expand Down Expand Up @@ -45,10 +51,10 @@ export function IntegrationsDemo() {
<div className="w-6 h-px bg-foreground/20" />
</div>
<div className="flex flex-col items-center gap-1.5">
<div className="w-10 h-10 rounded-lg bg-[#24292e] flex items-center justify-center">
<SiGithub className="w-5 h-5 text-white" />
<div className="w-10 h-10 rounded-lg bg-[#4A154B] flex items-center justify-center">
<FaSlack className="w-5 h-5 text-white" />
</div>
<span className="text-[10px] text-muted-foreground">GitHub</span>
<span className="text-[10px] text-muted-foreground">Slack</span>
</div>
</div>

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/components/Paywall/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const PRO_FEATURES: ProFeature[] = [
id: "integrations",
title: "Integrations",
description:
"Connect Linear, GitHub, and more to sync issues and PRs directly with your workspaces.",
"Connect Linear, Slack, and more to sync issues and conversations directly with your workspaces.",
icon: HiOutlinePuzzlePiece,
iconColor: "text-purple-500",
gradientColors: ["#7c3aed", "#6d28d9", "#4c1d95", "#1a1a2e"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const PLANS: Record<PlanTier, Plan> = {
{ id: "workspaces", name: "Up to 5 workspaces", included: true },
{ id: "local-only", name: "Local workspaces only", included: true },
{ id: "desktop-app", name: "Desktop app", included: true },
{ id: "github", name: "GitHub integration", included: true },
],
cta: { text: "Current plan", action: "current", disabled: true },
},
Expand Down
Loading