mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
fix: tab blinking for hook events persists through session:working
Implement pending hooks state machine to track hook events per session. Tab alerts now derive from pending hooks rather than being set directly, so session:working events don't clear alerts when prompts are still pending. - Add pendingHooks Map to track permission_prompt, elicitation_dialog, idle_prompt - Action hooks (permission/elicitation) take priority over idle for alert type - hook:stop clears all pending hooks when Claude finishes responding - selectSession() only clears idle hooks; action hooks persist until user input - User input clears all pending hooks (they've addressed the prompt) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+67
-12
@@ -474,6 +474,10 @@ class ClaudemanApp {
|
||||
// Tab alert states: Map<sessionId, 'action' | 'idle'>
|
||||
this.tabAlerts = new Map();
|
||||
|
||||
// Pending hooks per session: Map<sessionId, Set<hookType>>
|
||||
// Tracks pending hook events that need resolution (permission_prompt, elicitation_dialog, idle_prompt)
|
||||
this.pendingHooks = new Map();
|
||||
|
||||
// Terminal write batching with DEC 2026 sync support
|
||||
this.pendingWrites = '';
|
||||
this.writeFrameScheduled = false;
|
||||
@@ -527,6 +531,44 @@ class ClaudemanApp {
|
||||
return inputCost + outputCost;
|
||||
}
|
||||
|
||||
// ========== Pending Hooks State Machine ==========
|
||||
// Track pending hook events per session to determine tab alerts.
|
||||
// Action hooks (permission_prompt, elicitation_dialog) take priority over idle_prompt.
|
||||
|
||||
setPendingHook(sessionId, hookType) {
|
||||
if (!this.pendingHooks.has(sessionId)) {
|
||||
this.pendingHooks.set(sessionId, new Set());
|
||||
}
|
||||
this.pendingHooks.get(sessionId).add(hookType);
|
||||
this.updateTabAlertFromHooks(sessionId);
|
||||
}
|
||||
|
||||
clearPendingHooks(sessionId, hookType = null) {
|
||||
const hooks = this.pendingHooks.get(sessionId);
|
||||
if (!hooks) return;
|
||||
if (hookType) {
|
||||
hooks.delete(hookType);
|
||||
} else {
|
||||
hooks.clear();
|
||||
}
|
||||
if (hooks.size === 0) {
|
||||
this.pendingHooks.delete(sessionId);
|
||||
}
|
||||
this.updateTabAlertFromHooks(sessionId);
|
||||
}
|
||||
|
||||
updateTabAlertFromHooks(sessionId) {
|
||||
const hooks = this.pendingHooks.get(sessionId);
|
||||
if (!hooks || hooks.size === 0) {
|
||||
this.tabAlerts.delete(sessionId);
|
||||
} else if (hooks.has('permission_prompt') || hooks.has('elicitation_dialog')) {
|
||||
this.tabAlerts.set(sessionId, 'action');
|
||||
} else if (hooks.has('idle_prompt')) {
|
||||
this.tabAlerts.set(sessionId, 'idle');
|
||||
}
|
||||
this.renderSessionTabs();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.initTerminal();
|
||||
this.loadFontSize();
|
||||
@@ -645,12 +687,15 @@ class ClaudemanApp {
|
||||
const flushInput = () => {
|
||||
if (this._pendingInput && this.activeSessionId) {
|
||||
const input = this._pendingInput;
|
||||
const sessionId = this.activeSessionId;
|
||||
this._pendingInput = '';
|
||||
fetch(`/api/sessions/${this.activeSessionId}/input`, {
|
||||
fetch(`/api/sessions/${sessionId}/input`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ input })
|
||||
});
|
||||
// Clear pending hooks when user sends input (they've addressed the prompt)
|
||||
this.clearPendingHooks(sessionId);
|
||||
}
|
||||
this._inputFlushTimeout = null;
|
||||
};
|
||||
@@ -1175,7 +1220,10 @@ class ClaudemanApp {
|
||||
const session = this.sessions.get(data.id);
|
||||
if (session) {
|
||||
session.status = 'busy';
|
||||
this.tabAlerts.delete(data.id);
|
||||
// Only clear tab alert if no pending hooks (permission_prompt, elicitation_dialog, etc.)
|
||||
if (!this.pendingHooks.has(data.id)) {
|
||||
this.tabAlerts.delete(data.id);
|
||||
}
|
||||
this.renderSessionTabs();
|
||||
this.sendPendingCtrlL(data.id);
|
||||
}
|
||||
@@ -1541,12 +1589,14 @@ class ClaudemanApp {
|
||||
});
|
||||
|
||||
// Hook events (from Claude Code hooks system)
|
||||
// Use pendingHooks state machine to track hook events and derive tab alerts.
|
||||
// This ensures alerts persist even when session:working events fire.
|
||||
this.eventSource.addEventListener('hook:idle_prompt', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
if (data.sessionId && data.sessionId !== this.activeSessionId) {
|
||||
this.tabAlerts.set(data.sessionId, 'idle');
|
||||
this.renderSessionTabs();
|
||||
// Always track pending hook - alert will show when switching away from session
|
||||
if (data.sessionId) {
|
||||
this.setPendingHook(data.sessionId, 'idle_prompt');
|
||||
}
|
||||
this.notificationManager?.notify({
|
||||
urgency: 'warning',
|
||||
@@ -1561,9 +1611,9 @@ class ClaudemanApp {
|
||||
this.eventSource.addEventListener('hook:permission_prompt', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
if (data.sessionId && data.sessionId !== this.activeSessionId) {
|
||||
this.tabAlerts.set(data.sessionId, 'action');
|
||||
this.renderSessionTabs();
|
||||
// Always track pending hook - action alerts need user interaction to clear
|
||||
if (data.sessionId) {
|
||||
this.setPendingHook(data.sessionId, 'permission_prompt');
|
||||
}
|
||||
const toolInfo = data.tool ? `${data.tool}${data.command ? ': ' + data.command : data.file ? ': ' + data.file : ''}` : '';
|
||||
this.notificationManager?.notify({
|
||||
@@ -1579,9 +1629,9 @@ class ClaudemanApp {
|
||||
this.eventSource.addEventListener('hook:elicitation_dialog', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
if (data.sessionId && data.sessionId !== this.activeSessionId) {
|
||||
this.tabAlerts.set(data.sessionId, 'action');
|
||||
this.renderSessionTabs();
|
||||
// Always track pending hook - action alerts need user interaction to clear
|
||||
if (data.sessionId) {
|
||||
this.setPendingHook(data.sessionId, 'elicitation_dialog');
|
||||
}
|
||||
this.notificationManager?.notify({
|
||||
urgency: 'critical',
|
||||
@@ -1596,6 +1646,10 @@ class ClaudemanApp {
|
||||
this.eventSource.addEventListener('hook:stop', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const session = this.sessions.get(data.sessionId);
|
||||
// Clear all pending hooks when Claude finishes responding
|
||||
if (data.sessionId) {
|
||||
this.clearPendingHooks(data.sessionId);
|
||||
}
|
||||
this.notificationManager?.notify({
|
||||
urgency: 'info',
|
||||
category: 'hook-stop',
|
||||
@@ -2126,7 +2180,8 @@ class ClaudemanApp {
|
||||
if (this.activeSessionId === sessionId) return;
|
||||
|
||||
this.activeSessionId = sessionId;
|
||||
this.tabAlerts.delete(sessionId);
|
||||
// Clear idle hooks on view, but keep action hooks until user interacts
|
||||
this.clearPendingHooks(sessionId, 'idle_prompt');
|
||||
this.renderSessionTabs();
|
||||
|
||||
// Check if this is a restored session that needs to be attached
|
||||
|
||||
Reference in New Issue
Block a user