feat: build
This commit is contained in:
commit
f90eee85d8
1906 files changed
+514339
No files matched your search
@@ -0,0 +1,493 @@
|
||||
import { feature } from 'bun:bundle'
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { z } from 'zod/v4'
|
||||
import {
|
||||
getAllowedChannels,
|
||||
hasExitedPlanModeInSession,
|
||||
setHasExitedPlanMode,
|
||||
setNeedsAutoModeExitAttachment,
|
||||
setNeedsPlanModeExitAttachment,
|
||||
} from '../../bootstrap/state.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS } from '../../services/analytics/metadata.js'
|
||||
import {
|
||||
buildTool,
|
||||
type Tool,
|
||||
type ToolDef,
|
||||
toolMatchesName,
|
||||
} from '../../Tool.js'
|
||||
import { formatAgentId, generateRequestId } from '../../utils/agentId.js'
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import {
|
||||
findInProcessTeammateTaskId,
|
||||
setAwaitingPlanApproval,
|
||||
} from '../../utils/inProcessTeammateHelpers.js'
|
||||
import { lazySchema } from '../../utils/lazySchema.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import {
|
||||
getPlan,
|
||||
getPlanFilePath,
|
||||
persistFileSnapshotIfRemote,
|
||||
} from '../../utils/plans.js'
|
||||
import { jsonStringify } from '../../utils/slowOperations.js'
|
||||
import {
|
||||
getAgentName,
|
||||
getTeamName,
|
||||
isPlanModeRequired,
|
||||
isTeammate,
|
||||
} from '../../utils/teammate.js'
|
||||
import { writeToMailbox } from '../../utils/teammateMailbox.js'
|
||||
import { AGENT_TOOL_NAME } from '../AgentTool/constants.js'
|
||||
import { TEAM_CREATE_TOOL_NAME } from '../TeamCreateTool/constants.js'
|
||||
import { EXIT_PLAN_MODE_V2_TOOL_NAME } from './constants.js'
|
||||
import { EXIT_PLAN_MODE_V2_TOOL_PROMPT } from './prompt.js'
|
||||
import {
|
||||
renderToolResultMessage,
|
||||
renderToolUseMessage,
|
||||
renderToolUseRejectedMessage,
|
||||
} from './UI.js'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const autoModeStateModule = feature('TRANSCRIPT_CLASSIFIER')
|
||||
? (require('../../utils/permissions/autoModeState.js') as typeof import('../../utils/permissions/autoModeState.js'))
|
||||
: null
|
||||
const permissionSetupModule = feature('TRANSCRIPT_CLASSIFIER')
|
||||
? (require('../../utils/permissions/permissionSetup.js') as typeof import('../../utils/permissions/permissionSetup.js'))
|
||||
: null
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
/**
|
||||
* Schema for prompt-based permission requests.
|
||||
* Used by Claude to request semantic permissions when exiting plan mode.
|
||||
*/
|
||||
const allowedPromptSchema = lazySchema(() =>
|
||||
z.object({
|
||||
tool: z.enum(['Bash']).describe('The tool this prompt applies to'),
|
||||
prompt: z
|
||||
.string()
|
||||
.describe(
|
||||
'Semantic description of the action, e.g. "run tests", "install dependencies"',
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
export type AllowedPrompt = z.infer<ReturnType<typeof allowedPromptSchema>>
|
||||
|
||||
const inputSchema = lazySchema(() =>
|
||||
z
|
||||
.strictObject({
|
||||
// Prompt-based permissions requested by the plan
|
||||
allowedPrompts: z
|
||||
.array(allowedPromptSchema())
|
||||
.optional()
|
||||
.describe(
|
||||
'Prompt-based permissions needed to implement the plan. These describe categories of actions rather than specific commands.',
|
||||
),
|
||||
})
|
||||
.passthrough(),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
|
||||
/**
|
||||
* SDK-facing input schema - includes fields injected by normalizeToolInput.
|
||||
* The internal inputSchema doesn't have these fields because plan is read from disk,
|
||||
* but the SDK/hooks see the normalized version with plan and file path included.
|
||||
*/
|
||||
export const _sdkInputSchema = lazySchema(() =>
|
||||
inputSchema().extend({
|
||||
plan: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('The plan content (injected by normalizeToolInput from disk)'),
|
||||
planFilePath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('The plan file path (injected by normalizeToolInput)'),
|
||||
}),
|
||||
)
|
||||
|
||||
export const outputSchema = lazySchema(() =>
|
||||
z.object({
|
||||
plan: z
|
||||
.string()
|
||||
.nullable()
|
||||
.describe('The plan that was presented to the user'),
|
||||
isAgent: z.boolean(),
|
||||
filePath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('The file path where the plan was saved'),
|
||||
hasTaskTool: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Whether the Agent tool is available in the current context'),
|
||||
planWasEdited: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
'True when the user edited the plan (CCR web UI or Ctrl+G); determines whether the plan is echoed back in tool_result',
|
||||
),
|
||||
awaitingLeaderApproval: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe(
|
||||
'When true, the teammate has sent a plan approval request to the team leader',
|
||||
),
|
||||
requestId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Unique identifier for the plan approval request'),
|
||||
}),
|
||||
)
|
||||
type OutputSchema = ReturnType<typeof outputSchema>
|
||||
|
||||
export type Output = z.infer<OutputSchema>
|
||||
|
||||
export const ExitPlanModeV2Tool: Tool<InputSchema, Output> = buildTool({
|
||||
name: EXIT_PLAN_MODE_V2_TOOL_NAME,
|
||||
searchHint: 'present plan for approval and start coding (plan mode only)',
|
||||
maxResultSizeChars: 100_000,
|
||||
async description() {
|
||||
return 'Prompts the user to exit plan mode and start coding'
|
||||
},
|
||||
async prompt() {
|
||||
return EXIT_PLAN_MODE_V2_TOOL_PROMPT
|
||||
},
|
||||
get inputSchema(): InputSchema {
|
||||
return inputSchema()
|
||||
},
|
||||
get outputSchema(): OutputSchema {
|
||||
return outputSchema()
|
||||
},
|
||||
userFacingName() {
|
||||
return ''
|
||||
},
|
||||
shouldDefer: true,
|
||||
isEnabled() {
|
||||
// When --channels is active the user is likely on Telegram/Discord, not
|
||||
// watching the TUI. The plan-approval dialog would hang. Paired with the
|
||||
// same gate on EnterPlanMode so plan mode isn't a trap.
|
||||
if (
|
||||
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
||||
getAllowedChannels().length > 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
isConcurrencySafe() {
|
||||
return true
|
||||
},
|
||||
isReadOnly() {
|
||||
return false // Now writes to disk
|
||||
},
|
||||
requiresUserInteraction() {
|
||||
// For ALL teammates, no local user interaction needed:
|
||||
// - If isPlanModeRequired(): team lead approves via mailbox
|
||||
// - Otherwise: exits locally without approval (voluntary plan mode)
|
||||
if (isTeammate()) {
|
||||
return false
|
||||
}
|
||||
// For non-teammates, require user confirmation to exit plan mode
|
||||
return true
|
||||
},
|
||||
async validateInput(_input, { getAppState, options }) {
|
||||
// Teammate AppState may show leader's mode (runAgent.ts skips override in
|
||||
// acceptEdits/bypassPermissions/auto); isPlanModeRequired() is the real source
|
||||
if (isTeammate()) {
|
||||
return { result: true }
|
||||
}
|
||||
// The deferred-tool list announces this tool regardless of mode, so the
|
||||
// model can call it after plan approval (fresh delta on compact/clear).
|
||||
// Reject before checkPermissions to avoid showing the approval dialog.
|
||||
const mode = getAppState().toolPermissionContext.mode
|
||||
if (mode !== 'plan') {
|
||||
logEvent('tengu_exit_plan_mode_called_outside_plan', {
|
||||
model:
|
||||
options.mainLoopModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
mode: mode as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
hasExitedPlanModeInSession: hasExitedPlanModeInSession(),
|
||||
})
|
||||
return {
|
||||
result: false,
|
||||
message:
|
||||
'You are not in plan mode. This tool is only for exiting plan mode after writing a plan. If your plan was already approved, continue with implementation.',
|
||||
errorCode: 1,
|
||||
}
|
||||
}
|
||||
return { result: true }
|
||||
},
|
||||
async checkPermissions(input, context) {
|
||||
// For ALL teammates, bypass the permission UI to avoid sending permission_request
|
||||
// The call() method handles the appropriate behavior:
|
||||
// - If isPlanModeRequired(): sends plan_approval_request to leader
|
||||
// - Otherwise: exits plan mode locally (voluntary plan mode)
|
||||
if (isTeammate()) {
|
||||
return {
|
||||
behavior: 'allow' as const,
|
||||
updatedInput: input,
|
||||
}
|
||||
}
|
||||
|
||||
// For non-teammates, require user confirmation to exit plan mode
|
||||
return {
|
||||
behavior: 'ask' as const,
|
||||
message: 'Exit plan mode?',
|
||||
updatedInput: input,
|
||||
}
|
||||
},
|
||||
renderToolUseMessage,
|
||||
renderToolResultMessage,
|
||||
renderToolUseRejectedMessage,
|
||||
async call(input, context) {
|
||||
const isAgent = !!context.agentId
|
||||
|
||||
const filePath = getPlanFilePath(context.agentId)
|
||||
// CCR web UI may send an edited plan via permissionResult.updatedInput.
|
||||
// queryHelpers.ts full-replaces finalInput, so when CCR sends {} (no edit)
|
||||
// input.plan is undefined -> disk fallback. The internal inputSchema omits
|
||||
// `plan` (normally injected by normalizeToolInput), hence the narrowing.
|
||||
const inputPlan =
|
||||
'plan' in input && typeof input.plan === 'string' ? input.plan : undefined
|
||||
const plan = inputPlan ?? getPlan(context.agentId)
|
||||
|
||||
// Sync disk so VerifyPlanExecution / Read see the edit. Re-snapshot
|
||||
// after: the only other persistFileSnapshotIfRemote call (api.ts) runs
|
||||
// in normalizeToolInput, pre-permission — it captured the old plan.
|
||||
if (inputPlan !== undefined && filePath) {
|
||||
await writeFile(filePath, inputPlan, 'utf-8').catch(e => logError(e))
|
||||
void persistFileSnapshotIfRemote()
|
||||
}
|
||||
|
||||
// Check if this is a teammate that requires leader approval
|
||||
if (isTeammate() && isPlanModeRequired()) {
|
||||
// Plan is required for plan_mode_required teammates
|
||||
if (!plan) {
|
||||
throw new Error(
|
||||
`No plan file found at ${filePath}. Please write your plan to this file before calling ExitPlanMode.`,
|
||||
)
|
||||
}
|
||||
const agentName = getAgentName() || 'unknown'
|
||||
const teamName = getTeamName()
|
||||
const requestId = generateRequestId(
|
||||
'plan_approval',
|
||||
formatAgentId(agentName, teamName || 'default'),
|
||||
)
|
||||
|
||||
const approvalRequest = {
|
||||
type: 'plan_approval_request',
|
||||
from: agentName,
|
||||
timestamp: new Date().toISOString(),
|
||||
planFilePath: filePath,
|
||||
planContent: plan,
|
||||
requestId,
|
||||
}
|
||||
|
||||
await writeToMailbox(
|
||||
'team-lead',
|
||||
{
|
||||
from: agentName,
|
||||
text: jsonStringify(approvalRequest),
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
teamName,
|
||||
)
|
||||
|
||||
// Update task state to show awaiting approval (for in-process teammates)
|
||||
const appState = context.getAppState()
|
||||
const agentTaskId = findInProcessTeammateTaskId(agentName, appState)
|
||||
if (agentTaskId) {
|
||||
setAwaitingPlanApproval(agentTaskId, context.setAppState, true)
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
plan,
|
||||
isAgent: true,
|
||||
filePath,
|
||||
awaitingLeaderApproval: true,
|
||||
requestId,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Background verification hook is registered in REPL.tsx AFTER context clear
|
||||
// via registerPlanVerificationHook(). Registering here would be cleared during context clear.
|
||||
|
||||
// Ensure mode is changed when exiting plan mode.
|
||||
// This handles cases where permission flow didn't set the mode
|
||||
// (e.g., when PermissionRequest hook auto-approves without providing updatedPermissions).
|
||||
const appState = context.getAppState()
|
||||
// Compute gate-off fallback before setAppState so we can notify the user.
|
||||
// Circuit breaker defense: if prePlanMode was an auto-like mode but the
|
||||
// gate is now off (circuit breaker or settings disable), restore to
|
||||
// 'default' instead. Without this, ExitPlanMode would bypass the circuit
|
||||
// breaker by calling setAutoModeActive(true) directly.
|
||||
let gateFallbackNotification: string | null = null
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
const prePlanRaw = appState.toolPermissionContext.prePlanMode ?? 'default'
|
||||
if (
|
||||
prePlanRaw === 'auto' &&
|
||||
!(permissionSetupModule?.isAutoModeGateEnabled() ?? false)
|
||||
) {
|
||||
const reason =
|
||||
permissionSetupModule?.getAutoModeUnavailableReason() ??
|
||||
'circuit-breaker'
|
||||
gateFallbackNotification =
|
||||
permissionSetupModule?.getAutoModeUnavailableNotification(reason) ??
|
||||
'auto mode unavailable'
|
||||
logForDebugging(
|
||||
`[auto-mode gate @ ExitPlanModeV2Tool] prePlanMode=${prePlanRaw} ` +
|
||||
`but gate is off (reason=${reason}) — falling back to default on plan exit`,
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
}
|
||||
if (gateFallbackNotification) {
|
||||
context.addNotification?.({
|
||||
key: 'auto-mode-gate-plan-exit-fallback',
|
||||
text: `plan exit → default · ${gateFallbackNotification}`,
|
||||
priority: 'immediate',
|
||||
color: 'warning',
|
||||
timeoutMs: 10000,
|
||||
})
|
||||
}
|
||||
|
||||
context.setAppState(prev => {
|
||||
if (prev.toolPermissionContext.mode !== 'plan') return prev
|
||||
setHasExitedPlanMode(true)
|
||||
setNeedsPlanModeExitAttachment(true)
|
||||
let restoreMode = prev.toolPermissionContext.prePlanMode ?? 'default'
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
if (
|
||||
restoreMode === 'auto' &&
|
||||
!(permissionSetupModule?.isAutoModeGateEnabled() ?? false)
|
||||
) {
|
||||
restoreMode = 'default'
|
||||
}
|
||||
const finalRestoringAuto = restoreMode === 'auto'
|
||||
// Capture pre-restore state — isAutoModeActive() is the authoritative
|
||||
// signal (prePlanMode/strippedDangerousRules are stale after
|
||||
// transitionPlanAutoMode deactivates mid-plan).
|
||||
const autoWasUsedDuringPlan =
|
||||
autoModeStateModule?.isAutoModeActive() ?? false
|
||||
autoModeStateModule?.setAutoModeActive(finalRestoringAuto)
|
||||
if (autoWasUsedDuringPlan && !finalRestoringAuto) {
|
||||
setNeedsAutoModeExitAttachment(true)
|
||||
}
|
||||
}
|
||||
// If restoring to a non-auto mode and permissions were stripped (either
|
||||
// from entering plan from auto, or from shouldPlanUseAutoMode),
|
||||
// restore them. If restoring to auto, keep them stripped.
|
||||
const restoringToAuto = restoreMode === 'auto'
|
||||
let baseContext = prev.toolPermissionContext
|
||||
if (restoringToAuto) {
|
||||
baseContext =
|
||||
permissionSetupModule?.stripDangerousPermissionsForAutoMode(
|
||||
baseContext,
|
||||
) ?? baseContext
|
||||
} else if (prev.toolPermissionContext.strippedDangerousRules) {
|
||||
baseContext =
|
||||
permissionSetupModule?.restoreDangerousPermissions(baseContext) ??
|
||||
baseContext
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
toolPermissionContext: {
|
||||
...baseContext,
|
||||
mode: restoreMode,
|
||||
prePlanMode: undefined,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const hasTaskTool =
|
||||
isAgentSwarmsEnabled() &&
|
||||
context.options.tools.some(t => toolMatchesName(t, AGENT_TOOL_NAME))
|
||||
|
||||
return {
|
||||
data: {
|
||||
plan,
|
||||
isAgent,
|
||||
filePath,
|
||||
hasTaskTool: hasTaskTool || undefined,
|
||||
planWasEdited: inputPlan !== undefined || undefined,
|
||||
},
|
||||
}
|
||||
},
|
||||
mapToolResultToToolResultBlockParam(
|
||||
{
|
||||
isAgent,
|
||||
plan,
|
||||
filePath,
|
||||
hasTaskTool,
|
||||
planWasEdited,
|
||||
awaitingLeaderApproval,
|
||||
requestId,
|
||||
},
|
||||
toolUseID,
|
||||
) {
|
||||
// Handle teammate awaiting leader approval
|
||||
if (awaitingLeaderApproval) {
|
||||
return {
|
||||
type: 'tool_result',
|
||||
content: `Your plan has been submitted to the team lead for approval.
|
||||
|
||||
Plan file: ${filePath}
|
||||
|
||||
**What happens next:**
|
||||
1. Wait for the team lead to review your plan
|
||||
2. You will receive a message in your inbox with approval/rejection
|
||||
3. If approved, you can proceed with implementation
|
||||
4. If rejected, refine your plan based on the feedback
|
||||
|
||||
**Important:** Do NOT proceed until you receive approval. Check your inbox for response.
|
||||
|
||||
Request ID: ${requestId}`,
|
||||
tool_use_id: toolUseID,
|
||||
}
|
||||
}
|
||||
|
||||
if (isAgent) {
|
||||
return {
|
||||
type: 'tool_result',
|
||||
content:
|
||||
'User has approved the plan. There is nothing else needed from you now. Please respond with "ok"',
|
||||
tool_use_id: toolUseID,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle empty plan
|
||||
if (!plan || plan.trim() === '') {
|
||||
return {
|
||||
type: 'tool_result',
|
||||
content: 'User has approved exiting plan mode. You can now proceed.',
|
||||
tool_use_id: toolUseID,
|
||||
}
|
||||
}
|
||||
|
||||
const teamHint = hasTaskTool
|
||||
? `\n\nIf this plan can be broken down into multiple independent tasks, consider using the ${TEAM_CREATE_TOOL_NAME} tool to create a team and parallelize the work.`
|
||||
: ''
|
||||
|
||||
// Always include the plan — extractApprovedPlan() in the Ultraplan CCR
|
||||
// flow parses the tool_result to retrieve the plan text for the local CLI.
|
||||
// Label edited plans so the model knows the user changed something.
|
||||
const planLabel = planWasEdited
|
||||
? 'Approved Plan (edited by user)'
|
||||
: 'Approved Plan'
|
||||
|
||||
return {
|
||||
type: 'tool_result',
|
||||
content: `User has approved your plan. You can now start coding. Start with updating your todo list if applicable
|
||||
|
||||
Your plan has been saved to: ${filePath}
|
||||
You can refer back to it if needed during implementation.${teamHint}
|
||||
|
||||
## ${planLabel}:
|
||||
${plan}`,
|
||||
tool_use_id: toolUseID,
|
||||
}
|
||||
},
|
||||
} satisfies ToolDef<InputSchema, Output>)
|
||||
@@ -0,0 +1,82 @@
|
||||
import * as React from 'react';
|
||||
import { Markdown } from 'src/components/Markdown.js';
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js';
|
||||
import { RejectedPlanMessage } from 'src/components/messages/UserToolResultMessage/RejectedPlanMessage.js';
|
||||
import { BLACK_CIRCLE } from 'src/constants/figures.js';
|
||||
import { getModeColor } from 'src/utils/permissions/PermissionMode.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { ToolProgressData } from '../../Tool.js';
|
||||
import type { ProgressMessage } from '../../types/message.js';
|
||||
import { getDisplayPath } from '../../utils/file.js';
|
||||
import { getPlan } from '../../utils/plans.js';
|
||||
import type { ThemeName } from '../../utils/theme.js';
|
||||
import type { Output } from './ExitPlanModeV2Tool.js';
|
||||
export function renderToolUseMessage(): React.ReactNode {
|
||||
return null;
|
||||
}
|
||||
export function renderToolResultMessage(output: Output, _progressMessagesForMessage: ProgressMessage<ToolProgressData>[], {
|
||||
theme: _theme
|
||||
}: {
|
||||
theme: ThemeName;
|
||||
}): React.ReactNode {
|
||||
const {
|
||||
plan,
|
||||
filePath
|
||||
} = output;
|
||||
const isEmpty = !plan || plan.trim() === '';
|
||||
const displayPath = filePath ? getDisplayPath(filePath) : '';
|
||||
const awaitingLeaderApproval = output.awaitingLeaderApproval;
|
||||
|
||||
// Simplified message for empty plans
|
||||
if (isEmpty) {
|
||||
return <Box flexDirection="column" marginTop={1}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={getModeColor('plan')}>{BLACK_CIRCLE}</Text>
|
||||
<Text> Exited plan mode</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
// When awaiting leader approval, show a different message
|
||||
if (awaitingLeaderApproval) {
|
||||
return <Box flexDirection="column" marginTop={1}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={getModeColor('plan')}>{BLACK_CIRCLE}</Text>
|
||||
<Text> Plan submitted for team lead approval</Text>
|
||||
</Box>
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
{filePath && <Text dimColor>Plan file: {displayPath}</Text>}
|
||||
<Text dimColor>Waiting for team lead to review and approve...</Text>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
</Box>;
|
||||
}
|
||||
return <Box flexDirection="column" marginTop={1}>
|
||||
<Box flexDirection="row">
|
||||
<Text color={getModeColor('plan')}>{BLACK_CIRCLE}</Text>
|
||||
<Text> User approved Claude's plan</Text>
|
||||
</Box>
|
||||
<MessageResponse>
|
||||
<Box flexDirection="column">
|
||||
{filePath && <Text dimColor>Plan saved to: {displayPath} · /plan to edit</Text>}
|
||||
<Markdown>{plan}</Markdown>
|
||||
</Box>
|
||||
</MessageResponse>
|
||||
</Box>;
|
||||
}
|
||||
export function renderToolUseRejectedMessage({
|
||||
plan
|
||||
}: {
|
||||
plan?: string;
|
||||
}, {
|
||||
theme: _theme
|
||||
}: {
|
||||
theme: ThemeName;
|
||||
}): React.ReactNode {
|
||||
const planContent = plan ?? getPlan() ?? 'No plan found';
|
||||
return <Box flexDirection="column">
|
||||
<RejectedPlanMessage plan={planContent} />
|
||||
</Box>;
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJSZWFjdCIsIk1hcmtkb3duIiwiTWVzc2FnZVJlc3BvbnNlIiwiUmVqZWN0ZWRQbGFuTWVzc2FnZSIsIkJMQUNLX0NJUkNMRSIsImdldE1vZGVDb2xvciIsIkJveCIsIlRleHQiLCJUb29sUHJvZ3Jlc3NEYXRhIiwiUHJvZ3Jlc3NNZXNzYWdlIiwiZ2V0RGlzcGxheVBhdGgiLCJnZXRQbGFuIiwiVGhlbWVOYW1lIiwiT3V0cHV0IiwicmVuZGVyVG9vbFVzZU1lc3NhZ2UiLCJSZWFjdE5vZGUiLCJyZW5kZXJUb29sUmVzdWx0TWVzc2FnZSIsIm91dHB1dCIsIl9wcm9ncmVzc01lc3NhZ2VzRm9yTWVzc2FnZSIsInRoZW1lIiwiX3RoZW1lIiwicGxhbiIsImZpbGVQYXRoIiwiaXNFbXB0eSIsInRyaW0iLCJkaXNwbGF5UGF0aCIsImF3YWl0aW5nTGVhZGVyQXBwcm92YWwiLCJyZW5kZXJUb29sVXNlUmVqZWN0ZWRNZXNzYWdlIiwicGxhbkNvbnRlbnQiXSwic291cmNlcyI6WyJVSS50c3giXSwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgUmVhY3QgZnJvbSAncmVhY3QnXG5pbXBvcnQgeyBNYXJrZG93biB9IGZyb20gJ3NyYy9jb21wb25lbnRzL01hcmtkb3duLmpzJ1xuaW1wb3J0IHsgTWVzc2FnZVJlc3BvbnNlIH0gZnJvbSAnc3JjL2NvbXBvbmVudHMvTWVzc2FnZVJlc3BvbnNlLmpzJ1xuaW1wb3J0IHsgUmVqZWN0ZWRQbGFuTWVzc2FnZSB9IGZyb20gJ3NyYy9jb21wb25lbnRzL21lc3NhZ2VzL1VzZXJUb29sUmVzdWx0TWVzc2FnZS9SZWplY3RlZFBsYW5NZXNzYWdlLmpzJ1xuaW1wb3J0IHsgQkxBQ0tfQ0lSQ0xFIH0gZnJvbSAnc3JjL2NvbnN0YW50cy9maWd1cmVzLmpzJ1xuaW1wb3J0IHsgZ2V0TW9kZUNvbG9yIH0gZnJvbSAnc3JjL3V0aWxzL3Blcm1pc3Npb25zL1Blcm1pc3Npb25Nb2RlLmpzJ1xuaW1wb3J0IHsgQm94LCBUZXh0IH0gZnJvbSAnLi4vLi4vaW5rLmpzJ1xuaW1wb3J0IHR5cGUgeyBUb29sUHJvZ3Jlc3NEYXRhIH0gZnJvbSAnLi4vLi4vVG9vbC5qcydcbmltcG9ydCB0eXBlIHsgUHJvZ3Jlc3NNZXNzYWdlIH0gZnJvbSAnLi4vLi4vdHlwZXMvbWVzc2FnZS5qcydcbmltcG9ydCB7IGdldERpc3BsYXlQYXRoIH0gZnJvbSAnLi4vLi4vdXRpbHMvZmlsZS5qcydcbmltcG9ydCB7IGdldFBsYW4gfSBmcm9tICcuLi8uLi91dGlscy9wbGFucy5qcydcbmltcG9ydCB0eXBlIHsgVGhlbWVOYW1lIH0gZnJvbSAnLi4vLi4vdXRpbHMvdGhlbWUuanMnXG5pbXBvcnQgdHlwZSB7IE91dHB1dCB9IGZyb20gJy4vRXhpdFBsYW5Nb2RlVjJUb29sLmpzJ1xuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFVzZU1lc3NhZ2UoKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgcmV0dXJuIG51bGxcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJlbmRlclRvb2xSZXN1bHRNZXNzYWdlKFxuICBvdXRwdXQ6IE91dHB1dCxcbiAgX3Byb2dyZXNzTWVzc2FnZXNGb3JNZXNzYWdlOiBQcm9ncmVzc01lc3NhZ2U8VG9vbFByb2dyZXNzRGF0YT5bXSxcbiAgeyB0aGVtZTogX3RoZW1lIH06IHsgdGhlbWU6IFRoZW1lTmFtZSB9LFxuKTogUmVhY3QuUmVhY3ROb2RlIHtcbiAgY29uc3QgeyBwbGFuLCBmaWxlUGF0aCB9ID0gb3V0cHV0XG4gIGNvbnN0IGlzRW1wdHkgPSAhcGxhbiB8fCBwbGFuLnRyaW0oKSA9PT0gJydcbiAgY29uc3QgZGlzcGxheVBhdGggPSBmaWxlUGF0aCA/IGdldERpc3BsYXlQYXRoKGZpbGVQYXRoKSA6ICcnXG4gIGNvbnN0IGF3YWl0aW5nTGVhZGVyQXBwcm92YWwgPSBvdXRwdXQuYXdhaXRpbmdMZWFkZXJBcHByb3ZhbFxuXG4gIC8vIFNpbXBsaWZpZWQgbWVzc2FnZSBmb3IgZW1wdHkgcGxhbnNcbiAgaWYgKGlzRW1wdHkpIHtcbiAgICByZXR1cm4gKFxuICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfT5cbiAgICAgICAgPEJveCBmbGV4RGlyZWN0aW9uPVwicm93XCI+XG4gICAgICAgICAgPFRleHQgY29sb3I9e2dldE1vZGVDb2xvcigncGxhbicpfT57QkxBQ0tfQ0lSQ0xFfTwvVGV4dD5cbiAgICAgICAgICA8VGV4dD4gRXhpdGVkIHBsYW4gbW9kZTwvVGV4dD5cbiAgICAgICAgPC9Cb3g+XG4gICAgICA8L0JveD5cbiAgICApXG4gIH1cblxuICAvLyBXaGVuIGF3YWl0aW5nIGxlYWRlciBhcHByb3ZhbCwgc2hvdyBhIGRpZmZlcmVudCBtZXNzYWdlXG4gIGlmIChhd2FpdGluZ0xlYWRlckFwcHJvdmFsKSB7XG4gICAgcmV0dXJuIChcbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiIG1hcmdpblRvcD17MX0+XG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICAgIDxUZXh0IGNvbG9yPXtnZXRNb2RlQ29sb3IoJ3BsYW4nKX0+e0JMQUNLX0NJUkNMRX08L1RleHQ+XG4gICAgICAgICAgPFRleHQ+IFBsYW4gc3VibWl0dGVkIGZvciB0ZWFtIGxlYWQgYXBwcm92YWw8L1RleHQ+XG4gICAgICAgIDwvQm94PlxuICAgICAgICA8TWVzc2FnZVJlc3BvbnNlPlxuICAgICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgICAge2ZpbGVQYXRoICYmIDxUZXh0IGRpbUNvbG9yPlBsYW4gZmlsZToge2Rpc3BsYXlQYXRofTwvVGV4dD59XG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5XYWl0aW5nIGZvciB0ZWFtIGxlYWQgdG8gcmV2aWV3IGFuZCBhcHByb3ZlLi4uPC9UZXh0PlxuICAgICAgICAgIDwvQm94PlxuICAgICAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgICAgIDwvQm94PlxuICAgIClcbiAgfVxuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCIgbWFyZ2luVG9wPXsxfT5cbiAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cInJvd1wiPlxuICAgICAgICA8VGV4dCBjb2xvcj17Z2V0TW9kZUNvbG9yKCdwbGFuJyl9PntCTEFDS19DSVJDTEV9PC9UZXh0PlxuICAgICAgICA8VGV4dD4gVXNlciBhcHByb3ZlZCBDbGF1ZGUmYXBvcztzIHBsYW48L1RleHQ+XG4gICAgICA8L0JveD5cbiAgICAgIDxNZXNzYWdlUmVzcG9uc2U+XG4gICAgICAgIDxCb3ggZmxleERpcmVjdGlvbj1cImNvbHVtblwiPlxuICAgICAgICAgIHtmaWxlUGF0aCAmJiAoXG4gICAgICAgICAgICA8VGV4dCBkaW1Db2xvcj5QbGFuIHNhdmVkIHRvOiB7ZGlzcGxheVBhdGh9IMK3IC9wbGFuIHRvIGVkaXQ8L1RleHQ+XG4gICAgICAgICAgKX1cbiAgICAgICAgICA8TWFya2Rvd24+e3BsYW59PC9NYXJrZG93bj5cbiAgICAgICAgPC9Cb3g+XG4gICAgICA8L01lc3NhZ2VSZXNwb25zZT5cbiAgICA8L0JveD5cbiAgKVxufVxuXG5leHBvcnQgZnVuY3Rpb24gcmVuZGVyVG9vbFVzZVJlamVjdGVkTWVzc2FnZShcbiAgeyBwbGFuIH06IHsgcGxhbj86IHN0cmluZyB9LFxuICB7IHRoZW1lOiBfdGhlbWUgfTogeyB0aGVtZTogVGhlbWVOYW1lIH0sXG4pOiBSZWFjdC5SZWFjdE5vZGUge1xuICBjb25zdCBwbGFuQ29udGVudCA9IHBsYW4gPz8gZ2V0UGxhbigpID8/ICdObyBwbGFuIGZvdW5kJ1xuXG4gIHJldHVybiAoXG4gICAgPEJveCBmbGV4RGlyZWN0aW9uPVwiY29sdW1uXCI+XG4gICAgICA8UmVqZWN0ZWRQbGFuTWVzc2FnZSBwbGFuPXtwbGFuQ29udGVudH0gLz5cbiAgICA8L0JveD5cbiAgKVxufVxuIl0sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUtBLEtBQUssTUFBTSxPQUFPO0FBQzlCLFNBQVNDLFFBQVEsUUFBUSw0QkFBNEI7QUFDckQsU0FBU0MsZUFBZSxRQUFRLG1DQUFtQztBQUNuRSxTQUFTQyxtQkFBbUIsUUFBUSxzRUFBc0U7QUFDMUcsU0FBU0MsWUFBWSx
Line truncated
|
||||
@@ -0,0 +1,2 @@
|
||||
export const EXIT_PLAN_MODE_TOOL_NAME = 'ExitPlanMode'
|
||||
export const EXIT_PLAN_MODE_V2_TOOL_NAME = 'ExitPlanMode'
|
||||
@@ -0,0 +1,29 @@
|
||||
// External stub for ExitPlanModeTool prompt - excludes Ant-only allowedPrompts section
|
||||
|
||||
// Hardcoded to avoid relative import issues in stub
|
||||
const ASK_USER_QUESTION_TOOL_NAME = 'AskUserQuestion'
|
||||
|
||||
export const EXIT_PLAN_MODE_V2_TOOL_PROMPT = `Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.
|
||||
|
||||
## How This Tool Works
|
||||
- You should have already written your plan to the plan file specified in the plan mode system message
|
||||
- This tool does NOT take the plan content as a parameter - it will read the plan from the file you wrote
|
||||
- This tool simply signals that you're done planning and ready for the user to review and approve
|
||||
- The user will see the contents of your plan file when they review it
|
||||
|
||||
## When to Use This Tool
|
||||
IMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you're gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.
|
||||
|
||||
## Before Using This Tool
|
||||
Ensure your plan is complete and unambiguous:
|
||||
- If you have unresolved questions about requirements or approach, use ${ASK_USER_QUESTION_TOOL_NAME} first (in earlier phases)
|
||||
- Once your plan is finalized, use THIS tool to request approval
|
||||
|
||||
**Important:** Do NOT use ${ASK_USER_QUESTION_TOOL_NAME} to ask "Is this plan okay?" or "Should I proceed?" - that's exactly what THIS tool does. ExitPlanMode inherently requests user approval of your plan.
|
||||
|
||||
## Examples
|
||||
|
||||
1. Initial task: "Search for and understand the implementation of vim mode in the codebase" - Do not use the exit plan mode tool because you are not planning the implementation steps of a task.
|
||||
2. Initial task: "Help me implement yank mode for vim" - Use the exit plan mode tool after you have finished planning the implementation steps of the task.
|
||||
3. Initial task: "Add a new feature to handle user authentication" - If unsure about auth method (OAuth, JWT, etc.), use ${ASK_USER_QUESTION_TOOL_NAME} first, then use exit plan mode tool after clarifying the approach.
|
||||
`
|
||||
Reference in new issue
Block a user