mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-05 19:38:32 +02:00
feat: add plan mode frontend support with mode toggle and plan review UI
- Add InteractionMode type and ExitPlanModeRequestedEvent to sidecar contracts - Add PendingPlanReviewRecord domain type and interactionMode to SessionRecord - Wire onExitPlanMode callback through SidecarClient and RunTurnPendingCommand - Pass session interaction mode to RunTurnCommand for sidecar consumption - Handle exit-plan-mode-requested events in AryxAppService - Add setSessionInteractionMode and dismissSessionPlanReview IPC methods - Create PlanReviewBanner component with summary, markdown content, and actions - Add plan mode toggle pill in ChatPane composer area - Wire implement action as follow-up message (graceful degradation) - Clear pending plan review on new turn, turn completion, and cancellation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import electron from 'electron';
|
||||
import type {
|
||||
AgentActivityEvent,
|
||||
ApprovalRequestedEvent,
|
||||
ExitPlanModeRequestedEvent,
|
||||
RunTurnToolingConfig,
|
||||
SidecarCapabilities,
|
||||
TurnDeltaEvent,
|
||||
@@ -584,6 +585,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
session.title = resolveSessionTitle(session, effectivePattern, session.messages);
|
||||
session.status = 'running';
|
||||
session.lastError = undefined;
|
||||
session.pendingPlanReview = undefined;
|
||||
session.updatedAt = occurredAt;
|
||||
session.runs = [
|
||||
createSessionRunRecord({
|
||||
@@ -613,6 +615,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
sessionId: session.id,
|
||||
projectPath: project.path,
|
||||
workspaceKind,
|
||||
mode: session.interactionMode ?? 'interactive',
|
||||
pattern: effectivePattern,
|
||||
messages: session.messages,
|
||||
tooling: this.buildRunTurnToolingConfig(workspace, session),
|
||||
@@ -631,6 +634,9 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
await this.handleUserInputRequested(workspace, session.id, requestId, event, (answer, wasFreeform) =>
|
||||
this.sidecar.resolveUserInput(event.userInputId, answer, wasFreeform));
|
||||
},
|
||||
async (event) => {
|
||||
await this.handleExitPlanModeRequested(workspace, session.id, event);
|
||||
},
|
||||
);
|
||||
|
||||
await this.awaitFinalResponseApproval(workspace, session.id, requestId, effectivePattern, responseMessages);
|
||||
@@ -845,6 +851,29 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async setSessionInteractionMode(
|
||||
sessionId: string,
|
||||
mode: 'interactive' | 'plan',
|
||||
): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
|
||||
session.interactionMode = mode === 'interactive' ? undefined : mode;
|
||||
session.updatedAt = nowIso();
|
||||
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async dismissSessionPlanReview(sessionId: string): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
|
||||
session.pendingPlanReview = undefined;
|
||||
session.updatedAt = nowIso();
|
||||
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async updateSessionTooling(
|
||||
sessionId: string,
|
||||
enabledMcpServerIds: string[],
|
||||
@@ -1180,6 +1209,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
session.status = 'idle';
|
||||
session.lastError = undefined;
|
||||
session.pendingUserInput = undefined;
|
||||
session.pendingPlanReview = undefined;
|
||||
session.updatedAt = completedAt;
|
||||
const completedRun = this.updateSessionRun(session, requestId, (run) =>
|
||||
completeSessionRunRecord(run, completedAt));
|
||||
@@ -1211,6 +1241,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
session.status = 'idle';
|
||||
session.lastError = undefined;
|
||||
session.pendingUserInput = undefined;
|
||||
session.pendingPlanReview = undefined;
|
||||
session.updatedAt = cancelledAt;
|
||||
const cancelledRun = this.updateSessionRun(session, requestId, (run) =>
|
||||
cancelSessionRunRecord(run, cancelledAt));
|
||||
@@ -1285,6 +1316,30 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
await this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
private async handleExitPlanModeRequested(
|
||||
workspace: WorkspaceState,
|
||||
sessionId: string,
|
||||
event: ExitPlanModeRequestedEvent,
|
||||
): Promise<void> {
|
||||
const session = this.requireSession(workspace, sessionId);
|
||||
const requestedAt = nowIso();
|
||||
|
||||
session.pendingPlanReview = {
|
||||
id: event.exitPlanId,
|
||||
status: 'pending',
|
||||
agentId: event.agentId,
|
||||
agentName: event.agentName,
|
||||
summary: event.summary,
|
||||
planContent: event.planContent,
|
||||
actions: event.actions,
|
||||
recommendedAction: event.recommendedAction,
|
||||
requestedAt,
|
||||
};
|
||||
session.updatedAt = requestedAt;
|
||||
|
||||
await this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
private createPendingApprovalFromSidecarEvent(event: ApprovalRequestedEvent): PendingApprovalRecord {
|
||||
return {
|
||||
id: event.approvalId,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CreateSessionInput,
|
||||
ResolveProjectDiscoveredToolingInput,
|
||||
ResolveWorkspaceDiscoveredToolingInput,
|
||||
DismissSessionPlanReviewInput,
|
||||
DuplicateSessionInput,
|
||||
RenameSessionInput,
|
||||
RescanProjectConfigsInput,
|
||||
@@ -18,6 +19,7 @@ import type {
|
||||
SendSessionMessageInput,
|
||||
SetPatternFavoriteInput,
|
||||
SetSessionArchivedInput,
|
||||
SetSessionInteractionModeInput,
|
||||
SetSessionPinnedInput,
|
||||
UpdateSessionApprovalSettingsInput,
|
||||
UpdateSessionToolingInput,
|
||||
@@ -114,6 +116,12 @@ export function registerIpcHandlers(window: BrowserWindow, service: AryxAppServi
|
||||
ipcMain.handle(ipcChannels.resolveSessionUserInput, (_event, input: ResolveSessionUserInputInput) =>
|
||||
service.resolveSessionUserInput(input.sessionId, input.userInputId, input.answer, input.wasFreeform),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.setSessionInteractionMode, (_event, input: SetSessionInteractionModeInput) =>
|
||||
service.setSessionInteractionMode(input.sessionId, input.mode),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.dismissSessionPlanReview, (_event, input: DismissSessionPlanReviewInput) =>
|
||||
service.dismissSessionPlanReview(input.sessionId),
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcChannels.updateSessionModelConfig,
|
||||
(_event, input: UpdateSessionModelConfigInput) =>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AgentActivityEvent,
|
||||
ApprovalRequestedEvent,
|
||||
ExitPlanModeRequestedEvent,
|
||||
TurnDeltaEvent,
|
||||
UserInputRequestedEvent,
|
||||
} from '@shared/contracts/sidecar';
|
||||
@@ -14,6 +15,7 @@ export interface RunTurnPendingCommand {
|
||||
onActivity: (event: AgentActivityEvent) => void | Promise<void>;
|
||||
onApproval: (event: ApprovalRequestedEvent) => void | Promise<void>;
|
||||
onUserInput: (event: UserInputRequestedEvent) => void | Promise<void>;
|
||||
onExitPlanMode: (event: ExitPlanModeRequestedEvent) => void | Promise<void>;
|
||||
errored: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
SidecarEvent,
|
||||
TurnDeltaEvent,
|
||||
UserInputRequestedEvent,
|
||||
ExitPlanModeRequestedEvent,
|
||||
ValidatePatternCommand,
|
||||
RunTurnCommand,
|
||||
} from '@shared/contracts/sidecar';
|
||||
@@ -102,8 +103,9 @@ export class SidecarClient {
|
||||
onActivity: (event: AgentActivityEvent) => void | Promise<void>,
|
||||
onApproval: (event: ApprovalRequestedEvent) => void | Promise<void>,
|
||||
onUserInput: (event: UserInputRequestedEvent) => void | Promise<void>,
|
||||
onExitPlanMode: (event: ExitPlanModeRequestedEvent) => void | Promise<void>,
|
||||
): Promise<ChatMessageRecord[]> {
|
||||
return this.dispatch<ChatMessageRecord[]>(command, onDelta, onActivity, onApproval, onUserInput);
|
||||
return this.dispatch<ChatMessageRecord[]>(command, onDelta, onActivity, onApproval, onUserInput, onExitPlanMode);
|
||||
}
|
||||
|
||||
async resolveUserInput(userInputId: string, answer: string, wasFreeform: boolean): Promise<void> {
|
||||
@@ -218,6 +220,7 @@ export class SidecarClient {
|
||||
onActivity?: (event: AgentActivityEvent) => void | Promise<void>,
|
||||
onApproval?: (event: ApprovalRequestedEvent) => void | Promise<void>,
|
||||
onUserInput?: (event: UserInputRequestedEvent) => void | Promise<void>,
|
||||
onExitPlanMode?: (event: ExitPlanModeRequestedEvent) => void | Promise<void>,
|
||||
): Promise<TResult> {
|
||||
const state = await this.ensureProcess();
|
||||
|
||||
@@ -232,6 +235,7 @@ export class SidecarClient {
|
||||
onActivity: onActivity ?? (() => undefined),
|
||||
onApproval: onApproval ?? (() => undefined),
|
||||
onUserInput: onUserInput ?? (() => undefined),
|
||||
onExitPlanMode: onExitPlanMode ?? (() => undefined),
|
||||
errored: false,
|
||||
});
|
||||
} else if (command.type === 'validate-pattern') {
|
||||
@@ -329,6 +333,11 @@ export class SidecarClient {
|
||||
this.invokeRunTurnHandler(event.requestId, pending, () => pending.onUserInput(event));
|
||||
}
|
||||
return;
|
||||
case 'exit-plan-mode-requested':
|
||||
if (pending.kind === 'run-turn' && shouldHandleRunTurnEvent(pending)) {
|
||||
this.invokeRunTurnHandler(event.requestId, pending, () => pending.onExitPlanMode(event));
|
||||
}
|
||||
return;
|
||||
case 'turn-complete':
|
||||
if (pending.kind === 'run-turn') {
|
||||
if (shouldHandleRunTurnEvent(pending)) {
|
||||
|
||||
Reference in New Issue
Block a user