mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat(terminal-dashboard): improve UX for pane/session management
- Add confirmation dialog when closing pane with active session - Add explicit "Close Session" button to terminate backend PTY - Handle "session not found" scenario with user-friendly message - Add i18n keys for new UI elements (en/zh)
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
// Renders within the TerminalGrid recursive layout.
|
// Renders within the TerminalGrid recursive layout.
|
||||||
// File preview is triggered from right sidebar FileSidebarPanel.
|
// File preview is triggered from right sidebar FileSidebarPanel.
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import {
|
import {
|
||||||
SplitSquareHorizontal,
|
SplitSquareHorizontal,
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
WifiOff,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { TerminalInstance } from './TerminalInstance';
|
import { TerminalInstance } from './TerminalInstance';
|
||||||
@@ -64,6 +65,7 @@ const statusDotStyles: Record<TerminalStatus, string> = {
|
|||||||
error: 'bg-red-500',
|
error: 'bg-red-500',
|
||||||
paused: 'bg-yellow-500',
|
paused: 'bg-yellow-500',
|
||||||
resuming: 'bg-blue-400 animate-pulse',
|
resuming: 'bg-blue-400 animate-pulse',
|
||||||
|
locked: 'bg-amber-500 animate-pulse',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ========== Props ==========
|
// ========== Props ==========
|
||||||
@@ -129,6 +131,20 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
const status: TerminalStatus = meta?.status ?? 'idle';
|
const status: TerminalStatus = meta?.status ?? 'idle';
|
||||||
const alertCount = meta?.alertCount ?? 0;
|
const alertCount = meta?.alertCount ?? 0;
|
||||||
|
|
||||||
|
// Check if session exists - handles case where pane references a sessionId
|
||||||
|
// that no longer exists in cliSessionStore (e.g., after page refresh if backend restarted)
|
||||||
|
const isSessionNotFound = sessionId && !sessions[sessionId];
|
||||||
|
|
||||||
|
// Clear invalid sessionId after showing the message
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSessionNotFound) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
assignSession(paneId, null);
|
||||||
|
}, 3000); // Clear after 3 seconds to let user see the message
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isSessionNotFound, paneId, assignSession]);
|
||||||
|
|
||||||
// Build session options for dropdown
|
// Build session options for dropdown
|
||||||
// Use sessions from cliSessionStore directly (all sessions, not just grouped ones)
|
// Use sessions from cliSessionStore directly (all sessions, not just grouped ones)
|
||||||
const sessionOptions = useMemo(() => {
|
const sessionOptions = useMemo(() => {
|
||||||
@@ -261,7 +277,9 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
) : (
|
) : (
|
||||||
// Terminal mode header
|
// Terminal mode header
|
||||||
<>
|
<>
|
||||||
{sessionId && (
|
{isSessionNotFound ? (
|
||||||
|
<WifiOff className="w-3.5 h-3.5 text-yellow-500 shrink-0" />
|
||||||
|
) : sessionId && (
|
||||||
<span
|
<span
|
||||||
className={cn('w-2 h-2 rounded-full shrink-0', statusDotStyles[status])}
|
className={cn('w-2 h-2 rounded-full shrink-0', statusDotStyles[status])}
|
||||||
/>
|
/>
|
||||||
@@ -314,7 +332,7 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
>
|
>
|
||||||
<SplitSquareVertical className="w-3.5 h-3.5" />
|
<SplitSquareVertical className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
{!isFileMode && sessionId && (
|
{!isFileMode && sessionId && !isSessionNotFound && (
|
||||||
<>
|
<>
|
||||||
{/* Restart button */}
|
{/* Restart button */}
|
||||||
<button
|
<button
|
||||||
@@ -417,6 +435,19 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
className="h-full"
|
className="h-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : isSessionNotFound ? (
|
||||||
|
// Session not found state - pane references a sessionId that no longer exists
|
||||||
|
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
||||||
|
<div className="text-center px-4">
|
||||||
|
<WifiOff className="h-6 w-6 mx-auto mb-1.5 text-yellow-500" />
|
||||||
|
<p className="text-sm font-medium text-foreground">
|
||||||
|
{formatMessage({ id: 'terminalDashboard.pane.sessionNotFound' })}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs mt-1 opacity-70 max-w-[200px]">
|
||||||
|
{formatMessage({ id: 'terminalDashboard.pane.sessionNotFoundHint' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
) : sessionId ? (
|
) : sessionId ? (
|
||||||
// Terminal mode with session
|
// Terminal mode with session
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
|
|||||||
@@ -143,6 +143,8 @@
|
|||||||
"pane": {
|
"pane": {
|
||||||
"selectSession": "Select a session",
|
"selectSession": "Select a session",
|
||||||
"selectSessionHint": "Choose a terminal session from the dropdown",
|
"selectSessionHint": "Choose a terminal session from the dropdown",
|
||||||
|
"sessionNotFound": "Session no longer exists",
|
||||||
|
"sessionNotFoundHint": "The session may have been closed or the server restarted. Please select another session.",
|
||||||
"splitHorizontal": "Split Right",
|
"splitHorizontal": "Split Right",
|
||||||
"splitVertical": "Split Down",
|
"splitVertical": "Split Down",
|
||||||
"clearTerminal": "Clear Terminal",
|
"clearTerminal": "Clear Terminal",
|
||||||
|
|||||||
@@ -143,6 +143,8 @@
|
|||||||
"pane": {
|
"pane": {
|
||||||
"selectSession": "选择会话",
|
"selectSession": "选择会话",
|
||||||
"selectSessionHint": "从下拉菜单中选择终端会话",
|
"selectSessionHint": "从下拉菜单中选择终端会话",
|
||||||
|
"sessionNotFound": "会话已不存在",
|
||||||
|
"sessionNotFoundHint": "该会话可能已关闭或服务器已重启,请选择其他会话。",
|
||||||
"splitHorizontal": "向右分割",
|
"splitHorizontal": "向右分割",
|
||||||
"splitVertical": "向下分割",
|
"splitVertical": "向下分割",
|
||||||
"clearTerminal": "清屏",
|
"clearTerminal": "清屏",
|
||||||
|
|||||||
@@ -361,6 +361,56 @@ export const useSessionManagerStore = create<SessionManagerStore>()(
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ========== Session Lock Actions ==========
|
||||||
|
|
||||||
|
lockSession: (sessionId: string, reason: string, executionId?: string) => {
|
||||||
|
set(
|
||||||
|
(state) => {
|
||||||
|
const existing = state.terminalMetas[sessionId];
|
||||||
|
if (!existing) return state;
|
||||||
|
return {
|
||||||
|
terminalMetas: {
|
||||||
|
...state.terminalMetas,
|
||||||
|
[sessionId]: {
|
||||||
|
...existing,
|
||||||
|
status: 'locked' as TerminalStatus,
|
||||||
|
isLocked: true,
|
||||||
|
lockReason: reason,
|
||||||
|
lockedByExecutionId: executionId,
|
||||||
|
lockedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
'lockSession'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
unlockSession: (sessionId: string) => {
|
||||||
|
set(
|
||||||
|
(state) => {
|
||||||
|
const existing = state.terminalMetas[sessionId];
|
||||||
|
if (!existing) return state;
|
||||||
|
return {
|
||||||
|
terminalMetas: {
|
||||||
|
...state.terminalMetas,
|
||||||
|
[sessionId]: {
|
||||||
|
...existing,
|
||||||
|
status: 'active' as TerminalStatus,
|
||||||
|
isLocked: false,
|
||||||
|
lockReason: undefined,
|
||||||
|
lockedByExecutionId: undefined,
|
||||||
|
lockedAt: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
'unlockSession'
|
||||||
|
);
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{ name: 'SessionManagerStore' }
|
{ name: 'SessionManagerStore' }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export interface SessionLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Terminal status indicator */
|
/** Terminal status indicator */
|
||||||
export type TerminalStatus = 'active' | 'idle' | 'error' | 'paused' | 'resuming';
|
export type TerminalStatus = 'active' | 'idle' | 'error' | 'paused' | 'resuming' | 'locked';
|
||||||
|
|
||||||
/** Metadata for a terminal instance in the dashboard */
|
/** Metadata for a terminal instance in the dashboard */
|
||||||
export interface TerminalMeta {
|
export interface TerminalMeta {
|
||||||
@@ -28,6 +28,14 @@ export interface TerminalMeta {
|
|||||||
status: TerminalStatus;
|
status: TerminalStatus;
|
||||||
/** Number of unread alerts (errors, warnings) */
|
/** Number of unread alerts (errors, warnings) */
|
||||||
alertCount: number;
|
alertCount: number;
|
||||||
|
/** Whether the session is locked (executing a workflow) */
|
||||||
|
isLocked?: boolean;
|
||||||
|
/** Reason for the lock (e.g., workflow name) */
|
||||||
|
lockReason?: string;
|
||||||
|
/** Execution ID that locked this session */
|
||||||
|
lockedByExecutionId?: string;
|
||||||
|
/** Timestamp when the session was locked */
|
||||||
|
lockedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Group of terminal sessions */
|
/** Group of terminal sessions */
|
||||||
@@ -91,6 +99,10 @@ export interface SessionManagerActions {
|
|||||||
restartSession: (terminalId: string) => Promise<void>;
|
restartSession: (terminalId: string) => Promise<void>;
|
||||||
/** Close and terminate a terminal session permanently */
|
/** Close and terminate a terminal session permanently */
|
||||||
closeSession: (terminalId: string) => Promise<void>;
|
closeSession: (terminalId: string) => Promise<void>;
|
||||||
|
/** Lock a session to prevent user input during workflow execution */
|
||||||
|
lockSession: (sessionId: string, reason: string, executionId?: string) => void;
|
||||||
|
/** Unlock a session after workflow execution completes */
|
||||||
|
unlockSession: (sessionId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SessionManagerStore = SessionManagerState & SessionManagerActions;
|
export type SessionManagerStore = SessionManagerState & SessionManagerActions;
|
||||||
|
|||||||
Reference in New Issue
Block a user