mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 00:35:51 +00:00
更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-2): 格式化 commands (79 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-5): 格式化 components其余 + hooks + tools (232 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files) 纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md - README.md: 大幅重写,更详细版本历史和配置示例 - Run.ps1: 新增 Windows 启动脚本 - TODO.md: 新增包完成清单 - V6.md: 删除(架构重构规划已不适用) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复以前的问题 * fix: 修复 login 面板的问题 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +1,42 @@
|
||||
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';
|
||||
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';
|
||||
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'
|
||||
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'
|
||||
|
||||
// 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;
|
||||
export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the-web';
|
||||
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000
|
||||
|
||||
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
|
||||
@@ -30,7 +44,10 @@ export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the
|
||||
// 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
|
||||
@@ -43,9 +60,11 @@ 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,
|
||||
@@ -53,7 +72,10 @@ const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPromp
|
||||
// Shell-set env only, so top-level process.env read is fine
|
||||
// — settings.env never injects this.
|
||||
/* eslint-disable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs -- ant-only dev override; eager top-level read is the point (crash at startup, not silently inside the slash-command try/catch) */
|
||||
const ULTRAPLAN_INSTRUCTIONS: string = (process.env.USER_TYPE) === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE ? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd() : DEFAULT_INSTRUCTIONS;
|
||||
const ULTRAPLAN_INSTRUCTIONS: string =
|
||||
process.env.USER_TYPE === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE
|
||||
? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd()
|
||||
: DEFAULT_INSTRUCTIONS
|
||||
/* eslint-enable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs */
|
||||
|
||||
/**
|
||||
@@ -61,107 +83,123 @@ const ULTRAPLAN_INSTRUCTIONS: string = (process.env.USER_TYPE) === 'ant' && proc
|
||||
* 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(taskId: string, sessionId: string, url: string, getAppState: () => AppState, setAppState: (f: (prev: AppState) => AppState) => void): void {
|
||||
const started = Date.now();
|
||||
let failed = false;
|
||||
|
||||
function startDetachedPoll(
|
||||
taskId: string,
|
||||
sessionId: string,
|
||||
url: string,
|
||||
getAppState: () => AppState,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): void {
|
||||
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;
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => t.status !== 'running' ? t : {
|
||||
...t,
|
||||
status: 'completed',
|
||||
endTime: Date.now()
|
||||
});
|
||||
setAppState(prev => prev.ultraplanSessionUrl === url ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined
|
||||
} : prev);
|
||||
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,
|
||||
)
|
||||
enqueuePendingNotification({
|
||||
value: [`Ultraplan approved — executing in Claude Code on the web. Follow along at: ${url}`, '', 'Results will land as a pull request when the remote session finishes. There is nothing to do here.'].join('\n'),
|
||||
mode: 'task-notification'
|
||||
});
|
||||
value: [
|
||||
`Ultraplan approved — executing in Claude Code on the web. Follow along at: ${url}`,
|
||||
'',
|
||||
'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
|
||||
}
|
||||
};
|
||||
});
|
||||
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
|
||||
});
|
||||
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,
|
||||
})
|
||||
enqueuePendingNotification({
|
||||
value: `Ultraplan failed: ${errorMessage(e)}\n\nSession: ${url}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
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);
|
||||
// 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,
|
||||
)
|
||||
} finally {
|
||||
// Remote path already set status=completed above; teleport path
|
||||
// leaves status=running so the pill shows the ultraplanPhase state
|
||||
@@ -170,27 +208,31 @@ function startDetachedPoll(taskId: string, sessionId: string, url: string, getAp
|
||||
// isBackgroundTask before the pill can render the phase state.
|
||||
// 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()
|
||||
});
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t =>
|
||||
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.';
|
||||
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.'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,26 +242,37 @@ function buildAlreadyActiveMessage(url: string | undefined): string {
|
||||
* shouldStop callback sees the killed status on its next tick and throws;
|
||||
* the catch block early-returns when status !== 'running'.
|
||||
*/
|
||||
export async function stopUltraplan(taskId: string, sessionId: string, setAppState: (f: (prev: AppState) => AppState) => void): Promise<void> {
|
||||
export async function stopUltraplan(
|
||||
taskId: string,
|
||||
sessionId: string,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): Promise<void> {
|
||||
// RemoteAgentTask.kill archives the session (with .catch) — no separate
|
||||
// archive call needed here.
|
||||
await RemoteAgentTask.kill(taskId, setAppState);
|
||||
setAppState(prev => prev.ultraplanSessionUrl || prev.ultraplanPendingChoice || prev.ultraplanLaunching ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined,
|
||||
ultraplanPendingChoice: undefined,
|
||||
ultraplanLaunching: undefined
|
||||
} : prev);
|
||||
const url = getRemoteSessionUrl(sessionId, process.env.SESSION_INGRESS_URL);
|
||||
await RemoteAgentTask.kill(taskId, setAppState)
|
||||
setAppState(prev =>
|
||||
prev.ultraplanSessionUrl ||
|
||||
prev.ultraplanPendingChoice ||
|
||||
prev.ultraplanLaunching
|
||||
? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined,
|
||||
ultraplanPendingChoice: undefined,
|
||||
ultraplanLaunching: undefined,
|
||||
}
|
||||
: prev,
|
||||
)
|
||||
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
|
||||
});
|
||||
})
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,13 +285,13 @@ export async function stopUltraplan(taskId: string, sessionId: string, setAppSta
|
||||
* 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
|
||||
@@ -246,7 +299,7 @@ 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,
|
||||
@@ -255,79 +308,90 @@ export async function launchUltraplan(opts: {
|
||||
setAppState,
|
||||
signal,
|
||||
disconnectedBridge,
|
||||
onSessionReady
|
||||
} = opts;
|
||||
const {
|
||||
ultraplanSessionUrl: active,
|
||||
ultraplanLaunching
|
||||
} = getAppState();
|
||||
onSessionReady,
|
||||
} = opts
|
||||
|
||||
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);
|
||||
reason: (active
|
||||
? 'already_polling'
|
||||
: 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return buildAlreadyActiveMessage(active)
|
||||
}
|
||||
|
||||
if (!blurb && !seedPlan) {
|
||||
// No event — bare /ultraplan is a usage query, not an attempt.
|
||||
return [
|
||||
// Rendered via <Markdown>; raw <message> is tokenized as HTML
|
||||
// and dropped. Backslash-escape the brackets.
|
||||
'Usage: /ultraplan \\<prompt\\>, or include "ultraplan" anywhere', 'in your prompt', '', 'Advanced multi-agent plan mode with our most powerful model', '(Opus). Runs in Claude Code on the web. When the plan is ready,', 'you can execute it in the web session or send it back here.', 'Terminal stays free while the remote plans.', 'Requires /login.', '', `Terms: ${CCR_TERMS_URL}`].join('\n');
|
||||
// Rendered via <Markdown>; raw <message> is tokenized as HTML
|
||||
// and dropped. Backslash-escape the brackets.
|
||||
'Usage: /ultraplan \\<prompt\\>, or include "ultraplan" anywhere',
|
||||
'in your prompt',
|
||||
'',
|
||||
'Advanced multi-agent plan mode with our most powerful model',
|
||||
'(Opus). Runs in Claude Code on the web. When the plan is ready,',
|
||||
'you can execute it in the web session or send it back here.',
|
||||
'Terminal stays free while the remote plans.',
|
||||
'Requires /login.',
|
||||
'',
|
||||
`Terms: ${CCR_TERMS_URL}`,
|
||||
].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,
|
||||
getAppState,
|
||||
setAppState,
|
||||
signal,
|
||||
onSessionReady
|
||||
});
|
||||
return buildLaunchMessage(disconnectedBridge);
|
||||
onSessionReady,
|
||||
})
|
||||
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 eligibility = await checkRemoteAgentEligibility();
|
||||
const model = getUltraplanModel()
|
||||
|
||||
const eligibility = await checkRemoteAgentEligibility()
|
||||
if (!eligibility.eligible) {
|
||||
const eligErrors = (eligibility as { eligible: false; errors: Array<{ type: string }> }).errors;
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: 'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
precondition_errors: eligErrors.map(e => e.type).join(',') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
const reasons = eligErrors.map(formatPreconditionError).join('\n');
|
||||
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')
|
||||
enqueuePendingNotification({
|
||||
value: `ultraplan: cannot launch remote session —\n${reasons}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
return;
|
||||
mode: 'task-notification',
|
||||
})
|
||||
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',
|
||||
@@ -337,80 +401,85 @@ 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
|
||||
});
|
||||
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;
|
||||
mode: 'task-notification',
|
||||
})
|
||||
return
|
||||
}
|
||||
sessionId = session.id;
|
||||
const url = getRemoteSessionUrl(session.id, process.env.SESSION_INGRESS_URL);
|
||||
sessionId = session.id
|
||||
|
||||
const url = getRemoteSessionUrl(session.id, process.env.SESSION_INGRESS_URL)
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
ultraplanSessionUrl: url,
|
||||
ultraplanLaunching: undefined
|
||||
}));
|
||||
onSessionReady?.(buildSessionReadyMessage(url));
|
||||
ultraplanLaunching: undefined,
|
||||
}))
|
||||
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({
|
||||
const { taskId } = registerRemoteAgentTask({
|
||||
remoteTaskType: 'ultraplan',
|
||||
session: {
|
||||
id: session.id,
|
||||
title: blurb || 'Ultraplan'
|
||||
},
|
||||
session: { id: session.id, title: blurb || 'Ultraplan' },
|
||||
command: blurb,
|
||||
context: {
|
||||
abortController: new AbortController(),
|
||||
getAppState,
|
||||
setAppState
|
||||
setAppState,
|
||||
},
|
||||
isUltraplan: true
|
||||
});
|
||||
startDetachedPoll(taskId, session.id, url, getAppState, setAppState);
|
||||
isUltraplan: true,
|
||||
})
|
||||
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'
|
||||
});
|
||||
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));
|
||||
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) {
|
||||
@@ -418,54 +487,42 @@ const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
blurb,
|
||||
getAppState: context.getAppState,
|
||||
setAppState: context.setAppState,
|
||||
signal: context.abortController.signal
|
||||
});
|
||||
onDone(msg, {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
signal: context.abortController.signal,
|
||||
})
|
||||
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;
|
||||
reason: (active
|
||||
? 'already_polling'
|
||||
: 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
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: `~10–30 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',
|
||||
load: () => Promise.resolve({
|
||||
call
|
||||
})
|
||||
} satisfies Command;
|
||||
isEnabled: () => process.env.USER_TYPE === 'ant',
|
||||
load: () => Promise.resolve({ call }),
|
||||
} satisfies Command
|
||||
|
||||
Reference in New Issue
Block a user