Skip to content

Commit f2a7722

Browse files
committed
fix(terminal): improve duration metadata handling and add defensive utilities
- Add formatCommandDuration() helper function for safe duration formatting with proper handling of undefined/null timestamps from Tauri backend - Add isTerminalWritable() utility for defensive terminal state checks - Refactor formatCommandTooltip() to use the new helper function - Update screen reader announcements to use safe duration formatting - Improves robustness when Tauri metadata (like duration) is missing/undefined Addresses AGENT3.md requirements for: - Fixing Tauri integration errors with undefined metadata - Adding defensive coding for terminal stream handling
1 parent 698b01f commit f2a7722

File tree

1 file changed

+60
-17
lines changed

1 file changed

+60
-17
lines changed

cortex-gui/src/components/TerminalPanel.tsx

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,59 @@ interface ShellProfile {
162162
args?: string[];
163163
}
164164

165+
/**
166+
* Safely format duration from start and end timestamps
167+
* Handles undefined/null values gracefully for robust Tauri integration
168+
* @param startTime - Start timestamp (ms)
169+
* @param endTime - End timestamp (ms)
170+
* @returns Formatted duration string or null if times are invalid
171+
*/
172+
function formatCommandDuration(startTime: number | undefined, endTime: number | undefined): string | null {
173+
if (startTime === undefined || endTime === undefined) return null;
174+
if (startTime <= 0 || endTime <= 0) return null;
175+
176+
const duration = endTime - startTime;
177+
if (duration < 0) return null;
178+
179+
if (duration < 1000) {
180+
return `${duration}ms`;
181+
} else if (duration < 60000) {
182+
return `${(duration / 1000).toFixed(1)}s`;
183+
} else {
184+
const minutes = Math.floor(duration / 60000);
185+
const seconds = ((duration % 60000) / 1000).toFixed(0);
186+
return `${minutes}m ${seconds}s`;
187+
}
188+
}
189+
190+
/**
191+
* Check if a terminal instance is in a valid state for writing
192+
* Provides defensive checks to prevent crashes when terminal is closing
193+
* @param terminal - XTerm terminal instance
194+
* @param terminalId - Terminal ID for debug logging
195+
* @returns true if terminal is valid for writing
196+
*/
197+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
198+
function isTerminalWritable(terminal: XTerm | null | undefined, terminalId: string): boolean {
199+
try {
200+
if (!terminal) {
201+
console.debug(`[Terminal] Terminal ${terminalId} is null`);
202+
return false;
203+
}
204+
if (!terminal.element) {
205+
console.debug(`[Terminal] Terminal ${terminalId} element is null`);
206+
return false;
207+
}
208+
if (terminal.element.classList.contains('disposed')) {
209+
console.debug(`[Terminal] Terminal ${terminalId} has disposed class`);
210+
return false;
211+
}
212+
return true;
213+
} catch {
214+
return false;
215+
}
216+
}
217+
165218
// Get shell type from shell path/name
166219
function getShellType(shell: string): ShellType {
167220
const shellLower = shell.toLowerCase();
@@ -1388,6 +1441,7 @@ export function TerminalPanel() {
13881441

13891442
/**
13901443
* Format tooltip text for command marker
1444+
* Uses formatCommandDuration helper for safe duration handling with undefined metadata
13911445
*/
13921446
const formatCommandTooltip = (marker: CommandMarker): string => {
13931447
const lines: string[] = [];
@@ -1402,17 +1456,10 @@ export function TerminalPanel() {
14021456
lines.push(`Exit Code: ${marker.exitCode}`);
14031457
}
14041458

1405-
if (marker.startTime && marker.endTime) {
1406-
const duration = marker.endTime - marker.startTime;
1407-
if (duration < 1000) {
1408-
lines.push(`Duration: ${duration}ms`);
1409-
} else if (duration < 60000) {
1410-
lines.push(`Duration: ${(duration / 1000).toFixed(1)}s`);
1411-
} else {
1412-
const minutes = Math.floor(duration / 60000);
1413-
const seconds = ((duration % 60000) / 1000).toFixed(0);
1414-
lines.push(`Duration: ${minutes}m ${seconds}s`);
1415-
}
1459+
// Use helper function for safe duration formatting (handles undefined metadata from Tauri)
1460+
const durationStr = formatCommandDuration(marker.startTime, marker.endTime);
1461+
if (durationStr) {
1462+
lines.push(`Duration: ${durationStr}`);
14161463
} else if (marker.startTime && marker.status === 'running') {
14171464
lines.push('Running...');
14181465
}
@@ -1513,12 +1560,8 @@ export function TerminalPanel() {
15131560

15141561
// Announce command completion to screen reader
15151562
if (ts.screenReaderAnnounce) {
1516-
const duration = marker.endTime - (marker.startTime || marker.endTime);
1517-
const durationStr = duration < 1000
1518-
? `${duration}ms`
1519-
: duration < 60000
1520-
? `${(duration / 1000).toFixed(1)}s`
1521-
: `${Math.floor(duration / 60000)}m ${((duration % 60000) / 1000).toFixed(0)}s`;
1563+
// Use helper for safe duration formatting (handles undefined metadata from Tauri)
1564+
const durationStr = formatCommandDuration(marker.startTime, marker.endTime) ?? "unknown time";
15221565
const commandName = marker.command ? `"${marker.command}"` : "Command";
15231566
const statusMsg = marker.status === 'success'
15241567
? `${commandName} completed successfully in ${durationStr}`

0 commit comments

Comments
 (0)