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.
|
||||
// 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 {
|
||||
SplitSquareHorizontal,
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
FileText,
|
||||
ArrowLeft,
|
||||
LogOut,
|
||||
WifiOff,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { TerminalInstance } from './TerminalInstance';
|
||||
@@ -64,6 +65,7 @@ const statusDotStyles: Record<TerminalStatus, string> = {
|
||||
error: 'bg-red-500',
|
||||
paused: 'bg-yellow-500',
|
||||
resuming: 'bg-blue-400 animate-pulse',
|
||||
locked: 'bg-amber-500 animate-pulse',
|
||||
};
|
||||
|
||||
// ========== Props ==========
|
||||
@@ -129,6 +131,20 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
||||
const status: TerminalStatus = meta?.status ?? 'idle';
|
||||
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
|
||||
// Use sessions from cliSessionStore directly (all sessions, not just grouped ones)
|
||||
const sessionOptions = useMemo(() => {
|
||||
@@ -261,7 +277,9 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
||||
) : (
|
||||
// Terminal mode header
|
||||
<>
|
||||
{sessionId && (
|
||||
{isSessionNotFound ? (
|
||||
<WifiOff className="w-3.5 h-3.5 text-yellow-500 shrink-0" />
|
||||
) : sessionId && (
|
||||
<span
|
||||
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" />
|
||||
</button>
|
||||
{!isFileMode && sessionId && (
|
||||
{!isFileMode && sessionId && !isSessionNotFound && (
|
||||
<>
|
||||
{/* Restart button */}
|
||||
<button
|
||||
@@ -417,6 +435,19 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
||||
className="h-full"
|
||||
/>
|
||||
</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 ? (
|
||||
// Terminal mode with session
|
||||
<div className="flex-1 min-h-0">
|
||||
|
||||
@@ -143,6 +143,8 @@
|
||||
"pane": {
|
||||
"selectSession": "Select a session",
|
||||
"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",
|
||||
"splitVertical": "Split Down",
|
||||
"clearTerminal": "Clear Terminal",
|
||||
|
||||
@@ -143,6 +143,8 @@
|
||||
"pane": {
|
||||
"selectSession": "选择会话",
|
||||
"selectSessionHint": "从下拉菜单中选择终端会话",
|
||||
"sessionNotFound": "会话已不存在",
|
||||
"sessionNotFoundHint": "该会话可能已关闭或服务器已重启,请选择其他会话。",
|
||||
"splitHorizontal": "向右分割",
|
||||
"splitVertical": "向下分割",
|
||||
"clearTerminal": "清屏",
|
||||
|
||||
@@ -361,6 +361,56 @@ export const useSessionManagerStore = create<SessionManagerStore>()(
|
||||
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' }
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface SessionLayout {
|
||||
}
|
||||
|
||||
/** 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 */
|
||||
export interface TerminalMeta {
|
||||
@@ -28,6 +28,14 @@ export interface TerminalMeta {
|
||||
status: TerminalStatus;
|
||||
/** Number of unread alerts (errors, warnings) */
|
||||
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 */
|
||||
@@ -91,6 +99,10 @@ export interface SessionManagerActions {
|
||||
restartSession: (terminalId: string) => Promise<void>;
|
||||
/** Close and terminate a terminal session permanently */
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user