Skip to content
Closed
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
8 changes: 1 addition & 7 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,7 @@
"@types/express": "^5.0.5",
"@types/pidusage": "^2.0.5",
"@vercel/blob": "^2.0.0",
"@xterm/addon-clipboard": "0.3.0-beta.148",
"@xterm/addon-fit": "0.12.0-beta.148",
"@xterm/addon-image": "0.10.0-beta.148",
"@xterm/addon-ligatures": "0.11.0-beta.148",
"@xterm/addon-search": "0.17.0-beta.148",
"@xterm/addon-serialize": "0.15.0-beta.148",
"@xterm/addon-unicode11": "0.10.0-beta.148",
"@xterm/addon-webgl": "0.20.0-beta.147",
"@xterm/headless": "6.1.0-beta.148",
"@xterm/xterm": "6.1.0-beta.148",
"ai": "^6.0.0",
Expand All @@ -137,6 +130,7 @@
"framer-motion": "^12.23.26",
"friendly-words": "^1.3.1",
"fuse.js": "^7.1.0",
"ghostty-web": "^0.4.0",
"highlight.js": "^11.11.1",
"http-proxy": "^1.18.1",
"idb": "^8.0.3",
Expand Down
49 changes: 31 additions & 18 deletions apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export const createTerminalRouter = () => {
isNew: result.isNew,
scrollback: result.scrollback,
wasRecovered: result.wasRecovered,
sessionGeneration: result.sessionGeneration,
// Cold restore fields (for reboot recovery)
isColdRestore: result.isColdRestore,
previousCwd: result.previousCwd,
Expand Down Expand Up @@ -437,52 +438,64 @@ export const createTerminalRouter = () => {
.input(z.string())
.subscription(({ input: paneId }) => {
return observable<
| { type: "data"; data: string }
| { type: "data"; data: string; sessionGeneration?: string }
| {
type: "exit";
exitCode: number;
signal?: number;
reason?: "killed" | "exited" | "error";
sessionGeneration?: string;
}
| { type: "disconnect"; reason: string }
| { type: "error"; error: string; code?: string }
| {
type: "error";
error: string;
code?: string;
sessionGeneration?: string;
}
>((emit) => {
if (DEBUG_TERMINAL) {
console.log(`[Terminal Stream] Subscribe: ${paneId}`);
}

let firstDataReceived = false;

const onData = (data: string) => {
const onData = (payload: {
data: string;
sessionGeneration?: string;
}) => {
if (DEBUG_TERMINAL && !firstDataReceived) {
firstDataReceived = true;
console.log(
`[Terminal Stream] First data for ${paneId}: ${data.length} bytes`,
`[Terminal Stream] First data for ${paneId}: ${payload.data.length} bytes`,
);
}
emit.next({ type: "data", data });
emit.next({
type: "data",
data: payload.data,
sessionGeneration: payload.sessionGeneration,
});
};

const onExit = (
exitCode: number,
signal?: number,
reason?: "killed" | "exited" | "error",
) => {
const onExit = (payload: {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The onExit handler signature was changed to expect a payload object, but terminal.emit in the write procedure still uses positional arguments.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/lib/trpc/routers/terminal/terminal.ts, line 480:

<comment>The `onExit` handler signature was changed to expect a `payload` object, but `terminal.emit` in the `write` procedure still uses positional arguments.</comment>

<file context>
@@ -437,52 +438,64 @@ export const createTerminalRouter = () => {
-						signal?: number,
-						reason?: "killed" | "exited" | "error",
-					) => {
+					const onExit = (payload: {
+						exitCode: number;
+						signal?: number;
</file context>
Fix with Cubic

exitCode: number;
signal?: number;
reason?: "killed" | "exited" | "error";
sessionGeneration?: string;
}) => {
// Don't emit.complete() - paneId is reused across restarts, completion would strand listeners
emit.next({ type: "exit", exitCode, signal, reason });
emit.next({ type: "exit", ...payload });
};

const onDisconnect = (reason: string) => {
emit.next({ type: "disconnect", reason });
};

const onError = (payload: { error: string; code?: string }) => {
emit.next({
type: "error",
error: payload.error,
code: payload.code,
});
};
const onError = (payload: {
error: string;
code?: string;
sessionGeneration?: string;
}) => emit.next({ type: "error", ...payload });

terminal.on(`data:${paneId}`, onData);
terminal.on(`exit:${paneId}`, onExit);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,20 @@ export function getClaudeSettingsContent(notifyPath: string): string {
hooks: {
UserPromptSubmit: [{ hooks: [{ type: "command", command: notifyPath }] }],
Stop: [{ hooks: [{ type: "command", command: notifyPath }] }],
Notification: [
{
matcher: "idle_prompt",
hooks: [{ type: "command", command: notifyPath }],
},
{
matcher: "permission_prompt",
hooks: [{ type: "command", command: notifyPath }],
},
{
matcher: "elicitation_dialog",
hooks: [{ type: "command", command: notifyPath }],
},
],
PostToolUse: [
{ matcher: "*", hooks: [{ type: "command", command: notifyPath }] },
],
Expand Down
39 changes: 39 additions & 0 deletions apps/desktop/src/main/lib/agent-setup/agent-wrappers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const {
buildCopilotWrapperExecLine,
buildWrapperScript,
createCodexWrapper,
getClaudeSettingsContent,
createMastraWrapper,
getCursorHooksJsonContent,
getCopilotHookScriptPath,
Expand Down Expand Up @@ -171,6 +172,44 @@ describe("agent-wrappers copilot", () => {
expect(wrapper).toContain('exec "$REAL_BIN" "$@"');
});

it("includes Claude Notification hooks for idle and permission prompts", () => {
const notifyPath = path.join(TEST_HOOKS_DIR, "notify.sh");
const parsed = JSON.parse(getClaudeSettingsContent(notifyPath)) as {
hooks: Record<
string,
Array<{
matcher?: string;
hooks: Array<{ type: string; command: string }>;
}>
>;
};

const notifications = parsed.hooks.Notification;
expect(Array.isArray(notifications)).toBe(true);
expect(
notifications.some(
(entry) =>
entry.matcher === "idle_prompt" &&
entry.hooks[0]?.type === "command" &&
entry.hooks[0]?.command === notifyPath,
),
).toBe(true);
expect(
notifications.some(
(entry) =>
entry.matcher === "permission_prompt" &&
entry.hooks[0]?.command === notifyPath,
),
).toBe(true);
expect(
notifications.some(
(entry) =>
entry.matcher === "elicitation_dialog" &&
entry.hooks[0]?.command === notifyPath,
),
).toBe(true);
});

it("replaces stale Cursor hook commands from old superset paths", () => {
const cursorHooksPath = path.join(mockedHomeDir, ".cursor", "hooks.json");
const staleHookPath = "/tmp/.superset-old/hooks/cursor-hook.sh";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,55 @@ else
INPUT=$(cat)
fi

INPUT_COMPACT=$(printf '%s' "$INPUT" | tr '\n' ' ')

extract_json_field() {
KEY="$1"

if command -v jq >/dev/null 2>&1; then
VALUE=$(printf '%s' "$INPUT" | jq -r --arg key "$KEY" 'if type == "object" then .[$key] // empty else empty end' 2>/dev/null | head -n 1)
if [ -n "$VALUE" ] && [ "$VALUE" != "null" ]; then
printf '%s' "$VALUE"
return
fi
fi

printf '%s' "$INPUT_COMPACT" | grep -oE "\"${KEY}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -n 1 | sed -E 's/.*:[[:space:]]*"([^"]*)"/\1/'
}

# Extract Mastra session ID when available (mastracode hooks)
SESSION_ID=$(echo "$INPUT" | grep -oE '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"')
SESSION_ID=$(extract_json_field "session_id")

# Skip if this isn't a Superset terminal hook and no Mastra session context exists
[ -z "$SUPERSET_TAB_ID" ] && [ -z "$SESSION_ID" ] && exit 0

# Extract event type - Claude uses "hook_event_name", Codex uses "type"
# Use flexible pattern to handle optional whitespace: "key": "value" or "key":"value"
EVENT_TYPE=$(echo "$INPUT" | grep -oE '"hook_event_name"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"')
# Use flexible parsing to handle pretty JSON with newlines or spaces.
EVENT_TYPE=$(extract_json_field "hook_event_name")
if [ -z "$EVENT_TYPE" ]; then
# Check for Codex "type" field (e.g., "agent-turn-complete")
CODEX_TYPE=$(echo "$INPUT" | grep -oE '"type"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -oE '"[^"]*"$' | tr -d '"')
CODEX_TYPE=$(extract_json_field "type")
if [ "$CODEX_TYPE" = "agent-turn-complete" ]; then
EVENT_TYPE="Stop"
fi
fi

# Claude Notification hooks include a subtype matcher (idle_prompt, permission_prompt, etc.)
NOTIFICATION_TYPE=$(extract_json_field "notification_type")
[ -z "$NOTIFICATION_TYPE" ] && NOTIFICATION_TYPE=$(extract_json_field "notificationType")
[ -z "$NOTIFICATION_TYPE" ] && NOTIFICATION_TYPE=$(extract_json_field "matcher")

if [ "$EVENT_TYPE" = "Notification" ] || [ "$EVENT_TYPE" = "notification" ]; then
case "$NOTIFICATION_TYPE" in
permission_prompt|PermissionPrompt|elicitation_dialog|ElicitationDialog)
EVENT_TYPE="PermissionRequest"
;;
idle_prompt|IdlePrompt|auth_success|AuthSuccess)
EVENT_TYPE="Stop"
;;
esac
fi

# NOTE: We intentionally do NOT default to "Stop" if EVENT_TYPE is empty.
# Parse failures should not trigger completion notifications.
# The server will ignore requests with missing eventType (forward compatibility).
Expand Down Expand Up @@ -53,7 +85,7 @@ elif [ "$SUPERSET_ENV" = "development" ] || [ "$NODE_ENV" = "development" ]; the
fi

if [ "$DEBUG_HOOKS_ENABLED" = "1" ]; then
echo "[notify-hook] event=$EVENT_TYPE sessionId=$SESSION_ID paneId=$SUPERSET_PANE_ID tabId=$SUPERSET_TAB_ID workspaceId=$SUPERSET_WORKSPACE_ID" >&2
echo "[notify-hook] event=$EVENT_TYPE notificationType=$NOTIFICATION_TYPE sessionId=$SESSION_ID paneId=$SUPERSET_PANE_ID tabId=$SUPERSET_TAB_ID workspaceId=$SUPERSET_WORKSPACE_ID" >&2
fi

# Timeouts prevent blocking agent completion if notification server is unresponsive
Expand All @@ -65,6 +97,7 @@ if [ "$DEBUG_HOOKS_ENABLED" = "1" ]; then
--data-urlencode "workspaceId=$SUPERSET_WORKSPACE_ID" \
--data-urlencode "sessionId=$SESSION_ID" \
--data-urlencode "eventType=$EVENT_TYPE" \
--data-urlencode "notificationType=$NOTIFICATION_TYPE" \
--data-urlencode "env=$SUPERSET_ENV" \
--data-urlencode "version=$SUPERSET_HOOK_VERSION" \
-o /dev/null -w "%{http_code}" 2>/dev/null)
Expand All @@ -77,6 +110,7 @@ else
--data-urlencode "workspaceId=$SUPERSET_WORKSPACE_ID" \
--data-urlencode "sessionId=$SESSION_ID" \
--data-urlencode "eventType=$EVENT_TYPE" \
--data-urlencode "notificationType=$NOTIFICATION_TYPE" \
--data-urlencode "env=$SUPERSET_ENV" \
--data-urlencode "version=$SUPERSET_HOOK_VERSION" \
> /dev/null 2>&1
Expand Down
45 changes: 31 additions & 14 deletions apps/desktop/src/main/lib/notifications/map-event-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,44 @@ export function mapEventType(
if (!eventType) {
return null;
}
const normalized = eventType.trim();
if (!normalized) {
return null;
}

const lower = normalized.toLowerCase();

if (
eventType === "Start" ||
eventType === "UserPromptSubmit" ||
eventType === "PostToolUse" ||
eventType === "PostToolUseFailure" ||
eventType === "BeforeAgent" ||
eventType === "AfterTool" ||
eventType === "sessionStart" ||
eventType === "userPromptSubmitted" ||
eventType === "postToolUse"
lower === "start" ||
lower === "userpromptsubmit" ||
lower === "posttooluse" ||
lower === "posttoolusefailure" ||
lower === "beforeagent" ||
lower === "aftertool" ||
lower === "sessionstart" ||
lower === "userpromptsubmitted"
) {
return "Start";
}
if (eventType === "PermissionRequest" || eventType === "preToolUse") {
if (
lower === "permissionrequest" ||
lower === "pretooluse" ||
lower === "permission_prompt" ||
lower === "permissionprompt" ||
lower === "elicitation_dialog" ||
lower === "elicitationdialog"
) {
return "PermissionRequest";
}
if (
eventType === "Stop" ||
eventType === "agent-turn-complete" ||
eventType === "AfterAgent" ||
eventType === "sessionEnd"
lower === "stop" ||
lower === "agent-turn-complete" ||
lower === "afteragent" ||
lower === "sessionend" ||
lower === "idle_prompt" ||
lower === "idleprompt" ||
lower === "auth_success" ||
lower === "authsuccess"
) {
return "Stop";
}
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/main/lib/notifications/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ describe("notifications/server", () => {
expect(mapEventType("PermissionRequest")).toBe("PermissionRequest");
});

it("should map Claude notification subtypes", () => {
expect(mapEventType("permission_prompt")).toBe("PermissionRequest");
expect(mapEventType("elicitation_dialog")).toBe("PermissionRequest");
expect(mapEventType("idle_prompt")).toBe("Stop");
expect(mapEventType("auth_success")).toBe("Stop");
});

it("should return null for unknown event types (forward compatibility)", () => {
expect(mapEventType("UnknownEvent")).toBeNull();
expect(mapEventType("FutureEvent")).toBeNull();
Expand Down
6 changes: 5 additions & 1 deletion apps/desktop/src/main/lib/notifications/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ app.get("/hook/complete", (req, res) => {
workspaceId,
sessionId,
eventType,
notificationType,
env: clientEnv,
version,
} = req.query;
Expand All @@ -133,7 +134,9 @@ app.get("/hook/complete", (req, res) => {
);
}

const mappedEventType = mapEventType(eventType as string | undefined);
const mappedEventType =
mapEventType(eventType as string | undefined) ??
mapEventType(notificationType as string | undefined);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Validate notificationType is a string before passing it to mapEventType; the current type assertion can trigger a runtime trim error on non-string query values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/main/lib/notifications/server.ts, line 139:

<comment>Validate `notificationType` is a string before passing it to `mapEventType`; the current type assertion can trigger a runtime `trim` error on non-string query values.</comment>

<file context>
@@ -133,7 +134,9 @@ app.get("/hook/complete", (req, res) => {
-	const mappedEventType = mapEventType(eventType as string | undefined);
+	const mappedEventType =
+		mapEventType(eventType as string | undefined) ??
+		mapEventType(notificationType as string | undefined);
 
 	// Unknown or missing eventType: return success but don't process
</file context>
Suggested change
mapEventType(notificationType as string | undefined);
mapEventType(
typeof notificationType === "string" ? notificationType : undefined,
);
Fix with Cubic

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Validate notificationType is a string before mapping; otherwise malformed query input can throw at runtime in mapEventType.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/main/lib/notifications/server.ts, line 139:

<comment>Validate `notificationType` is a string before mapping; otherwise malformed query input can throw at runtime in `mapEventType`.</comment>

<file context>
@@ -133,7 +134,9 @@ app.get("/hook/complete", (req, res) => {
-	const mappedEventType = mapEventType(eventType as string | undefined);
+	const mappedEventType =
+		mapEventType(eventType as string | undefined) ??
+		mapEventType(notificationType as string | undefined);
 
 	// Unknown or missing eventType: return success but don't process
</file context>
Fix with Cubic


// Unknown or missing eventType: return success but don't process
// This ensures forward compatibility and doesn't block the agent
Expand Down Expand Up @@ -161,6 +164,7 @@ app.get("/hook/complete", (req, res) => {
if (DEBUG_HOOKS_ENABLED) {
console.log("[notifications] hook event received", {
eventType,
notificationType: notificationType as string | undefined,
mappedEventType,
paneId: paneId as string | undefined,
tabId: tabId as string | undefined,
Expand Down
Loading