mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-12 20:07:31 +00:00
* feat: settings toggle for AGENT_NOTIFY_SUMMARY contract injection Add a hub-persisted, default-off Settings control so operators can opt agents into emitting the trailing AGENT_NOTIFY_SUMMARY line. Propagate the resolved flag on CLI session bootstrap and inject at call time for Claude, Codex, OpenCode, and Grok (Cursor still unsupported). Closes #1375 Co-authored-by: Cursor <cursoragent@cursor.com> * fix: lock hub settings RMW and restore abort deliveryMode Serialize settings.json updates with the shared .lock protocol and unique temp files so the new hub toggle cannot clobber CLI/relay fields under concurrency. Also supply deliveryMode on abort send-error restore so web typecheck (and CI) pass. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): wrap SettingsGeneralPage tests with QueryClientProvider The hub-settings toggle uses TanStack Query; the settings page suite was rendering without a QueryClient and blew up CI. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): defer summary-contract toggle until settings load Avoid rendering an interactive false switch while the hub GET is still in flight, which could overwrite an enabled preference on early click. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(web): clarify Grok coverage for summary-contract toggle Local Grok has no instruction inject path; settings copy now matches remote-only Grok support (and still excludes Cursor). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
190 lines
5.4 KiB
TypeScript
190 lines
5.4 KiB
TypeScript
import { logger } from '@/ui/logger';
|
|
import { spawnWithTerminalGuard } from '@/utils/spawnWithTerminalGuard';
|
|
import {
|
|
buildMcpServerConfigArgs,
|
|
buildDeveloperInstructionsArg,
|
|
buildCodexHookConfigArgs,
|
|
buildModelReasoningEffortConfigArgs
|
|
} from './utils/codexMcpConfig';
|
|
import { getCodexSystemPrompt } from './utils/systemPrompt';
|
|
import type { ReasoningEffort } from './appServerTypes';
|
|
import { resolveCodexCommand } from './utils/codexExecutable';
|
|
import type { McpServersConfig } from './utils/buildHapiMcpBridge';
|
|
|
|
const CODEX_OPTIONS_WITH_VALUE = new Set([
|
|
'-a',
|
|
'--add-dir',
|
|
'--ask-for-approval',
|
|
'-C',
|
|
'--cd',
|
|
'-c',
|
|
'--config',
|
|
'--disable',
|
|
'--enable',
|
|
'--local-provider',
|
|
'-m',
|
|
'--model',
|
|
'-p',
|
|
'--profile',
|
|
'--remote',
|
|
'--remote-auth-token-env',
|
|
'-s',
|
|
'--sandbox'
|
|
]);
|
|
// -i/--image is intentionally omitted because it accepts a variable number of files.
|
|
|
|
function findResumeSubcommandIndex(args: string[]): number {
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
|
|
if (arg === '--') {
|
|
return -1;
|
|
}
|
|
if (arg === 'resume') {
|
|
return i;
|
|
}
|
|
if (!arg.startsWith('-')) {
|
|
return -1;
|
|
}
|
|
if (!arg.includes('=') && CODEX_OPTIONS_WITH_VALUE.has(arg)) {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
function findResumeSessionIdIndex(args: string[], resumeIndex: number): number {
|
|
for (let i = resumeIndex + 1; i < args.length; i++) {
|
|
const arg = args[i];
|
|
|
|
if (arg === '--') {
|
|
return -1;
|
|
}
|
|
if (!arg.startsWith('-')) {
|
|
return i;
|
|
}
|
|
if (!arg.includes('=') && CODEX_OPTIONS_WITH_VALUE.has(arg)) {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Filter out the Codex resume selector which is managed internally by hapi.
|
|
* Codex accepts global options before the subcommand, for example
|
|
* `codex --sandbox danger-full-access resume --last`.
|
|
*/
|
|
export function filterResumeSubcommand(args: string[]): string[] {
|
|
const resumeIndex = findResumeSubcommandIndex(args);
|
|
if (resumeIndex === -1) {
|
|
return args;
|
|
}
|
|
|
|
const optionTerminatorIndex = args.indexOf('--', resumeIndex + 1);
|
|
const lastIndex = args.findIndex((arg, index) => (
|
|
arg === '--last'
|
|
&& index > resumeIndex
|
|
&& (optionTerminatorIndex === -1 || index < optionTerminatorIndex)
|
|
));
|
|
const sessionIdIndex = lastIndex === -1
|
|
? findResumeSessionIdIndex(args, resumeIndex)
|
|
: -1;
|
|
const filtered = args.filter((_, index) => (
|
|
index !== resumeIndex
|
|
&& index !== lastIndex
|
|
&& index !== sessionIdIndex
|
|
));
|
|
|
|
if (lastIndex !== -1) {
|
|
logger.debug("[CodexLocal] Filtered 'resume --last' - session managed by hapi");
|
|
} else if (sessionIdIndex !== -1) {
|
|
logger.debug(`[CodexLocal] Filtered 'resume ${args[sessionIdIndex]}' - session managed by hapi`);
|
|
} else {
|
|
logger.debug("[CodexLocal] Filtered 'resume' - session managed by hapi");
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
export async function codexLocal(opts: {
|
|
abort: AbortSignal;
|
|
sessionId: string | null;
|
|
path: string;
|
|
model?: string;
|
|
modelReasoningEffort?: ReasoningEffort;
|
|
sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
onSessionFound: (id: string) => void;
|
|
codexArgs?: string[];
|
|
mcpServers?: McpServersConfig;
|
|
sessionHook?: {
|
|
port: number;
|
|
token: string;
|
|
};
|
|
}): Promise<void> {
|
|
const args: string[] = [];
|
|
|
|
if (opts.sessionId) {
|
|
args.push('resume', opts.sessionId);
|
|
opts.onSessionFound(opts.sessionId);
|
|
}
|
|
|
|
if (opts.model) {
|
|
args.push('--model', opts.model);
|
|
}
|
|
|
|
if (opts.modelReasoningEffort) {
|
|
args.push(...buildModelReasoningEffortConfigArgs(opts.modelReasoningEffort));
|
|
}
|
|
|
|
if (opts.sandbox) {
|
|
args.push('--sandbox', opts.sandbox);
|
|
}
|
|
|
|
// Add MCP server configuration
|
|
if (opts.mcpServers && Object.keys(opts.mcpServers).length > 0) {
|
|
args.push(...buildMcpServerConfigArgs(opts.mcpServers));
|
|
}
|
|
|
|
if (opts.sessionHook) {
|
|
args.push(...buildCodexHookConfigArgs(opts.sessionHook.port, opts.sessionHook.token));
|
|
}
|
|
|
|
// Add developer instructions (system prompt)
|
|
args.push(...buildDeveloperInstructionsArg(getCodexSystemPrompt()));
|
|
|
|
if (opts.codexArgs) {
|
|
// Before the first launch, Codex still needs the user's selector (for
|
|
// example `resume --last`). Once hapi has the concrete session ID, it
|
|
// prepends that ID above and removes the original selector here.
|
|
const safeArgs = opts.sessionId
|
|
? filterResumeSubcommand(opts.codexArgs)
|
|
: opts.codexArgs;
|
|
args.push(...safeArgs);
|
|
}
|
|
|
|
logger.debug(`[CodexLocal] Spawning codex with args: ${JSON.stringify(args)}`);
|
|
|
|
if (opts.abort.aborted) {
|
|
logger.debug('[CodexLocal] Abort already signaled before spawn; skipping launch');
|
|
return;
|
|
}
|
|
|
|
const codexCommand = resolveCodexCommand();
|
|
|
|
await spawnWithTerminalGuard({
|
|
command: codexCommand.command,
|
|
args: [...codexCommand.args, ...args],
|
|
cwd: opts.path,
|
|
env: process.env,
|
|
signal: opts.abort,
|
|
logLabel: 'CodexLocal',
|
|
spawnName: 'codex',
|
|
installHint: 'Codex CLI',
|
|
includeCause: true,
|
|
logExit: true
|
|
});
|
|
}
|