mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-26 21:03:58 +02:00
feat: add integrated terminal backend
- add a PTY manager with platform shell resolution and streamed data/exit events - expose terminal lifecycle and terminal height IPC through preload and app service - persist terminal panel height in workspace settings and document the backend contract - add backend tests and validate native packaging with bun run package Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -69,6 +69,7 @@ import {
|
||||
type SessionQueryResult,
|
||||
} from '@shared/domain/sessionLibrary';
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import type { TerminalExitInfo, TerminalSnapshot } from '@shared/domain/terminal';
|
||||
import {
|
||||
applySessionApprovalSettings,
|
||||
applySessionModelConfig,
|
||||
@@ -93,6 +94,7 @@ import {
|
||||
import {
|
||||
createSessionToolingSelection,
|
||||
listApprovalToolNames,
|
||||
normalizeTerminalHeight,
|
||||
normalizeTheme,
|
||||
resolveProjectToolingSettings,
|
||||
resolveWorkspaceToolingSettings,
|
||||
@@ -129,12 +131,15 @@ import {
|
||||
import { getStoredToken } from '@main/services/mcpTokenStore';
|
||||
import { performMcpOAuthFlow, requiresOAuth } from '@main/services/mcpOAuthService';
|
||||
import { probeServers, type McpProbeResult } from '@main/services/mcpToolProber';
|
||||
import { PtyManager } from '@main/services/ptyManager';
|
||||
|
||||
const { dialog, shell } = electron;
|
||||
|
||||
type AppServiceEvents = {
|
||||
'workspace-updated': [WorkspaceState];
|
||||
'session-event': [SessionEventRecord];
|
||||
'terminal-data': [string];
|
||||
'terminal-exit': [TerminalExitInfo];
|
||||
};
|
||||
|
||||
type PendingApprovalHandle = {
|
||||
@@ -182,6 +187,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
private readonly configScanner = new ConfigScannerRegistry();
|
||||
private readonly customizationScanner = new ProjectCustomizationScanner();
|
||||
private readonly probeMcpServers = probeServers;
|
||||
private readonly ptyManager = new PtyManager();
|
||||
private readonly pendingApprovalHandles = new Map<string, PendingApprovalHandle>();
|
||||
private readonly pendingUserInputHandles = new Map<string, PendingUserInputHandle>();
|
||||
private workspace?: WorkspaceState;
|
||||
@@ -190,6 +196,17 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
private didScheduleInitialProjectGitRefresh = false;
|
||||
private mcpProbeUpdateQueue = Promise.resolve();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.ptyManager.on('data', (data) => {
|
||||
this.emit('terminal-data', data);
|
||||
});
|
||||
this.ptyManager.on('exit', (info) => {
|
||||
this.emit('terminal-exit', info);
|
||||
});
|
||||
}
|
||||
|
||||
async describeSidecarCapabilities(): Promise<SidecarCapabilities> {
|
||||
return this.loadSidecarCapabilities();
|
||||
}
|
||||
@@ -241,6 +258,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.ptyManager.dispose();
|
||||
await this.sidecar.dispose();
|
||||
void this.secretStore;
|
||||
}
|
||||
@@ -464,6 +482,53 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async setTerminalHeight(height?: number): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const normalizedHeight = normalizeTerminalHeight(height);
|
||||
|
||||
if (normalizedHeight === undefined) {
|
||||
if (workspace.settings.terminalHeight === undefined) {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
delete workspace.settings.terminalHeight;
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
if (workspace.settings.terminalHeight === normalizedHeight) {
|
||||
return workspace;
|
||||
}
|
||||
|
||||
workspace.settings.terminalHeight = normalizedHeight;
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async describeTerminal(): Promise<TerminalSnapshot | undefined> {
|
||||
return this.ptyManager.getSnapshot();
|
||||
}
|
||||
|
||||
async createTerminal(): Promise<TerminalSnapshot> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
return this.ptyManager.create(this.resolveTerminalWorkingDirectory(workspace));
|
||||
}
|
||||
|
||||
async restartTerminal(): Promise<TerminalSnapshot> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
return this.ptyManager.restart(this.resolveTerminalWorkingDirectory(workspace));
|
||||
}
|
||||
|
||||
async killTerminal(): Promise<void> {
|
||||
this.ptyManager.kill();
|
||||
}
|
||||
|
||||
writeTerminal(data: string): void {
|
||||
this.ptyManager.write(data);
|
||||
}
|
||||
|
||||
resizeTerminal(cols: number, rows: number): void {
|
||||
this.ptyManager.resize(cols, rows);
|
||||
}
|
||||
|
||||
async deletePattern(patternId: string): Promise<WorkspaceState> {
|
||||
if (isBuiltinPattern(patternId)) {
|
||||
throw new Error('Built-in patterns cannot be deleted.');
|
||||
@@ -1317,6 +1382,25 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
return project;
|
||||
}
|
||||
|
||||
private resolveTerminalWorkingDirectory(workspace: WorkspaceState): string {
|
||||
const selectedSession = workspace.selectedSessionId
|
||||
? workspace.sessions.find((session) => session.id === workspace.selectedSessionId)
|
||||
: undefined;
|
||||
if (selectedSession) {
|
||||
const project = this.requireProject(workspace, selectedSession.projectId);
|
||||
return selectedSession.cwd ?? project.path;
|
||||
}
|
||||
|
||||
const selectedProject = workspace.selectedProjectId
|
||||
? workspace.projects.find((project) => project.id === workspace.selectedProjectId)
|
||||
: workspace.projects[0];
|
||||
if (!selectedProject) {
|
||||
throw new Error('Open a project or session before starting the integrated terminal.');
|
||||
}
|
||||
|
||||
return selectedProject.path;
|
||||
}
|
||||
|
||||
private async refreshGitContextForProject(project: ProjectRecord): Promise<boolean> {
|
||||
if (isScratchpadProject(project)) {
|
||||
if (!project.git) {
|
||||
|
||||
Reference in New Issue
Block a user