mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-15 03:01:39 +00:00
* feat(cli): add GitHub Copilot CLI agent support via ACP Wrap `copilot --acp --stdio` for remote sessions and spawn the native TUI locally, with full hub/web integration for spawn, resume, and permissions. Fixes tiann/hapi#362 Co-Authored-By: HAPI <noreply@hapi.run> Co-authored-by: Cursor <cursoragent@cursor.com> * feat(copilot): agent modes, models, slash/file UX, local session sync Add Interactive/Plan/Autopilot (fleet is slash-only), subscription-aware model discovery, web StatusBar/permission UX, @ file mentions, and fix local Safe Yolo plus session-id locator for handoff/resume. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: re-trigger Codex PR review after auth outage Co-authored-by: Cursor <cursoragent@cursor.com> * chore: retry Codex PR review Co-authored-by: Cursor <cursoragent@cursor.com> * fix(copilot): preserve agent mode on resume and apply via ACP set_mode Resume was dropping copilotAgentMode so Plan/Autopilot reset to interactive. Also switch local/remote mode application to --mode / session set_mode instead of slash prompts. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(copilot): wake remote loop when agent mode changes Empty isolated queue tick lets setMode apply without inventing a user prompt. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(copilot): confirm mode changes before persisting Await Copilot mode changes and expose discovered models so session state reflects backend acceptance. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(copilot): guard mode discovery and slash updates Keep model probes within runner roots and preserve active sessions when mode switching is unavailable or rejected. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(copilot): preserve resume and auto semantics Deduplicate Copilot resume rows, apply Auto explicitly, and fail closed on denied permissions. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(copilot): close permission and model discovery gaps Keep write-capable commands pending in read-only mode, extend model probe RPCs, and preserve explicit model validation before session creation. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(copilot): persist runtime model and agent mode Fallback to ACP model options when direct model switching is unavailable and retain Copilot agent mode across hub restarts. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(copilot): normalize composer auto selection Use the null session sentinel for Copilot Auto so the composer selects and resets default models consistently. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(copilot): reject local permission mode changes * style(copilot): remove trailing blank line * fix(copilot): secure local config handoffs * fix(copilot): reject local agent mode slashes * fix(copilot): reject mode changes during turns * fix(copilot): consume rejected slash updates * fix(copilot): preserve thinking across slash handling * fix(copilot): stabilize async config changes * fix(copilot): roll back rejected startup model * fix(copilot): preserve cancellation and file mentions * fix(copilot): hide local permission controls * fix(deps): support clean workspace installs * test(copilot): account for spawn mode argument * fix(copilot): attribute usage to active model --------- Co-authored-by: HAPI <noreply@hapi.run> Co-authored-by: Cursor <cursoragent@cursor.com>
118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
import { ApiClient, ApiSessionClient } from '@/lib';
|
|
import { MessageQueue2 } from '@/utils/MessageQueue2';
|
|
import { AgentSessionBase } from '@/agent/sessionBase';
|
|
import type { CopilotMode, PermissionMode } from './types';
|
|
import type { CopilotAgentMode } from '@hapi/protocol';
|
|
import type { LocalLaunchExitReason } from '@/agent/localLaunchPolicy';
|
|
|
|
type LocalLaunchFailure = {
|
|
message: string;
|
|
exitReason: LocalLaunchExitReason;
|
|
};
|
|
|
|
export class CopilotSession extends AgentSessionBase<CopilotMode> {
|
|
readonly startedBy: 'runner' | 'terminal';
|
|
readonly startingMode: 'local' | 'remote';
|
|
localLaunchFailure: LocalLaunchFailure | null = null;
|
|
private remoteAgentModeApplier: ((mode: CopilotAgentMode) => Promise<void>) | null = null;
|
|
|
|
constructor(opts: {
|
|
api: ApiClient;
|
|
client: ApiSessionClient;
|
|
path: string;
|
|
logPath: string;
|
|
sessionId: string | null;
|
|
messageQueue: MessageQueue2<CopilotMode>;
|
|
onModeChange: (mode: 'local' | 'remote') => void;
|
|
mode?: 'local' | 'remote';
|
|
startedBy: 'runner' | 'terminal';
|
|
startingMode: 'local' | 'remote';
|
|
permissionMode?: PermissionMode;
|
|
agentMode?: CopilotAgentMode;
|
|
}) {
|
|
super({
|
|
api: opts.api,
|
|
client: opts.client,
|
|
path: opts.path,
|
|
logPath: opts.logPath,
|
|
sessionId: opts.sessionId,
|
|
messageQueue: opts.messageQueue,
|
|
onModeChange: opts.onModeChange,
|
|
mode: opts.mode,
|
|
sessionLabel: 'CopilotSession',
|
|
sessionIdLabel: 'Copilot',
|
|
applySessionIdToMetadata: (metadata, sessionId) => ({
|
|
...metadata,
|
|
copilotSessionId: sessionId
|
|
}),
|
|
permissionMode: opts.permissionMode
|
|
});
|
|
|
|
this.startedBy = opts.startedBy;
|
|
this.startingMode = opts.startingMode;
|
|
this.permissionMode = opts.permissionMode;
|
|
this.agentMode = opts.agentMode ?? 'interactive';
|
|
}
|
|
|
|
agentMode: CopilotAgentMode;
|
|
|
|
setPermissionMode = (mode: PermissionMode): void => {
|
|
this.permissionMode = mode;
|
|
};
|
|
|
|
setModel = (model: string | null): void => {
|
|
this.model = model;
|
|
};
|
|
|
|
setAgentMode = (mode: CopilotAgentMode): void => {
|
|
this.agentMode = mode;
|
|
};
|
|
|
|
getAgentMode = (): CopilotAgentMode => this.agentMode;
|
|
|
|
setRemoteAgentModeApplier = (applier: ((mode: CopilotAgentMode) => Promise<void>) | null): void => {
|
|
this.remoteAgentModeApplier = applier;
|
|
};
|
|
|
|
applyRemoteAgentMode = async (mode: CopilotAgentMode): Promise<void> => {
|
|
if (this.thinking) {
|
|
throw new Error('Wait for the current Copilot turn to finish before changing agent mode');
|
|
}
|
|
if (!this.remoteAgentModeApplier) {
|
|
throw new Error('Copilot agent mode switching is unavailable for this session');
|
|
}
|
|
await this.remoteAgentModeApplier(mode);
|
|
};
|
|
|
|
protected override getKeepAliveRuntime() {
|
|
return {
|
|
...(super.getKeepAliveRuntime() ?? {}),
|
|
copilotAgentMode: this.agentMode
|
|
};
|
|
}
|
|
|
|
pushKeepAlive = (): void => {
|
|
this.client.keepAlive(this.thinking, this.mode, {
|
|
permissionMode: this.permissionMode,
|
|
model: this.model,
|
|
copilotAgentMode: this.agentMode
|
|
});
|
|
};
|
|
|
|
recordLocalLaunchFailure = (message: string, exitReason: LocalLaunchExitReason): void => {
|
|
this.localLaunchFailure = { message, exitReason };
|
|
};
|
|
|
|
sendAgentMessage = (message: unknown): void => {
|
|
this.client.sendAgentMessage(message);
|
|
};
|
|
|
|
sendUserMessage = (text: string): void => {
|
|
this.client.sendUserMessage(text);
|
|
};
|
|
|
|
sendSessionEvent = (event: Parameters<ApiSessionClient['sendSessionEvent']>[0]): void => {
|
|
this.client.sendSessionEvent(event);
|
|
};
|
|
}
|