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
10 changes: 10 additions & 0 deletions apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,16 @@ export const createGitOperationsRouter = () => {
},
),

fetchRemote: publicProcedure
.input(z.object({ worktreePath: z.string() }))
.mutation(async ({ input }): Promise<{ success: boolean }> => {
assertRegisteredWorktree(input.worktreePath);
const git = await getGitWithShellPath(input.worktreePath);
await git.fetch(["--prune"]);
clearStatusCacheForWorktree(input.worktreePath);
return { success: true };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}),

generateCommitMessage: publicProcedure
.input(z.object({ worktreePath: z.string() }))
.mutation(async ({ input }): Promise<{ message: string | null }> => {
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
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
101 changes: 101 additions & 0 deletions apps/desktop/src/renderer/lib/terminal/terminal-runtime-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {
attachToContainer,
createRuntime,
detachFromContainer,
disposeRuntime,
type TerminalRuntime,
} from "./terminal-runtime";
import {
type ConnectionState,
connect,
createTransport,
disposeTransport,
sendDispose,
sendResize,
type TerminalTransport,
} from "./terminal-ws-transport";

interface RegistryEntry {
runtime: TerminalRuntime;
transport: TerminalTransport;
}

class TerminalRuntimeRegistryImpl {
private entries = new Map<string, RegistryEntry>();

private getOrCreate(paneId: string): RegistryEntry {
let entry = this.entries.get(paneId);
if (entry) return entry;

entry = {
runtime: createRuntime(paneId),
transport: createTransport(),
};

this.entries.set(paneId, entry);
return entry;
}

attach(paneId: string, container: HTMLDivElement, wsUrl: string) {
const { runtime, transport } = this.getOrCreate(paneId);

attachToContainer(runtime, container, () => {
sendResize(transport, runtime.terminal.cols, runtime.terminal.rows);
});

connect(transport, runtime.terminal, wsUrl);
}

/**
* Detach the terminal from its DOM container.
*
* This only removes the DOM attachment (wrapper, resize observer, focus).
* The WebSocket and xterm data flow are intentionally kept alive so output
* written while the pane is hidden is not lost. Disposal of the transport
* happens exclusively through {@link dispose} when the paneId is removed
* from persisted pane state.
*/
detach(paneId: string) {
const entry = this.entries.get(paneId);
if (!entry) return;

detachFromContainer(entry.runtime);
}

dispose(paneId: string) {
const entry = this.entries.get(paneId);
if (!entry) return;

sendDispose(entry.transport);
disposeTransport(entry.transport);
disposeRuntime(entry.runtime);

this.entries.delete(paneId);
}

getAllPaneIds(): Set<string> {
return new Set(this.entries.keys());
}

has(paneId: string): boolean {
return this.entries.has(paneId);
}

getConnectionState(paneId: string): ConnectionState {
return (
this.entries.get(paneId)?.transport.connectionState ?? "disconnected"
);
}

onStateChange(paneId: string, listener: () => void): () => void {
const { transport } = this.getOrCreate(paneId);
transport.stateListeners.add(listener);
return () => {
transport.stateListeners.delete(listener);
};
}
}

export const terminalRuntimeRegistry = new TerminalRuntimeRegistryImpl();

export type { ConnectionState };
Loading
Loading