Fix terminal rendering, persistence, and link handling (#1215)

* auto-claude: subtask-1-1 - Add fit trigger after drag-drop completes in TerminalGrid

When terminals are reordered via drag-drop, the xterm instances need to be
refitted to their containers to prevent black screens. This change:

- Dispatches a 'terminal-refit-all' custom event from TerminalGrid after
  terminal reordering completes (with 50ms delay to allow DOM update)
- Adds event listener in useXterm that triggers fit on all terminals
  when the event is received

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-1-2 - Pass fit callback from useXterm to Terminal component

- Export TerminalHandle interface from Terminal component with fit() method
- Use forwardRef and useImperativeHandle to expose fit callback to parent components
- Export SortableTerminalWrapperHandle interface with fit() method
- Forward fit callback through SortableTerminalWrapper to enable external triggering
- This allows parent components to trigger terminal resize after container changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-1-3 - Add fit trigger on expansion state change

Add useEffect that calls fit() when isExpanded prop changes. This ensures
the terminal content properly resizes to fill the container when a terminal
is expanded or collapsed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-2-1 - Add displayOrder field to TerminalSession type def

* auto-claude: subtask-2-2 - Add displayOrder field to renderer Terminal interface

- Added displayOrder?: number to Terminal interface for tab persistence
- Updated addTerminal to set displayOrder based on current array length
- Updated addRestoredTerminal to restore displayOrder from session
- Updated addExternalTerminal to set displayOrder for new terminals
- Updated reorderTerminals to update displayOrder values after drag-drop
- Updated restoreTerminalSessions to sort sessions by displayOrder

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-2-3 - Persist displayOrder when saving terminal sessions

Add displayOrder field to TerminalSession interface in the main process
terminal-session-store.ts. This field stores the UI position for ordering
terminals after drag-drop, enabling order persistence across app restarts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-2-4 - Save order after drag-drop reorder and restore on app startup

Implements terminal display order persistence:
- Added TERMINAL_UPDATE_DISPLAY_ORDERS IPC channel
- Added updateDisplayOrders method to TerminalSessionStore
- Added session-handler and terminal-manager wrapper functions
- Added IPC handler in terminal-handlers.ts
- Added ElectronAPI type and preload API method
- Updated TerminalGrid.tsx to persist order after drag-drop reorder
- Added browser mock for updateTerminalDisplayOrders

Now when terminals are reordered via drag-drop, the new order is
persisted to disk and restored when the app restarts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* auto-claude: subtask-3-1 - Add WebLinksAddon callback to open links via openExternal IPC

* fix(main): add error handling to setWindowOpenHandler shell.openExternal

The setWindowOpenHandler calls shell.openExternal without handling its promise,
causing unhandled rejection errors when the OS cannot open a URL (e.g., no
registered handler for the protocol).

While PR #1215 fixes terminal link clicks by routing them through IPC with
proper error handling, this setWindowOpenHandler is still used as a fallback
for any other window.open() calls (e.g., from third-party libraries).

This change adds a .catch() handler to gracefully log failures instead of
causing Sentry errors.

Fixes: Sentry error "No application found to open URL" in production v2.7.4

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(terminal): resolve PR review findings for persistence and security

- Preserve displayOrder when updating existing sessions to prevent tab
  order loss during periodic saves
- Sort sessions by displayOrder in handleRestoreFromDate to maintain
  user's custom tab ordering when restoring from history
- Add URL scheme allowlist (http, https, mailto) in setWindowOpenHandler
  for security hardening against malicious URL schemes
- Extract 50ms DOM update delay to TERMINAL_DOM_UPDATE_DELAY_MS constant
- Add error handling for IPC persistence calls in TerminalGrid
- Add .catch() handler to WebLinksAddon openExternal promise

---------

Co-authored-by: Test User <test@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andy
2026-01-17 21:51:44 +01:00
committed by GitHub
parent 902044695b
commit 75a3684cae
16 changed files with 307 additions and 74 deletions
+17 -2
View File
@@ -194,9 +194,24 @@ function createWindow(): void {
mainWindow?.show();
});
// Handle external links
// Handle external links with URL scheme allowlist for security
// Note: Terminal links now use IPC via WebLinksAddon callback, but this handler
// catches any other window.open() calls (e.g., from third-party libraries)
const ALLOWED_URL_SCHEMES = ['http:', 'https:', 'mailto:'];
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
try {
const url = new URL(details.url);
if (!ALLOWED_URL_SCHEMES.includes(url.protocol)) {
console.warn('[main] Blocked URL with disallowed scheme:', details.url);
return { action: 'deny' };
}
} catch {
console.warn('[main] Blocked invalid URL:', details.url);
return { action: 'deny' };
}
shell.openExternal(details.url).catch((error) => {
console.warn('[main] Failed to open external URL:', details.url, error);
});
return { action: 'deny' };
});
@@ -661,6 +661,26 @@ export function registerTerminalHandlers(
}
}
);
// Update terminal display orders after drag-drop reorder
ipcMain.handle(
IPC_CHANNELS.TERMINAL_UPDATE_DISPLAY_ORDERS,
async (
_,
projectPath: string,
orders: Array<{ terminalId: string; displayOrder: number }>
): Promise<IPCResult> => {
try {
terminalManager.updateDisplayOrders(projectPath, orders);
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update display orders'
};
}
}
);
}
/**
@@ -18,6 +18,8 @@ export interface TerminalSession {
lastActiveAt: string; // ISO timestamp
/** Associated worktree configuration (validated on restore) */
worktreeConfig?: TerminalWorktreeConfig;
/** UI display position for ordering terminals after drag-drop */
displayOrder?: number;
}
/**
@@ -256,11 +258,16 @@ export class TerminalSessionStore {
// Update existing or add new
const existingIndex = todaySessions[projectPath].findIndex(s => s.id === session.id);
if (existingIndex >= 0) {
// Preserve displayOrder from existing session if not provided in incoming session
// This prevents periodic saves (which don't include displayOrder) from losing tab order
const existingSession = todaySessions[projectPath][existingIndex];
todaySessions[projectPath][existingIndex] = {
...session,
// Limit output buffer size
outputBuffer: session.outputBuffer.slice(-MAX_OUTPUT_BUFFER),
lastActiveAt: new Date().toISOString()
lastActiveAt: new Date().toISOString(),
// Preserve existing displayOrder if incoming session doesn't have it
displayOrder: session.displayOrder ?? existingSession.displayOrder,
};
} else {
todaySessions[projectPath].push({
@@ -523,6 +530,30 @@ export class TerminalSessionStore {
this.save();
}
/**
* Update display orders for multiple terminals (after drag-drop reorder).
* This updates the displayOrder property for matching sessions in today's bucket.
*/
updateDisplayOrders(projectPath: string, orders: Array<{ terminalId: string; displayOrder: number }>): void {
const todaySessions = this.getTodaysSessions();
const sessions = todaySessions[projectPath];
if (!sessions) return;
let hasChanges = false;
for (const { terminalId, displayOrder } of orders) {
const session = sessions.find(s => s.id === terminalId);
if (session && session.displayOrder !== displayOrder) {
session.displayOrder = displayOrder;
session.lastActiveAt = new Date().toISOString();
hasChanges = true;
}
}
if (hasChanges) {
this.save();
}
}
/**
* Save a terminal session asynchronously (non-blocking)
*
@@ -282,6 +282,17 @@ export function getSessionsForDate(date: string, projectPath: string): TerminalS
return store.getSessionsForDate(date, projectPath);
}
/**
* Update display orders for terminals after drag-drop reorder
*/
export function updateDisplayOrders(
projectPath: string,
orders: Array<{ terminalId: string; displayOrder: number }>
): void {
const store = getTerminalSessionStore();
store.updateDisplayOrders(projectPath, orders);
}
/**
* Attempt to capture Claude session ID by polling the session directory.
* Uses the claim mechanism to prevent race conditions when multiple terminals
@@ -292,6 +292,16 @@ export class TerminalManager {
return SessionHandler.getSessionsForDate(date, projectPath);
}
/**
* Update display orders for terminals after drag-drop reorder
*/
updateDisplayOrders(
projectPath: string,
orders: Array<{ terminalId: string; displayOrder: number }>
): void {
SessionHandler.updateDisplayOrders(projectPath, orders);
}
/**
* Restore all sessions from a specific date
*/
@@ -60,6 +60,10 @@ export interface TerminalAPI {
rows?: number
) => Promise<IPCResult<import('../../shared/types').SessionDateRestoreResult>>;
checkTerminalPtyAlive: (terminalId: string) => Promise<IPCResult<{ alive: boolean }>>;
updateTerminalDisplayOrders: (
projectPath: string,
orders: Array<{ terminalId: string; displayOrder: number }>
) => Promise<IPCResult>;
// Terminal Worktree Operations (isolated development)
createTerminalWorktree: (request: CreateTerminalWorktreeRequest) => Promise<TerminalWorktreeResult>;
@@ -172,6 +176,12 @@ export const createTerminalAPI = (): TerminalAPI => ({
checkTerminalPtyAlive: (terminalId: string): Promise<IPCResult<{ alive: boolean }>> =>
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_CHECK_PTY_ALIVE, terminalId),
updateTerminalDisplayOrders: (
projectPath: string,
orders: Array<{ terminalId: string; displayOrder: number }>
): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_UPDATE_DISPLAY_ORDERS, projectPath, orders),
// Terminal Worktree Operations (isolated development)
createTerminalWorktree: (request: CreateTerminalWorktreeRequest): Promise<TerminalWorktreeResult> =>
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_WORKTREE_CREATE, request),
@@ -1,10 +1,19 @@
import React from 'react';
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type { Task } from '../../shared/types';
import { Terminal } from './Terminal';
import { Terminal, type TerminalHandle } from './Terminal';
import { cn } from '../lib/utils';
/**
* Handle interface exposed by SortableTerminalWrapper for external control.
* Allows parent components to trigger terminal operations like fit.
*/
export interface SortableTerminalWrapperHandle {
/** Refit the terminal to its container size */
fit: () => void;
}
interface SortableTerminalWrapperProps {
id: string;
cwd?: string;
@@ -19,65 +28,76 @@ interface SortableTerminalWrapperProps {
onToggleExpand?: () => void;
}
export function SortableTerminalWrapper({
id,
cwd,
projectPath,
isActive,
onClose,
onActivate,
tasks,
onNewTaskClick,
terminalCount,
isExpanded,
onToggleExpand,
}: SortableTerminalWrapperProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
export const SortableTerminalWrapper = forwardRef<SortableTerminalWrapperHandle, SortableTerminalWrapperProps>(
function SortableTerminalWrapper({
id,
data: {
type: 'terminal-panel',
terminalId: id,
},
});
cwd,
projectPath,
isActive,
onClose,
onActivate,
tasks,
onNewTaskClick,
terminalCount,
isExpanded,
onToggleExpand,
}, ref) {
const terminalRef = useRef<TerminalHandle>(null);
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 50 : undefined,
};
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id,
data: {
type: 'terminal-panel',
terminalId: id,
},
});
return (
<div
ref={setNodeRef}
style={style}
className={cn(
'h-full',
isDragging && 'opacity-50'
)}
{...attributes}
>
<Terminal
id={id}
cwd={cwd}
projectPath={projectPath}
isActive={isActive}
onClose={onClose}
onActivate={onActivate}
tasks={tasks}
onNewTaskClick={onNewTaskClick}
terminalCount={terminalCount}
dragHandleListeners={listeners}
isDragging={isDragging}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
</div>
);
}
// Expose fit method to parent components via ref
// This allows external triggering of terminal resize (e.g., after drag-drop reorder)
useImperativeHandle(ref, () => ({
fit: () => terminalRef.current?.fit(),
}), []);
const style = {
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 50 : undefined,
};
return (
<div
ref={setNodeRef}
style={style}
className={cn(
'h-full',
isDragging && 'opacity-50'
)}
{...attributes}
>
<Terminal
ref={terminalRef}
id={id}
cwd={cwd}
projectPath={projectPath}
isActive={isActive}
onClose={onClose}
onActivate={onActivate}
tasks={tasks}
onNewTaskClick={onNewTaskClick}
terminalCount={terminalCount}
dragHandleListeners={listeners}
isDragging={isDragging}
isExpanded={isExpanded}
onToggleExpand={onToggleExpand}
/>
</div>
);
}
);
@@ -1,4 +1,4 @@
import { useEffect, useRef, useCallback, useState, useMemo } from 'react';
import { useEffect, useRef, useCallback, useState, useMemo, forwardRef, useImperativeHandle } from 'react';
import { useDroppable, useDndContext } from '@dnd-kit/core';
import '@xterm/xterm/css/xterm.css';
import { FileDown } from 'lucide-react';
@@ -8,6 +8,7 @@ import { useSettingsStore } from '../stores/settings-store';
import { useToast } from '../hooks/use-toast';
import type { TerminalProps } from './terminal/types';
import type { TerminalWorktreeConfig } from '../../shared/types';
import { TERMINAL_DOM_UPDATE_DELAY_MS } from '../../shared/constants';
import { TerminalHeader } from './terminal/TerminalHeader';
import { CreateWorktreeDialog } from './terminal/CreateWorktreeDialog';
import { useXterm } from './terminal/useXterm';
@@ -20,7 +21,17 @@ import { useTerminalFileDrop } from './terminal/useTerminalFileDrop';
const MIN_COLS = 10;
const MIN_ROWS = 3;
export function Terminal({
/**
* Handle interface exposed by Terminal component for external control.
* Used by parent components (e.g., SortableTerminalWrapper) to trigger operations
* like refitting the terminal after container size changes.
*/
export interface TerminalHandle {
/** Refit the terminal to its container size */
fit: () => void;
}
export const Terminal = forwardRef<TerminalHandle, TerminalProps>(function Terminal({
id,
cwd,
projectPath,
@@ -34,7 +45,7 @@ export function Terminal({
isDragging,
isExpanded,
onToggleExpand,
}: TerminalProps) {
}, ref) {
const isMountedRef = useRef(true);
const isCreatedRef = useRef(false);
// Track deliberate terminal recreation (e.g., worktree switching)
@@ -103,6 +114,7 @@ export function Terminal({
const {
terminalRef,
xtermRef: _xtermRef,
fit,
write: _write, // Output now handled by useGlobalTerminalListeners
writeln,
focus,
@@ -120,6 +132,12 @@ export function Terminal({
onDimensionsReady: handleDimensionsReady,
});
// Expose fit method to parent components via ref
// This allows external triggering of terminal resize (e.g., after drag-drop reorder)
useImperativeHandle(ref, () => ({
fit,
}), [fit]);
// Use ready dimensions for PTY creation (wait until xterm has measured)
// This prevents creating PTY with default 80x24 when container is smaller
const ptyDimensions = useMemo(() => {
@@ -171,6 +189,14 @@ export function Terminal({
}
}, [isActive, focus]);
// Refit terminal when expansion state changes
useEffect(() => {
const timeoutId = setTimeout(() => {
fit();
}, TERMINAL_DOM_UPDATE_DELAY_MS);
return () => clearTimeout(timeoutId);
}, [isExpanded, fit]);
// Trigger deferred Claude resume when terminal becomes active
// This ensures Claude sessions are only resumed when the user actually views the terminal,
// preventing all terminals from resuming simultaneously on app startup (which can crash the app)
@@ -426,4 +452,4 @@ Please confirm you're ready by saying: I'm ready to work on ${selectedTask.title
)}
</div>
);
}
});
@@ -35,6 +35,7 @@ import { cn } from '../lib/utils';
import { useTerminalStore } from '../stores/terminal-store';
import { useTaskStore } from '../stores/task-store';
import { useFileExplorerStore } from '../stores/file-explorer-store';
import { TERMINAL_DOM_UPDATE_DELAY_MS } from '../../shared/constants';
import type { SessionDateInfo } from '../../shared/types';
interface TerminalGridProps {
@@ -148,11 +149,17 @@ export function TerminalGrid({ projectPath, onNewTaskClick, isActive = false }:
if (result.success && result.data) {
console.warn(`[TerminalGrid] Main process restored ${result.data.restored} sessions from ${date}`);
// Sort sessions by displayOrder before restoring to preserve user's tab ordering
const sortedSessions = [...sessionsToRestore].sort((a, b) => {
const orderA = a.displayOrder ?? Number.MAX_SAFE_INTEGER;
const orderB = b.displayOrder ?? Number.MAX_SAFE_INTEGER;
return orderA - orderB;
});
// Add each successfully restored session to the renderer's terminal store
for (const sessionResult of result.data.sessions) {
if (sessionResult.success) {
// Find the full session data
const fullSession = sessionsToRestore.find(s => s.id === sessionResult.id);
const fullSession = sortedSessions.find(s => s.id === sessionResult.id);
if (fullSession) {
console.warn(`[TerminalGrid] Adding restored terminal to store: ${fullSession.id}`);
addRestoredTerminal(fullSession);
@@ -292,6 +299,29 @@ export function TerminalGrid({ projectPath, onNewTaskClick, isActive = false }:
if (activeId !== overId && terminals.some(t => t.id === overId)) {
reorderTerminals(activeId, overId);
// Persist the new order to disk so it survives app restarts
// Use a microtask to ensure the store has updated before we read the new order
if (projectPath) {
queueMicrotask(async () => {
const updatedTerminals = useTerminalStore.getState().terminals;
const orders = updatedTerminals
.filter(t => t.projectPath === projectPath || !t.projectPath)
.map(t => ({ terminalId: t.id, displayOrder: t.displayOrder ?? 0 }));
try {
const result = await window.electronAPI.updateTerminalDisplayOrders(projectPath, orders);
if (!result.success) {
console.warn('[TerminalGrid] Failed to persist terminal order:', result.error);
}
} catch (error) {
console.warn('[TerminalGrid] Failed to persist terminal order:', error);
}
});
}
setTimeout(() => {
window.dispatchEvent(new CustomEvent('terminal-refit-all'));
}, TERMINAL_DOM_UPDATE_DELAY_MS);
}
return;
}
@@ -82,7 +82,11 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea
});
const fitAddon = new FitAddon();
const webLinksAddon = new WebLinksAddon();
const webLinksAddon = new WebLinksAddon((_event, uri) => {
window.electronAPI?.openExternal?.(uri).catch((error) => {
console.warn('[useXterm] Failed to open URL:', uri, error);
});
});
const serializeAddon = new SerializeAddon();
xterm.loadAddon(fitAddon);
@@ -341,6 +345,24 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea
}
}, [onDimensionsReady]);
// Listen for terminal refit events (triggered after drag-drop reorder)
useEffect(() => {
const handleRefitAll = () => {
if (fitAddonRef.current && xtermRef.current && terminalRef.current) {
const rect = terminalRef.current.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
fitAddonRef.current.fit();
const cols = xtermRef.current.cols;
const rows = xtermRef.current.rows;
setDimensions({ cols, rows });
}
}
};
window.addEventListener('terminal-refit-all', handleRefitAll);
return () => window.removeEventListener('terminal-refit-all', handleRefitAll);
}, []);
const fit = useCallback(() => {
if (fitAddonRef.current && xtermRef.current) {
fitAddonRef.current.fit();
@@ -88,6 +88,10 @@ export const terminalMock = {
data: { alive: false }
}),
updateTerminalDisplayOrders: async () => ({
success: true
}),
// Terminal Event Listeners (no-op in browser)
onTerminalOutput: () => () => {},
onTerminalExit: () => () => {},
@@ -93,6 +93,7 @@ export interface Terminal {
worktreeConfig?: TerminalWorktreeConfig; // Associated worktree for isolated development
isClaudeBusy?: boolean; // Whether Claude Code is actively processing (for visual indicator)
pendingClaudeResume?: boolean; // Whether this terminal has a pending Claude resume (deferred until tab activated)
displayOrder?: number; // Display order for tab persistence (lower = further left)
}
interface TerminalLayout {
@@ -159,6 +160,7 @@ export const useTerminalStore = create<TerminalState>((set, get) => ({
isClaudeMode: false,
// outputBuffer removed - managed by terminalBufferManager
projectPath,
displayOrder: state.terminals.length, // New terminals appear at the end
};
set((state) => ({
@@ -193,6 +195,8 @@ export const useTerminalStore = create<TerminalState>((set, get) => ({
projectPath: session.projectPath,
// Worktree config is validated in main process before restore
worktreeConfig: session.worktreeConfig,
// Restore displayOrder for tab position persistence (falls back to end if not set)
displayOrder: session.displayOrder ?? state.terminals.length,
};
// Restore buffer to buffer manager
@@ -234,6 +238,7 @@ export const useTerminalStore = create<TerminalState>((set, get) => ({
createdAt: new Date(),
isClaudeMode: false,
projectPath,
displayOrder: state.terminals.length, // New terminals appear at the end
};
set((state) => ({
@@ -355,8 +360,15 @@ export const useTerminalStore = create<TerminalState>((set, get) => ({
return state;
}
// Reorder terminals and update displayOrder values based on new positions
const reorderedTerminals = arrayMove(state.terminals, oldIndex, newIndex);
const terminalsWithOrder = reorderedTerminals.map((terminal, index) => ({
...terminal,
displayOrder: index,
}));
return {
terminals: arrayMove(state.terminals, oldIndex, newIndex),
terminals: terminalsWithOrder,
};
});
},
@@ -460,8 +472,16 @@ export async function restoreTerminalSessions(projectPath: string): Promise<void
return;
}
// Add terminals to the store (they'll be created in the TerminalGrid component)
for (const session of result.data) {
// Sort sessions by displayOrder before restoring (lower = further left)
// Sessions without displayOrder are placed at the end
const sortedSessions = [...result.data].sort((a, b) => {
const orderA = a.displayOrder ?? Number.MAX_SAFE_INTEGER;
const orderB = b.displayOrder ?? Number.MAX_SAFE_INTEGER;
return orderA - orderB;
});
// Add terminals to the store in correct order (they'll be created in the TerminalGrid component)
for (const session of sortedSessions) {
store.addRestoredTerminal(session);
}
@@ -3,6 +3,13 @@
* Default settings, file paths, and project structure
*/
// ============================================
// Terminal Timing Constants
// ============================================
/** Delay for DOM updates before terminal operations (refit, resize) */
export const TERMINAL_DOM_UPDATE_DELAY_MS = 50;
// ============================================
// UI Scale Constants
// ============================================
@@ -78,6 +78,7 @@ export const IPC_CHANNELS = {
TERMINAL_GET_SESSIONS_FOR_DATE: 'terminal:getSessionsForDate',
TERMINAL_RESTORE_FROM_DATE: 'terminal:restoreFromDate',
TERMINAL_CHECK_PTY_ALIVE: 'terminal:checkPtyAlive',
TERMINAL_UPDATE_DISPLAY_ORDERS: 'terminal:updateDisplayOrders', // Persist terminal display order after drag-drop reorder
// Terminal worktree operations (isolated development in worktrees)
TERMINAL_WORKTREE_CREATE: 'terminal:worktreeCreate',
+4
View File
@@ -211,6 +211,10 @@ export interface ElectronAPI {
restoreTerminalSessionsFromDate: (date: string, projectPath: string, cols?: number, rows?: number) => Promise<IPCResult<SessionDateRestoreResult>>;
saveTerminalBuffer: (terminalId: string, serialized: string) => Promise<void>;
checkTerminalPtyAlive: (terminalId: string) => Promise<IPCResult<{ alive: boolean }>>;
updateTerminalDisplayOrders: (
projectPath: string,
orders: Array<{ terminalId: string; displayOrder: number }>
) => Promise<IPCResult>;
// Terminal worktree operations (isolated development)
createTerminalWorktree: (request: CreateTerminalWorktreeRequest) => Promise<TerminalWorktreeResult>;
@@ -37,6 +37,8 @@ export interface TerminalSession {
outputBuffer: string;
createdAt: string;
lastActiveAt: string;
/** Display order for tab persistence (lower = further left) */
displayOrder?: number;
/** Associated worktree configuration (validated on restore) */
worktreeConfig?: TerminalWorktreeConfig;
}