Merge pull request #156 from amDosion/feat/ultraplan-enablement

feat: enable /ultraplan and harden GrowthBook fallback chain
This commit is contained in:
Dosion
2026-04-06 22:09:28 +08:00
committed by GitHub
parent 35bc4f395d
commit 33949ce5a2
12 changed files with 2849 additions and 3156 deletions

View File

@@ -1,42 +1,38 @@
import { readFileSync } from 'fs'
import { REMOTE_CONTROL_DISCONNECTED_MSG } from '../bridge/types.js'
import type { Command } from '../commands.js'
import { DIAMOND_OPEN } from '../constants/figures.js'
import { getRemoteSessionUrl } from '../constants/product.js'
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
import { readFileSync } from 'fs';
import { REMOTE_CONTROL_DISCONNECTED_MSG } from '../bridge/types.js';
import type { Command } from '../commands.js';
import { DIAMOND_OPEN } from '../constants/figures.js';
import { getRemoteSessionUrl } from '../constants/product.js';
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js';
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from '../services/analytics/index.js'
import type { AppState } from '../state/AppStateStore.js'
} from '../services/analytics/index.js';
import type { AppState } from '../state/AppStateStore.js';
import {
checkRemoteAgentEligibility,
formatPreconditionError,
RemoteAgentTask,
type RemoteAgentTaskState,
registerRemoteAgentTask,
} from '../tasks/RemoteAgentTask/RemoteAgentTask.js'
import type { LocalJSXCommandCall } from '../types/command.js'
import { logForDebugging } from '../utils/debug.js'
import { errorMessage } from '../utils/errors.js'
import { logError } from '../utils/log.js'
import { enqueuePendingNotification } from '../utils/messageQueueManager.js'
import { ALL_MODEL_CONFIGS } from '../utils/model/configs.js'
import { updateTaskState } from '../utils/task/framework.js'
import { archiveRemoteSession, teleportToRemote } from '../utils/teleport.js'
import {
pollForApprovedExitPlanMode,
UltraplanPollError,
} from '../utils/ultraplan/ccrSession.js'
} from '../tasks/RemoteAgentTask/RemoteAgentTask.js';
import type { LocalJSXCommandCall } from '../types/command.js';
import { logForDebugging } from '../utils/debug.js';
import { errorMessage } from '../utils/errors.js';
import { logError } from '../utils/log.js';
import { enqueuePendingNotification } from '../utils/messageQueueManager.js';
import { ALL_MODEL_CONFIGS } from '../utils/model/configs.js';
import { updateTaskState } from '../utils/task/framework.js';
import { archiveRemoteSession, teleportToRemote } from '../utils/teleport.js';
import { pollForApprovedExitPlanMode, UltraplanPollError } from '../utils/ultraplan/ccrSession.js';
// TODO(prod-hardening): OAuth token may go stale over the 30min poll;
// consider refresh.
// Multi-agent exploration is slow; 30min timeout.
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000;
export const CCR_TERMS_URL =
'https://code.claude.com/docs/en/claude-code-on-the-web'
export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the-web';
// CCR runs against the first-party API — use the canonical ID, not the
// provider-specific string getModelStrings() would return (which may be a
@@ -44,10 +40,7 @@ export const CCR_TERMS_URL =
// load: the GrowthBook cache is empty at import and `/config` Gates can flip
// it between invocations.
function getUltraplanModel(): string {
return getFeatureValue_CACHED_MAY_BE_STALE(
'tengu_ultraplan_model',
ALL_MODEL_CONFIGS.opus46.firstParty,
)
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_ultraplan_model', ALL_MODEL_CONFIGS.opus46.firstParty);
}
// prompt.txt is wrapped in <system-reminder> so the CCR browser hides
@@ -60,11 +53,9 @@ function getUltraplanModel(): string {
//
// Bundler inlines .txt as a string; the test runner wraps it as {default}.
/* eslint-disable @typescript-eslint/no-require-imports */
const _rawPrompt = require('../utils/ultraplan/prompt.txt')
const _rawPrompt = require('../utils/ultraplan/prompt.txt');
/* eslint-enable @typescript-eslint/no-require-imports */
const DEFAULT_INSTRUCTIONS: string = (
typeof _rawPrompt === 'string' ? _rawPrompt : _rawPrompt.default
).trimEnd()
const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPrompt : _rawPrompt.default).trimEnd();
// Dev-only prompt override resolved eagerly at module load.
// Gated to ant builds (USER_TYPE is a build-time define,
@@ -75,7 +66,7 @@ const DEFAULT_INSTRUCTIONS: string = (
const ULTRAPLAN_INSTRUCTIONS: string =
process.env.USER_TYPE === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE
? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd()
: DEFAULT_INSTRUCTIONS
: DEFAULT_INSTRUCTIONS;
/* eslint-enable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs */
/**
@@ -83,15 +74,15 @@ const ULTRAPLAN_INSTRUCTIONS: string =
* system-reminder so the browser renders them; scaffolding is hidden.
*/
export function buildUltraplanPrompt(blurb: string, seedPlan?: string): string {
const parts: string[] = []
const parts: string[] = [];
if (seedPlan) {
parts.push('Here is a draft plan to refine:', '', seedPlan, '')
parts.push('Here is a draft plan to refine:', '', seedPlan, '');
}
parts.push(ULTRAPLAN_INSTRUCTIONS)
parts.push(ULTRAPLAN_INSTRUCTIONS);
if (blurb) {
parts.push('', blurb)
parts.push('', blurb);
}
return parts.join('\n')
return parts.join('\n');
}
function startDetachedPoll(
@@ -101,52 +92,41 @@ function startDetachedPoll(
getAppState: () => AppState,
setAppState: (f: (prev: AppState) => AppState) => void,
): void {
const started = Date.now()
let failed = false
const started = Date.now();
let failed = false;
void (async () => {
try {
const { plan, rejectCount, executionTarget } =
await pollForApprovedExitPlanMode(
sessionId,
ULTRAPLAN_TIMEOUT_MS,
phase => {
if (phase === 'needs_input')
logEvent('tengu_ultraplan_awaiting_input', {})
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => {
if (t.status !== 'running') return t
const next = phase === 'running' ? undefined : phase
return t.ultraplanPhase === next
? t
: { ...t, ultraplanPhase: next }
})
},
() => getAppState().tasks?.[taskId]?.status !== 'running',
)
const { plan, rejectCount, executionTarget } = await pollForApprovedExitPlanMode(
sessionId,
ULTRAPLAN_TIMEOUT_MS,
phase => {
if (phase === 'needs_input') logEvent('tengu_ultraplan_awaiting_input', {});
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => {
if (t.status !== 'running') return t;
const next = phase === 'running' ? undefined : phase;
return t.ultraplanPhase === next ? t : { ...t, ultraplanPhase: next };
});
},
() => getAppState().tasks?.[taskId]?.status !== 'running',
);
logEvent('tengu_ultraplan_approved', {
duration_ms: Date.now() - started,
plan_length: plan.length,
reject_count: rejectCount,
execution_target:
executionTarget as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
execution_target: executionTarget as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
if (executionTarget === 'remote') {
// User chose "execute in CCR" in the browser PlanModal — the remote
// session is now coding. Skip archive (ARCHIVE has no running-check,
// would kill mid-execution) and skip the choice dialog (already chose).
// Guard on task status so a poll that resolves after stopUltraplan
// doesn't notify for a killed session.
const task = getAppState().tasks?.[taskId]
if (task?.status !== 'running') return
const task = getAppState().tasks?.[taskId];
if (task?.status !== 'running') return;
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t =>
t.status !== 'running'
? t
: { ...t, status: 'completed', endTime: Date.now() },
)
setAppState(prev =>
prev.ultraplanSessionUrl === url
? { ...prev, ultraplanSessionUrl: undefined }
: prev,
)
t.status !== 'running' ? t : { ...t, status: 'completed', endTime: Date.now() },
);
setAppState(prev => (prev.ultraplanSessionUrl === url ? { ...prev, ultraplanSessionUrl: undefined } : prev));
enqueuePendingNotification({
value: [
`Ultraplan approved — executing in Claude Code on the web. Follow along at: ${url}`,
@@ -154,52 +134,47 @@ function startDetachedPoll(
'Results will land as a pull request when the remote session finishes. There is nothing to do here.',
].join('\n'),
mode: 'task-notification',
})
});
} else {
// Teleport: set pendingChoice so REPL mounts UltraplanChoiceDialog.
// The dialog owns archive + URL clear on choice. Guard on task status
// so a poll that resolves after stopUltraplan doesn't resurrect the
// dialog for a killed session.
setAppState(prev => {
const task = prev.tasks?.[taskId]
if (!task || task.status !== 'running') return prev
const task = prev.tasks?.[taskId];
if (!task || task.status !== 'running') return prev;
return {
...prev,
ultraplanPendingChoice: { plan, sessionId, taskId },
}
})
};
});
}
} catch (e) {
// If the task was stopped (stopUltraplan sets status=killed), the poll
// erroring is expected — skip the failure notification and cleanup
// (kill() already archived; stopUltraplan cleared the URL).
const task = getAppState().tasks?.[taskId]
if (task?.status !== 'running') return
failed = true
const task = getAppState().tasks?.[taskId];
if (task?.status !== 'running') return;
failed = true;
logEvent('tengu_ultraplan_failed', {
duration_ms: Date.now() - started,
reason: (e instanceof UltraplanPollError
? e.reason
: 'network_or_unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
reject_count:
e instanceof UltraplanPollError ? e.rejectCount : undefined,
})
reject_count: e instanceof UltraplanPollError ? e.rejectCount : undefined,
});
enqueuePendingNotification({
value: `Ultraplan failed: ${errorMessage(e)}\n\nSession: ${url}`,
mode: 'task-notification',
})
});
// Error path owns cleanup; teleport path defers to the dialog; remote
// path handled its own cleanup above.
void archiveRemoteSession(sessionId).catch(e =>
logForDebugging(`ultraplan archive failed: ${String(e)}`),
)
void archiveRemoteSession(sessionId).catch(e => logForDebugging(`ultraplan archive failed: ${String(e)}`));
setAppState(prev =>
// Compare against this poll's URL so a newer relaunched session's
// URL isn't cleared by a stale poll erroring out.
prev.ultraplanSessionUrl === url
? { ...prev, ultraplanSessionUrl: undefined }
: prev,
)
prev.ultraplanSessionUrl === url ? { ...prev, ultraplanSessionUrl: undefined } : prev,
);
} finally {
// Remote path already set status=completed above; teleport path
// leaves status=running so the pill shows the ultraplanPhase state
@@ -209,30 +184,28 @@ function startDetachedPoll(
// Failure path has no dialog, so it owns the status transition here.
if (failed) {
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t =>
t.status !== 'running'
? t
: { ...t, status: 'failed', endTime: Date.now() },
)
t.status !== 'running' ? t : { ...t, status: 'failed', endTime: Date.now() },
);
}
}
})()
})();
}
// Renders immediately so the terminal doesn't appear hung during the
// multi-second teleportToRemote round-trip.
function buildLaunchMessage(disconnectedBridge?: boolean): string {
const prefix = disconnectedBridge ? `${REMOTE_CONTROL_DISCONNECTED_MSG} ` : ''
return `${DIAMOND_OPEN} ultraplan\n${prefix}Starting Claude Code on the web…`
const prefix = disconnectedBridge ? `${REMOTE_CONTROL_DISCONNECTED_MSG} ` : '';
return `${DIAMOND_OPEN} ultraplan\n${prefix}Starting Claude Code on the web…`;
}
function buildSessionReadyMessage(url: string): string {
return `${DIAMOND_OPEN} ultraplan · Monitor progress in Claude Code on the web ${url}\nYou can continue working — when the ${DIAMOND_OPEN} fills, press ↓ to view results`
return `${DIAMOND_OPEN} ultraplan · Monitor progress in Claude Code on the web ${url}\nYou can continue working — when the ${DIAMOND_OPEN} fills, press ↓ to view results`;
}
function buildAlreadyActiveMessage(url: string | undefined): string {
return url
? `ultraplan: already polling. Open ${url} to check status, or wait for the plan to land here.`
: 'ultraplan: already launching. Please wait for the session to start.'
: 'ultraplan: already launching. Please wait for the session to start.';
}
/**
@@ -249,11 +222,9 @@ export async function stopUltraplan(
): Promise<void> {
// RemoteAgentTask.kill archives the session (with .catch) — no separate
// archive call needed here.
await RemoteAgentTask.kill(taskId, setAppState)
await RemoteAgentTask.kill(taskId, setAppState);
setAppState(prev =>
prev.ultraplanSessionUrl ||
prev.ultraplanPendingChoice ||
prev.ultraplanLaunching
prev.ultraplanSessionUrl || prev.ultraplanPendingChoice || prev.ultraplanLaunching
? {
...prev,
ultraplanSessionUrl: undefined,
@@ -261,18 +232,18 @@ export async function stopUltraplan(
ultraplanLaunching: undefined,
}
: prev,
)
const url = getRemoteSessionUrl(sessionId, process.env.SESSION_INGRESS_URL)
);
const url = getRemoteSessionUrl(sessionId, process.env.SESSION_INGRESS_URL);
enqueuePendingNotification({
value: `Ultraplan stopped.\n\nSession: ${url}`,
mode: 'task-notification',
})
});
enqueuePendingNotification({
value:
'The user stopped the ultraplan session above. Do not respond to the stop notification — wait for their next message.',
mode: 'task-notification',
isMeta: true,
})
});
}
/**
@@ -285,13 +256,13 @@ export async function stopUltraplan(
* enqueuePendingNotification.
*/
export async function launchUltraplan(opts: {
blurb: string
seedPlan?: string
getAppState: () => AppState
setAppState: (f: (prev: AppState) => AppState) => void
signal: AbortSignal
blurb: string;
seedPlan?: string;
getAppState: () => AppState;
setAppState: (f: (prev: AppState) => AppState) => void;
signal: AbortSignal;
/** True if the caller disconnected Remote Control before launching. */
disconnectedBridge?: boolean
disconnectedBridge?: boolean;
/**
* Called once teleportToRemote resolves with a session URL. Callers that
* have setMessages (REPL) append this as a second transcript message so the
@@ -299,26 +270,18 @@ export async function launchUltraplan(opts: {
* transcript access (ExitPlanModePermissionRequest) omit this — the pill
* still shows live status.
*/
onSessionReady?: (msg: string) => void
onSessionReady?: (msg: string) => void;
}): Promise<string> {
const {
blurb,
seedPlan,
getAppState,
setAppState,
signal,
disconnectedBridge,
onSessionReady,
} = opts
const { blurb, seedPlan, getAppState, setAppState, signal, disconnectedBridge, onSessionReady } = opts;
const { ultraplanSessionUrl: active, ultraplanLaunching } = getAppState()
const { ultraplanSessionUrl: active, ultraplanLaunching } = getAppState();
if (active || ultraplanLaunching) {
logEvent('tengu_ultraplan_create_failed', {
reason: (active
? 'already_polling'
: 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
return buildAlreadyActiveMessage(active)
});
return buildAlreadyActiveMessage(active);
}
if (!blurb && !seedPlan) {
@@ -336,14 +299,12 @@ export async function launchUltraplan(opts: {
'Requires /login.',
'',
`Terms: ${CCR_TERMS_URL}`,
].join('\n')
].join('\n');
}
// Set synchronously before the detached flow to prevent duplicate launches
// during the teleportToRemote window.
setAppState(prev =>
prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true },
)
setAppState(prev => (prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true }));
void launchDetached({
blurb,
seedPlan,
@@ -351,47 +312,43 @@ export async function launchUltraplan(opts: {
setAppState,
signal,
onSessionReady,
})
return buildLaunchMessage(disconnectedBridge)
});
return buildLaunchMessage(disconnectedBridge);
}
async function launchDetached(opts: {
blurb: string
seedPlan?: string
getAppState: () => AppState
setAppState: (f: (prev: AppState) => AppState) => void
signal: AbortSignal
onSessionReady?: (msg: string) => void
blurb: string;
seedPlan?: string;
getAppState: () => AppState;
setAppState: (f: (prev: AppState) => AppState) => void;
signal: AbortSignal;
onSessionReady?: (msg: string) => void;
}): Promise<void> {
const { blurb, seedPlan, getAppState, setAppState, signal, onSessionReady } =
opts
const { blurb, seedPlan, getAppState, setAppState, signal, onSessionReady } = opts;
// Hoisted so the catch block can archive the remote session if an error
// occurs after teleportToRemote succeeds (avoids 30min orphan).
let sessionId: string | undefined
let sessionId: string | undefined;
try {
const model = getUltraplanModel()
const model = getUltraplanModel();
const eligibility = await checkRemoteAgentEligibility()
const eligibility = await checkRemoteAgentEligibility();
if (!eligibility.eligible) {
logEvent('tengu_ultraplan_create_failed', {
reason:
'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
reason: 'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
precondition_errors: eligibility.errors
.map(e => e.type)
.join(
',',
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
const reasons = eligibility.errors.map(formatPreconditionError).join('\n')
.join(',') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
const reasons = eligibility.errors.map(formatPreconditionError).join('\n');
enqueuePendingNotification({
value: `ultraplan: cannot launch remote session —\n${reasons}`,
mode: 'task-notification',
})
return
});
return;
}
const prompt = buildUltraplanPrompt(blurb, seedPlan)
let bundleFailMsg: string | undefined
const prompt = buildUltraplanPrompt(blurb, seedPlan);
let bundleFailMsg: string | undefined;
const session = await teleportToRemote({
initialMessage: prompt,
description: blurb || 'Refine local plan',
@@ -401,35 +358,34 @@ async function launchDetached(opts: {
signal,
useDefaultEnvironment: true,
onBundleFail: msg => {
bundleFailMsg = msg
bundleFailMsg = msg;
},
})
});
if (!session) {
logEvent('tengu_ultraplan_create_failed', {
reason: (bundleFailMsg
? 'bundle_fail'
: 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
});
enqueuePendingNotification({
value: `ultraplan: session creation failed${bundleFailMsg ? `${bundleFailMsg}` : ''}. See --debug for details.`,
mode: 'task-notification',
})
return
});
return;
}
sessionId = session.id
sessionId = session.id;
const url = getRemoteSessionUrl(session.id, process.env.SESSION_INGRESS_URL)
const url = getRemoteSessionUrl(session.id, process.env.SESSION_INGRESS_URL);
setAppState(prev => ({
...prev,
ultraplanSessionUrl: url,
ultraplanLaunching: undefined,
}))
onSessionReady?.(buildSessionReadyMessage(url))
}));
onSessionReady?.(buildSessionReadyMessage(url));
logEvent('tengu_ultraplan_launched', {
has_seed_plan: Boolean(seedPlan),
model:
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
// TODO(#23985): replace registerRemoteAgentTask + startDetachedPoll with
// ExitPlanModeScanner inside startRemoteSessionPolling.
const { taskId } = registerRemoteAgentTask({
@@ -442,44 +398,35 @@ async function launchDetached(opts: {
setAppState,
},
isUltraplan: true,
})
startDetachedPoll(taskId, session.id, url, getAppState, setAppState)
});
startDetachedPoll(taskId, session.id, url, getAppState, setAppState);
} catch (e) {
logError(e)
logError(e);
logEvent('tengu_ultraplan_create_failed', {
reason:
'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
reason: 'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
});
enqueuePendingNotification({
value: `ultraplan: unexpected error — ${errorMessage(e)}`,
mode: 'task-notification',
})
});
if (sessionId) {
// Error after teleport succeeded — archive so the remote doesn't sit
// running for 30min with nobody polling it.
void archiveRemoteSession(sessionId).catch(err =>
logForDebugging('ultraplan: failed to archive orphaned session', err),
)
);
// ultraplanSessionUrl may have been set before the throw; clear it so
// the "already polling" guard doesn't block future launches.
setAppState(prev =>
prev.ultraplanSessionUrl
? { ...prev, ultraplanSessionUrl: undefined }
: prev,
)
setAppState(prev => (prev.ultraplanSessionUrl ? { ...prev, ultraplanSessionUrl: undefined } : prev));
}
} finally {
// No-op on success: the url-setting setAppState already cleared this.
setAppState(prev =>
prev.ultraplanLaunching
? { ...prev, ultraplanLaunching: undefined }
: prev,
)
setAppState(prev => (prev.ultraplanLaunching ? { ...prev, ultraplanLaunching: undefined } : prev));
}
}
const call: LocalJSXCommandCall = async (onDone, context, args) => {
const blurb = args.trim()
const blurb = args.trim();
// Bare /ultraplan (no args, no seed plan) just shows usage — no dialog.
if (!blurb) {
@@ -488,41 +435,40 @@ const call: LocalJSXCommandCall = async (onDone, context, args) => {
getAppState: context.getAppState,
setAppState: context.setAppState,
signal: context.abortController.signal,
})
onDone(msg, { display: 'system' })
return null
});
onDone(msg, { display: 'system' });
return null;
}
// Guard matches launchUltraplan's own check — showing the dialog when a
// session is already active or launching would waste the user's click and set
// hasSeenUltraplanTerms before the launch fails.
const { ultraplanSessionUrl: active, ultraplanLaunching } =
context.getAppState()
const { ultraplanSessionUrl: active, ultraplanLaunching } = context.getAppState();
if (active || ultraplanLaunching) {
logEvent('tengu_ultraplan_create_failed', {
reason: (active
? 'already_polling'
: 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
onDone(buildAlreadyActiveMessage(active), { display: 'system' })
return null
});
onDone(buildAlreadyActiveMessage(active), { display: 'system' });
return null;
}
// Mount the pre-launch dialog via focusedInputDialog (bottom region, like
// permission dialogs) rather than returning JSX (transcript area, anchors
// at top of scrollback). REPL.tsx handles launch/clear/cancel on choice.
context.setAppState(prev => ({ ...prev, ultraplanLaunchPending: { blurb } }))
context.setAppState(prev => ({ ...prev, ultraplanLaunchPending: { blurb } }));
// 'skip' suppresses the (no content) echo — the dialog's choice handler
// adds the real /ultraplan echo + launch confirmation.
onDone(undefined, { display: 'skip' })
return null
}
onDone(undefined, { display: 'skip' });
return null;
};
export default {
type: 'local-jsx',
name: 'ultraplan',
description: `~1030 min · Claude Code on the web drafts an advanced plan you can edit and approve. See ${CCR_TERMS_URL}`,
argumentHint: '<prompt>',
isEnabled: () => process.env.USER_TYPE === 'ant',
isEnabled: () => true,
load: () => Promise.resolve({ call }),
} satisfies Command
} satisfies Command;