feat: add Terminal Dashboard components and state management

- Implement TerminalTabBar for session tab management with status indicators and alert badges.
- Create TerminalWorkbench to combine TerminalTabBar and TerminalInstance for terminal session display.
- Add localization support for terminal dashboard in English and Chinese.
- Develop TerminalDashboardPage for the main layout of the terminal dashboard with a three-column structure.
- Introduce Zustand stores for session management and issue/queue integration, handling session groups, terminal metadata, and alert management.
- Create a monitor web worker for off-main-thread output analysis, detecting errors and stalls in terminal sessions.
- Define TypeScript types for terminal dashboard state management and integration.
This commit is contained in:
catlog22
2026-02-14 20:54:05 +08:00
parent 4d22ae4b2f
commit e4b898f401
37 changed files with 2810 additions and 5438 deletions

View File

@@ -0,0 +1,154 @@
// ========================================
// Monitor Web Worker
// ========================================
// Off-main-thread rule-based output analysis for terminal sessions.
// MVP rules:
// 1. Keyword matching: /error|failed|exception/i -> critical alert
// 2. Stall detection: no output for > 60s -> warning alert
//
// Message protocol:
// IN: { type: 'output', sessionId: string, text: string }
// IN: { type: 'reset', sessionId: string } -- reset session tracking
// OUT: { type: 'alert', sessionId: string, severity: string, message: string }
// ========== Types ==========
interface OutputMessage {
type: 'output';
sessionId: string;
text: string;
}
interface ResetMessage {
type: 'reset';
sessionId: string;
}
type IncomingMessage = OutputMessage | ResetMessage;
interface AlertMessage {
type: 'alert';
sessionId: string;
severity: 'critical' | 'warning';
message: string;
}
interface KeywordRule {
pattern: RegExp;
severity: 'critical' | 'warning';
label: string;
}
interface SessionState {
lastActivity: number;
alertCount: number;
/** Track stall alert to avoid repeated notifications per stall period */
stallAlerted: boolean;
}
// ========== Rules ==========
const KEYWORD_RULES: KeywordRule[] = [
{
pattern: /error|failed|exception/i,
severity: 'critical',
label: 'error keyword',
},
];
/** Stall threshold in milliseconds (60 seconds) */
const STALL_THRESHOLD_MS = 60_000;
/** Stall check interval in milliseconds (15 seconds) */
const STALL_CHECK_INTERVAL_MS = 15_000;
// ========== State ==========
const sessions = new Map<string, SessionState>();
// ========== Helpers ==========
function getOrCreateSession(sessionId: string): SessionState {
let state = sessions.get(sessionId);
if (!state) {
state = {
lastActivity: Date.now(),
alertCount: 0,
stallAlerted: false,
};
sessions.set(sessionId, state);
}
return state;
}
function postAlert(alert: AlertMessage): void {
self.postMessage(alert);
}
// ========== Output Processing ==========
function processOutput(sessionId: string, text: string): void {
const state = getOrCreateSession(sessionId);
state.lastActivity = Date.now();
// Reset stall alert flag on new output
state.stallAlerted = false;
// Run keyword rules against text
for (const rule of KEYWORD_RULES) {
if (rule.pattern.test(text)) {
state.alertCount++;
postAlert({
type: 'alert',
sessionId,
severity: rule.severity,
message: `Detected ${rule.label} in output`,
});
// Only report first matching rule per chunk to avoid alert flood
break;
}
}
}
// ========== Stall Detection ==========
function checkStalls(): void {
const now = Date.now();
sessions.forEach((state, sessionId) => {
if (state.stallAlerted) return;
const elapsed = now - state.lastActivity;
if (elapsed > STALL_THRESHOLD_MS) {
state.stallAlerted = true;
state.alertCount++;
const seconds = Math.floor(elapsed / 1000);
postAlert({
type: 'alert',
sessionId,
severity: 'warning',
message: `Session stalled: no output for ${seconds}s`,
});
}
});
}
// ========== Message Handler ==========
self.onmessage = (event: MessageEvent<IncomingMessage>) => {
const msg = event.data;
switch (msg.type) {
case 'output':
processOutput(msg.sessionId, msg.text);
break;
case 'reset':
sessions.delete(msg.sessionId);
break;
}
};
// ========== Periodic Stall Check ==========
const _stallInterval = setInterval(checkStalls, STALL_CHECK_INTERVAL_MS);
// Cleanup on worker termination (best-effort)
self.addEventListener('close', () => {
clearInterval(_stallInterval);
});