Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,4 +348,4 @@ All optional:
- [ ] Types check: `pnpm run type-check`.
- [ ] Tests pass: `pnpm exec vitest run`.
- [ ] No stray build artifacts or secrets committed.
- [ ] Documented any schema or config changes impacting users.
- [ ] Documented any schema or config changes impacting users.
9 changes: 9 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on(channel, wrapped);
return () => ipcRenderer.removeListener(channel, wrapped);
},
onPtyPrUrlDetected: (listener: (event: { id: string; url: string; cwd?: string }) => void) => {
const channel = 'pty:pr-url-detected';
const wrapped = (
_: Electron.IpcRendererEvent,
data: { id: string; url: string; cwd?: string }
) => listener(data);
ipcRenderer.on(channel, wrapped);
return () => ipcRenderer.removeListener(channel, wrapped);
},
onNotificationFocusTask: (listener: (taskId: string) => void) => {
const channel = 'notification:focus-task';
const wrapped = (_: Electron.IpcRendererEvent, taskId: string) => listener(taskId);
Expand Down
16 changes: 16 additions & 0 deletions src/main/services/ptyIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
killPty,
getPty,
getPtyKind,
getPtyCwd,
startDirectPty,
startSshPty,
removePtyRecord,
Expand Down Expand Up @@ -101,6 +102,16 @@ function bufferedSendPtyData(id: string, chunk: string): void {
ptyDataTimers.set(id, t);
}

// Detect GitHub PR URLs in terminal output for instant PR status refresh
const PR_URL_RE = /https?:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/pull\/\d+/;

function maybeEmitPrUrlDetected(id: string, chunk: string): void {
const match = chunk.match(PR_URL_RE);
if (match) {
safeSendToOwner(id, 'pty:pr-url-detected', { id, url: match[0], cwd: getPtyCwd(id) });
}
}

/**
* Deterministic port in the ephemeral range (49152–65535) derived from ptyId.
* Used for the reverse SSH tunnel so the remote hook can reach the local
Expand Down Expand Up @@ -365,6 +376,7 @@ export function registerPtyIpc(): void {
listeners.delete(id); // Clear old listener registration
if (!listeners.has(id)) {
proc.onData((data) => {
maybeEmitPrUrlDetected(id, data);
bufferedSendPtyData(id, data);
});

Expand Down Expand Up @@ -444,6 +456,7 @@ export function registerPtyIpc(): void {

if (!listeners.has(id)) {
proc.onData((data) => {
maybeEmitPrUrlDetected(id, data);
bufferedSendPtyData(id, data);
});
proc.onExit(({ exitCode, signal }) => {
Expand Down Expand Up @@ -587,6 +600,7 @@ export function registerPtyIpc(): void {
// Attach data/exit listeners once per PTY id
if (!listeners.has(id)) {
proc.onData((data) => {
maybeEmitPrUrlDetected(id, data);
bufferedSendPtyData(id, data);
});

Expand Down Expand Up @@ -911,6 +925,7 @@ export function registerPtyIpc(): void {

if (!listeners.has(id)) {
proc.onData((data) => {
maybeEmitPrUrlDetected(id, data);
bufferedSendPtyData(id, data);
});
proc.onExit(({ exitCode, signal }) => {
Expand Down Expand Up @@ -1032,6 +1047,7 @@ export function registerPtyIpc(): void {

if (!listeners.has(id)) {
proc.onData((data) => {
maybeEmitPrUrlDetected(id, data);
bufferedSendPtyData(id, data);
});

Expand Down
6 changes: 5 additions & 1 deletion src/main/services/ptyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1291,7 +1291,7 @@ export async function startPty(options: {
}
}

ptys.set(id, { id, proc, kind: 'local', cols, rows, tmuxSessionName });
ptys.set(id, { id, proc, cwd: useCwd, kind: 'local', cols, rows, tmuxSessionName });
return proc;
}

Expand Down Expand Up @@ -1366,6 +1366,10 @@ export function getPtyKind(id: string): 'local' | 'ssh' | undefined {
return ptys.get(id)?.kind;
}

export function getPtyCwd(id: string): string | undefined {
return ptys.get(id)?.cwd;
}

export function getPtyTmuxSessionName(id: string): string | undefined {
return ptys.get(id)?.tmuxSessionName;
}
14 changes: 14 additions & 0 deletions src/renderer/hooks/useAutoPrRefresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@ const COOLDOWN_MS = 5000; // 5 second debounce for rapid focus/visibility events
* Auto-refreshes PR status via:
* 1. Window focus - refreshes all subscribed tasks (debounced)
* 2. Polling - refreshes active task every 30s (pauses when hidden)
* 3. PTY PR URL events - refreshes immediately when terminal output includes a PR link
*/
export function useAutoPrRefresh(activeTaskPath: string | undefined): void {
const lastFocusRefresh = useRef(0);
const lastVisibilityRefresh = useRef(0);

// Event-driven refresh when terminal output includes a GitHub PR URL
useEffect(() => {
const unsubscribe = window.electronAPI.onPtyPrUrlDetected?.((event) => {
// Skip SSH PTYs where cwd is undefined to avoid refreshing wrong task
if (!event?.cwd) return;
refreshPrStatus(event.cwd).catch(() => {});
});

return () => {
unsubscribe?.();
};
}, []);

// Window focus refresh (all subscribed tasks, debounced)
useEffect(() => {
const handleFocus = () => {
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/types/electron-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ declare global {
onAgentEvent: (
listener: (event: AgentEvent, meta: { appFocused: boolean }) => void
) => () => void;
onPtyPrUrlDetected: (
listener: (event: { id: string; url: string; cwd?: string }) => void
) => () => void;
onNotificationFocusTask: (listener: (taskId: string) => void) => () => void;
terminalGetTheme: () => Promise<{
ok: boolean;
Expand Down Expand Up @@ -1206,6 +1209,9 @@ export interface ElectronAPI {
onAgentEvent: (
listener: (event: AgentEvent, meta: { appFocused: boolean }) => void
) => () => void;
onPtyPrUrlDetected: (
listener: (event: { id: string; url: string; cwd?: string }) => void
) => () => void;
onNotificationFocusTask: (listener: (taskId: string) => void) => () => void;

// Worktree management
Expand Down