fix: clients sync to server terminals instead of creating new ones
- Remove auto-creation of terminal sessions from init/selectSession/switchAgent - Clients only connect to existing alive terminals from server registry - Remove localStorage persistence (agent/sessionId) — state derived from server - Refine session-state types: new AgentStatus values, LastError interface - UI improvements: AgentBadge, ChatContainer, UserInput, BashCard updates - Simplify claude-hook routes, update session-state service
This commit is contained in:
@@ -6,14 +6,15 @@
|
||||
|
||||
export type AgentStatus =
|
||||
| 'idle'
|
||||
| 'processing'
|
||||
| 'thinking'
|
||||
| 'reading'
|
||||
| 'writing'
|
||||
| 'toolUse'
|
||||
| 'toolDone'
|
||||
| 'permissionRequest'
|
||||
| 'notification'
|
||||
| 'interrupted'
|
||||
| 'error'
|
||||
| 'sessionStart'
|
||||
| 'sessionEnd'
|
||||
|
||||
export interface ActiveTool {
|
||||
name: string
|
||||
@@ -49,6 +50,13 @@ export interface AgentTerminalInfo {
|
||||
connectedClients: number
|
||||
}
|
||||
|
||||
export interface LastError {
|
||||
tool: string
|
||||
message: string
|
||||
interrupted: boolean
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface AgentSessionState {
|
||||
agent: string
|
||||
sessionId: string | null
|
||||
@@ -60,6 +68,7 @@ export interface AgentSessionState {
|
||||
currentTool: ActiveTool | null
|
||||
lastActivity: number
|
||||
lastStopResponse: string | null
|
||||
lastError: LastError | null
|
||||
pendingApprovals: PendingApproval[]
|
||||
terminal: AgentTerminalInfo
|
||||
notifications: SessionNotification[]
|
||||
@@ -82,6 +91,9 @@ export interface HookPayload {
|
||||
tool_use_id?: string
|
||||
assistant_response?: string
|
||||
agent_name?: string
|
||||
is_interrupt?: boolean
|
||||
error?: string
|
||||
reason?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
@@ -107,9 +119,11 @@ const TTL_WARNING = 5000
|
||||
|
||||
const NOTIFICATION_MAP: Record<string, { title: string, type: SessionNotification['type'], persistent?: boolean }> = {
|
||||
SessionStart: { title: 'Session started', type: 'info' },
|
||||
SessionEnd: { title: 'Session ended', type: 'info' },
|
||||
UserPromptSubmit: { title: 'Processing prompt', type: 'info' },
|
||||
PreToolUse: { title: 'Using tool', type: 'info' },
|
||||
PostToolUse: { title: 'Tool complete', type: 'success' },
|
||||
PostToolUseFailure: { title: 'Tool failed', type: 'error' },
|
||||
PermissionRequest: { title: 'Permission required', type: 'warning', persistent: true },
|
||||
Notification: { title: 'Notification', type: 'info' },
|
||||
Stop: { title: 'Session stopped', type: 'info' },
|
||||
@@ -149,15 +163,17 @@ function buildNotification(payload: HookPayload): SessionNotification {
|
||||
|
||||
// ── Status derivation (moved from claude-hook.ts) ──
|
||||
|
||||
export function deriveStatus(payload: HookPayload): { status: AgentStatus, tool?: string } {
|
||||
export function deriveStatus(payload: HookPayload): { status: AgentStatus, tool?: string } | null {
|
||||
const event = payload.hook_event_name
|
||||
const toolName = payload.tool_name
|
||||
|
||||
switch (event) {
|
||||
case 'SessionStart':
|
||||
return { status: 'sessionStart' }
|
||||
case 'SessionEnd':
|
||||
return { status: 'sessionEnd' }
|
||||
case 'UserPromptSubmit':
|
||||
return { status: 'processing' }
|
||||
return { status: 'thinking' }
|
||||
case 'PreToolUse':
|
||||
if (toolName && /^(Read|Glob|Grep)$/.test(toolName))
|
||||
return { status: 'reading', tool: toolName }
|
||||
@@ -165,15 +181,18 @@ export function deriveStatus(payload: HookPayload): { status: AgentStatus, tool?
|
||||
return { status: 'writing', tool: toolName }
|
||||
return { status: 'toolUse', tool: toolName }
|
||||
case 'PostToolUse':
|
||||
return { status: 'toolDone', tool: toolName }
|
||||
return { status: 'thinking', tool: toolName }
|
||||
case 'PostToolUseFailure':
|
||||
if (payload.is_interrupt) return { status: 'interrupted', tool: toolName }
|
||||
return { status: 'error', tool: toolName }
|
||||
case 'PermissionRequest':
|
||||
return { status: 'permissionRequest', tool: toolName }
|
||||
case 'Notification':
|
||||
return { status: 'notification' }
|
||||
return null // Side-effect only, does not change agent status
|
||||
case 'Stop':
|
||||
return { status: 'idle' }
|
||||
default:
|
||||
return { status: 'processing' }
|
||||
return { status: 'thinking' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +210,7 @@ function createDefaultState(agent: string): AgentSessionState {
|
||||
currentTool: null,
|
||||
lastActivity: Date.now(),
|
||||
lastStopResponse: null,
|
||||
lastError: null,
|
||||
pendingApprovals: [],
|
||||
terminal: {
|
||||
ptySessionId: null,
|
||||
@@ -220,15 +240,52 @@ class SessionStateManager {
|
||||
const state = this.getOrCreateAgent(agentName)
|
||||
const now = Date.now()
|
||||
|
||||
// Derive status
|
||||
const { status, tool } = deriveStatus(payload)
|
||||
// Derive status (null means side-effect only, e.g. Notification)
|
||||
const derived = deriveStatus(payload)
|
||||
|
||||
// Build patch
|
||||
const patch: Partial<AgentSessionState> = {
|
||||
status,
|
||||
lastActivity: now,
|
||||
}
|
||||
|
||||
// Only update status/tool if deriveStatus returned a result
|
||||
if (derived) {
|
||||
const { status, tool } = derived
|
||||
patch.status = status
|
||||
|
||||
// Current tool tracking
|
||||
if (status === 'toolUse' || status === 'reading' || status === 'writing' || status === 'permissionRequest') {
|
||||
patch.currentTool = {
|
||||
name: payload.tool_name || 'unknown',
|
||||
input: payload.tool_input,
|
||||
startedAt: now,
|
||||
}
|
||||
} else if (status === 'thinking' || status === 'idle' || status === 'sessionEnd') {
|
||||
patch.currentTool = null
|
||||
}
|
||||
|
||||
// Track errors from PostToolUseFailure
|
||||
if (status === 'error' || status === 'interrupted') {
|
||||
patch.lastError = {
|
||||
tool: payload.tool_name || 'unknown',
|
||||
message: (payload.error as string) || '',
|
||||
interrupted: status === 'interrupted',
|
||||
timestamp: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Clear lastError on successful flow resumption
|
||||
if (status === 'thinking' || status === 'reading' || status === 'writing' || status === 'toolUse') {
|
||||
patch.lastError = null
|
||||
}
|
||||
|
||||
// SessionEnd: clean up session state
|
||||
if (payload.hook_event_name === 'SessionEnd') {
|
||||
patch.currentTool = null
|
||||
patch.pendingApprovals = []
|
||||
}
|
||||
}
|
||||
|
||||
// Update session identity fields
|
||||
if (payload.session_id) patch.sessionId = payload.session_id
|
||||
if (payload.model) patch.model = payload.model as string
|
||||
@@ -236,17 +293,6 @@ class SessionStateManager {
|
||||
if (payload.transcript_path) patch.transcriptPath = payload.transcript_path as string
|
||||
if (payload.permission_mode) patch.permissionMode = payload.permission_mode as string
|
||||
|
||||
// Current tool tracking
|
||||
if (status === 'toolUse' || status === 'reading' || status === 'writing' || status === 'permissionRequest') {
|
||||
patch.currentTool = {
|
||||
name: payload.tool_name || 'unknown',
|
||||
input: payload.tool_input,
|
||||
startedAt: now,
|
||||
}
|
||||
} else if (status === 'toolDone' || status === 'idle' || status === 'processing') {
|
||||
patch.currentTool = null
|
||||
}
|
||||
|
||||
// Last stop response
|
||||
if (payload.hook_event_name === 'Stop' && payload.assistant_response) {
|
||||
patch.lastStopResponse = payload.assistant_response as string
|
||||
@@ -255,6 +301,7 @@ class SessionStateManager {
|
||||
// Reset session fields on SessionStart
|
||||
if (payload.hook_event_name === 'SessionStart') {
|
||||
patch.lastStopResponse = null
|
||||
patch.lastError = null
|
||||
patch.pendingApprovals = []
|
||||
}
|
||||
|
||||
@@ -290,9 +337,9 @@ class SessionStateManager {
|
||||
const idx = state.pendingApprovals.findIndex(a => a.requestId === requestId)
|
||||
if (idx !== -1) {
|
||||
state.pendingApprovals = state.pendingApprovals.filter(a => a.requestId !== requestId)
|
||||
// If no more pending approvals, go back to processing
|
||||
// If no more pending approvals, go back to thinking
|
||||
if (state.pendingApprovals.length === 0 && state.status === 'permissionRequest') {
|
||||
state.status = 'processing'
|
||||
state.status = 'thinking'
|
||||
}
|
||||
state.lastActivity = Date.now()
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user