Преглед изворни кода

feat: add UltraplanLaunchDialog and UltraplanChoiceDialog components

These components were referenced in REPL.tsx but never defined, causing
a runtime crash when ultraplan tried to launch. Added:

- UltraplanLaunchDialog: pre-launch confirmation with terms/cancel
- UltraplanChoiceDialog: post-plan approval with execute-here/dismiss
- Conditional requires in REPL.tsx gated on feature('ULTRAPLAN')

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
paoloanzn пре 2 месеци
родитељ
комит
fcf5ab7dbb
3 измењених фајлова са 162 додато и 0 уклоњено
  1. 114 0
      src/components/UltraplanChoiceDialog.tsx
  2. 45 0
      src/components/UltraplanLaunchDialog.tsx
  3. 3 0
      src/screens/REPL.tsx

+ 114 - 0
src/components/UltraplanChoiceDialog.tsx

@@ -0,0 +1,114 @@
+import React, { useCallback } from 'react'
+import { Box, Text } from '../ink.js'
+import { Select } from './CustomSelect/index.js'
+import { Dialog } from './design-system/Dialog.js'
+import type { AppState } from '../state/AppStateStore.js'
+import { useSetAppState } from '../state/AppState.js'
+import type { Message } from '../types/message.js'
+import type { RemoteAgentTaskState } from '../tasks/RemoteAgentTask/RemoteAgentTask.js'
+import type { FileStateCache } from '../utils/fileStateCache.js'
+import {
+  createUserMessage,
+  createSystemMessage,
+  prepareUserContent,
+} from '../utils/messages.js'
+import { updateTaskState } from '../utils/task/framework.js'
+import { archiveRemoteSession } from '../utils/teleport.js'
+import { logForDebugging } from '../utils/debug.js'
+
+type UltraplanChoice = 'execute' | 'dismiss'
+
+type Props = {
+  plan: string
+  sessionId: string
+  taskId: string
+  setMessages: React.Dispatch<React.SetStateAction<Message[]>>
+  readFileState: FileStateCache
+  getAppState: () => AppState
+  setConversationId: (id: string) => void
+}
+
+export function UltraplanChoiceDialog({
+  plan,
+  sessionId,
+  taskId,
+  setMessages,
+}: Props): React.ReactNode {
+  const setAppState = useSetAppState()
+
+  const handleChoice = useCallback(
+    (choice: UltraplanChoice) => {
+      if (choice === 'execute') {
+        setMessages(prev => [
+          ...prev,
+          createSystemMessage(
+            'Ultraplan approved. Executing the following plan:',
+            'info',
+          ),
+          createUserMessage({
+            content: prepareUserContent({ inputString: plan }),
+          }),
+        ])
+      }
+
+      // Mark task completed
+      updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t =>
+        t.status !== 'running'
+          ? t
+          : { ...t, status: 'completed', endTime: Date.now() },
+      )
+
+      // Clear ultraplan state
+      setAppState(prev => ({
+        ...prev,
+        ultraplanPendingChoice: undefined,
+        ultraplanSessionUrl: undefined,
+      }))
+
+      // Archive the remote session
+      void archiveRemoteSession(sessionId).catch(e =>
+        logForDebugging(`ultraplan choice archive failed: ${String(e)}`),
+      )
+    },
+    [plan, sessionId, taskId, setMessages, setAppState],
+  )
+
+  const displayPlan =
+    plan.length > 2000 ? plan.slice(0, 2000) + '\n\n... (truncated)' : plan
+
+  return (
+    <Dialog
+      title="Ultraplan ready"
+      onCancel={() => handleChoice('dismiss')}
+    >
+      <Box flexDirection="column" gap={1}>
+        <Box
+          flexDirection="column"
+          borderStyle="single"
+          borderColor="gray"
+          paddingX={1}
+          height={Math.min(displayPlan.split('\n').length + 2, 20)}
+          overflow="hidden"
+        >
+          <Text>{displayPlan}</Text>
+        </Box>
+      </Box>
+      <Select
+        options={[
+          {
+            value: 'execute' as const,
+            label: 'Execute plan here',
+            description:
+              'Send the plan to Claude for execution in this session',
+          },
+          {
+            value: 'dismiss' as const,
+            label: 'Dismiss',
+            description: 'Discard the plan',
+          },
+        ]}
+        onChange={(value: UltraplanChoice) => handleChoice(value)}
+      />
+    </Dialog>
+  )
+}

+ 45 - 0
src/components/UltraplanLaunchDialog.tsx

@@ -0,0 +1,45 @@
+import React from 'react'
+import { Box, Text } from '../ink.js'
+import { Select } from './CustomSelect/index.js'
+import { Dialog } from './design-system/Dialog.js'
+import { CCR_TERMS_URL } from '../commands/ultraplan.js'
+
+type UltraplanLaunchChoice = 'launch' | 'cancel'
+
+type Props = {
+  onChoice: (
+    choice: UltraplanLaunchChoice,
+    opts?: { disconnectedBridge?: boolean },
+  ) => void
+}
+
+export function UltraplanLaunchDialog({ onChoice }: Props): React.ReactNode {
+  return (
+    <Dialog
+      title="Launch ultraplan?"
+      onCancel={() => onChoice('cancel')}
+    >
+      <Box flexDirection="column" gap={1}>
+        <Text>
+          This will start a remote Claude Code session on the web to draft an
+          advanced plan using Opus. The plan typically takes 10–30 minutes.
+          Your terminal stays free while it works.
+        </Text>
+        <Text dimColor>Terms: {CCR_TERMS_URL}</Text>
+      </Box>
+      <Select
+        options={[
+          {
+            value: 'launch' as const,
+            label: 'Launch ultraplan',
+          },
+          {
+            value: 'cancel' as const,
+            label: 'Cancel',
+          },
+        ]}
+        onChange={(value: UltraplanLaunchChoice) => onChoice(value)}
+      />
+    </Dialog>
+  )
+}

+ 3 - 0
src/screens/REPL.tsx

@@ -270,6 +270,9 @@ import type { HookProgress } from '../types/hooks.js';
 import { TungstenLiveMonitor } from '../tools/TungstenTool/TungstenLiveMonitor.js';
 /* eslint-disable @typescript-eslint/no-require-imports */
 const WebBrowserPanelModule = feature('WEB_BROWSER_TOOL') ? require('../tools/WebBrowserTool/WebBrowserPanel.js') as typeof import('../tools/WebBrowserTool/WebBrowserPanel.js') : null;
+const UltraplanLaunchDialog: typeof import('../components/UltraplanLaunchDialog.js').UltraplanLaunchDialog = feature('ULTRAPLAN') ? require('../components/UltraplanLaunchDialog.js').UltraplanLaunchDialog : () => null;
+const UltraplanChoiceDialog: typeof import('../components/UltraplanChoiceDialog.js').UltraplanChoiceDialog = feature('ULTRAPLAN') ? require('../components/UltraplanChoiceDialog.js').UltraplanChoiceDialog : () => null;
+const launchUltraplan: typeof import('../commands/ultraplan.js').launchUltraplan = feature('ULTRAPLAN') ? require('../commands/ultraplan.js').launchUltraplan : () => Promise.resolve('');
 /* eslint-enable @typescript-eslint/no-require-imports */
 import { IssueFlagBanner } from '../components/PromptInput/IssueFlagBanner.js';
 import { useIssueFlagBanner } from '../hooks/useIssueFlagBanner.js';