Skip to content

Commit 74bcedf

Browse files
authored
Merge pull request #11 from built-by-as/unread-indicator-fix
Improve unread indicator detection and add terminal logging
2 parents 26ab453 + d19fc88 commit 74bcedf

File tree

3 files changed

+29
-58
lines changed

3 files changed

+29
-58
lines changed

main.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {simpleGit} from "simple-git";
99
import {promisify} from "util";
1010
import {v4 as uuidv4} from "uuid";
1111
import {PersistedSession, SessionConfig} from "./types";
12+
import {isTerminalReady} from "./terminal-utils";
1213

1314
const execAsync = promisify(exec);
1415

@@ -31,12 +32,6 @@ function getWorktreeBaseDir(): string {
3132
return path.join(os.homedir(), "worktrees");
3233
}
3334

34-
function isTerminalReady(buffer: string, startPos: number = 0): boolean {
35-
const searchBuffer = buffer.slice(startPos);
36-
37-
return searchBuffer.includes("\x1b[?2004h", startPos);
38-
}
39-
4035
function savePersistedSessions(sessions: PersistedSession[]) {
4136
(store as any).set("sessions", sessions);
4237
}
@@ -139,6 +134,7 @@ function spawnMcpPoller(sessionId: string, projectDir: string) {
139134
const serverMap = new Map<string, any>();
140135

141136
ptyProcess.onData((data) => {
137+
142138
// Accumulate output without displaying it
143139
outputBuffer += data;
144140

@@ -469,6 +465,7 @@ ipcMain.on("create-session", async (event, config: SessionConfig) => {
469465
// Handle session input
470466
ipcMain.on("session-input", (_event, sessionId: string, data: string) => {
471467
const ptyProcess = activePtyProcesses.get(sessionId);
468+
472469
if (ptyProcess) {
473470
ptyProcess.write(data);
474471
}
@@ -508,6 +505,7 @@ ipcMain.on("reopen-session", (event, sessionId: string) => {
508505
// Close session (kill PTY but keep session)
509506
ipcMain.on("close-session", (_event, sessionId: string) => {
510507
const ptyProcess = activePtyProcesses.get(sessionId);
508+
511509
if (ptyProcess) {
512510
ptyProcess.kill();
513511
activePtyProcesses.delete(sessionId);

renderer.ts

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {FitAddon} from "@xterm/addon-fit";
22
import {ipcRenderer} from "electron";
33
import {Terminal} from "xterm";
44
import {PersistedSession, SessionConfig} from "./types";
5+
import {isClaudeSessionReady} from "./terminal-utils";
56

67
interface Session {
78
id: string;
@@ -12,7 +13,6 @@ interface Session {
1213
config: SessionConfig;
1314
worktreePath: string;
1415
hasActivePty: boolean;
15-
hasUnreadActivity: boolean;
1616
}
1717

1818
interface McpServer {
@@ -272,7 +272,6 @@ function addSession(persistedSession: PersistedSession, hasActivePty: boolean) {
272272
config: persistedSession.config,
273273
worktreePath: persistedSession.worktreePath,
274274
hasActivePty,
275-
hasUnreadActivity: false,
276275
};
277276

278277
sessions.set(persistedSession.id, session);
@@ -481,8 +480,6 @@ function markSessionAsUnread(sessionId: string) {
481480
const session = sessions.get(sessionId);
482481
if (!session) return;
483482

484-
session.hasUnreadActivity = true;
485-
486483
// Add unread indicator to tab
487484
const tab = document.getElementById(`tab-${sessionId}`);
488485
if (tab) {
@@ -494,8 +491,6 @@ function clearUnreadStatus(sessionId: string) {
494491
const session = sessions.get(sessionId);
495492
if (!session) return;
496493

497-
session.hasUnreadActivity = false;
498-
499494
// Remove unread indicator from tab
500495
const tab = document.getElementById(`tab-${sessionId}`);
501496
if (tab) {
@@ -537,13 +532,6 @@ function switchToSession(sessionId: string) {
537532
// Clear unread status when switching to this session
538533
clearUnreadStatus(sessionId);
539534

540-
// Clear any pending idle timer for this session (Bug 1 fix)
541-
const existingTimer = sessionIdleTimers.get(sessionId);
542-
if (existingTimer) {
543-
clearTimeout(existingTimer);
544-
sessionIdleTimers.delete(sessionId);
545-
}
546-
547535
// Focus and resize
548536
session.terminal.focus();
549537
// Dispatch resize event to trigger terminal resize
@@ -575,13 +563,6 @@ function closeSession(sessionId: string) {
575563
// Update UI indicator
576564
updateSessionState(sessionId, false);
577565

578-
// Clean up idle timer (Bug 2 fix)
579-
const existingTimer = sessionIdleTimers.get(sessionId);
580-
if (existingTimer) {
581-
clearTimeout(existingTimer);
582-
sessionIdleTimers.delete(sessionId);
583-
}
584-
585566
// Close PTY in main process
586567
ipcRenderer.send("close-session", sessionId);
587568

@@ -623,13 +604,6 @@ function deleteSession(sessionId: string) {
623604
// Remove from sessions map
624605
sessions.delete(sessionId);
625606

626-
// Clean up idle timer (Bug 2 fix)
627-
const existingTimer = sessionIdleTimers.get(sessionId);
628-
if (existingTimer) {
629-
clearTimeout(existingTimer);
630-
sessionIdleTimers.delete(sessionId);
631-
}
632-
633607
// Delete in main process (handles worktree removal)
634608
ipcRenderer.send("delete-session", sessionId);
635609

@@ -649,10 +623,6 @@ function deleteSession(sessionId: string) {
649623
}
650624
}
651625

652-
// Track idle timers per session to detect when output stops (Claude is done)
653-
const sessionIdleTimers = new Map<string, NodeJS.Timeout>();
654-
const IDLE_DELAY_MS = 500; // 0.5 seconds of no output = Claude is done
655-
656626
// Handle session output
657627
ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => {
658628
const session = sessions.get(sessionId);
@@ -664,27 +634,10 @@ ipcRenderer.on("session-output", (_event, sessionId: string, data: string) => {
664634
session.terminal.write(filteredData);
665635

666636
// Only mark as unread if this is not the active session
667-
if (activeSessionId !== sessionId && session.hasActivePty && !session.hasUnreadActivity) {
668-
// Only track substantive output (ignore cursor movements, keepalives, etc)
669-
// Look for actual text content or common escape sequences that indicate real output
670-
const hasSubstantiveOutput = /[a-zA-Z0-9]/.test(filteredData) ||
671-
filteredData.includes('\n') ||
672-
filteredData.includes('\r');
673-
674-
if (hasSubstantiveOutput) {
675-
// Clear any existing idle timer
676-
const existingTimer = sessionIdleTimers.get(sessionId);
677-
if (existingTimer) {
678-
clearTimeout(existingTimer);
679-
}
680-
681-
// Set a new timer - if no output for IDLE_DELAY_MS, mark as unread
682-
const timer = setTimeout(() => {
683-
markSessionAsUnread(sessionId);
684-
sessionIdleTimers.delete(sessionId);
685-
}, IDLE_DELAY_MS);
686-
687-
sessionIdleTimers.set(sessionId, timer);
637+
if (activeSessionId !== sessionId && session.hasActivePty) {
638+
// Check if Claude session is ready for input
639+
if (isClaudeSessionReady(filteredData)) {
640+
markSessionAsUnread(sessionId);
688641
}
689642
}
690643
}

terminal-utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Terminal escape sequences used for detecting terminal state
2+
3+
// Bracketed paste mode enable - indicates terminal is ready for input
4+
export const BRACKETED_PASTE_MODE_ENABLE = "\x1b[?2004h";
5+
6+
// Pattern that indicates Claude interactive session is done and waiting for input
7+
// Looks for: >\r\n (empty prompt with no space, no suggestion text)
8+
const CLAUDE_READY_PROMPT_PATTERN = />\r\n/;
9+
10+
// Check if a normal shell terminal is ready for input
11+
// Used during terminal initialization in main.ts
12+
export function isTerminalReady(buffer: string, startPos: number = 0): boolean {
13+
return buffer.includes(BRACKETED_PASTE_MODE_ENABLE, startPos);
14+
}
15+
16+
// Check if Claude interactive session is done and ready for input
17+
// Used for unread indicator detection in renderer.ts
18+
export function isClaudeSessionReady(buffer: string): boolean {
19+
return CLAUDE_READY_PROMPT_PATTERN.test(buffer);
20+
}

0 commit comments

Comments
 (0)