(taskId: string, sessionId: string, url: string, getAppState: () => AppState, setAppState: (f: (prev: AppState) => AppState) => void)
| 72 | return parts.join('\n'); |
| 73 | } |
| 74 | function startDetachedPoll(taskId: string, sessionId: string, url: string, getAppState: () => AppState, setAppState: (f: (prev: AppState) => AppState) => void): void { |
| 75 | const started = Date.now(); |
| 76 | let failed = false; |
| 77 | void (async () => { |
| 78 | try { |
| 79 | const { |
| 80 | plan, |
| 81 | rejectCount, |
| 82 | executionTarget |
| 83 | } = await pollForApprovedExitPlanMode(sessionId, ULTRAPLAN_TIMEOUT_MS, phase => { |
| 84 | if (phase === 'needs_input') logEvent('tengu_ultraplan_awaiting_input', {}); |
| 85 | updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => { |
| 86 | if (t.status !== 'running') return t; |
| 87 | const next = phase === 'running' ? undefined : phase; |
| 88 | return t.ultraplanPhase === next ? t : { |
| 89 | ...t, |
| 90 | ultraplanPhase: next |
| 91 | }; |
| 92 | }); |
| 93 | }, () => getAppState().tasks?.[taskId]?.status !== 'running'); |
| 94 | logEvent('tengu_ultraplan_approved', { |
| 95 | duration_ms: Date.now() - started, |
| 96 | plan_length: plan.length, |
| 97 | reject_count: rejectCount, |
| 98 | execution_target: executionTarget as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS |
| 99 | }); |
| 100 | if (executionTarget === 'remote') { |
| 101 | // User chose "execute in CCR" in the browser PlanModal — the remote |
| 102 | // session is now coding. Skip archive (ARCHIVE has no running-check, |
| 103 | // would kill mid-execution) and skip the choice dialog (already chose). |
| 104 | // Guard on task status so a poll that resolves after stopUltraplan |
| 105 | // doesn't notify for a killed session. |
| 106 | const task = getAppState().tasks?.[taskId]; |
| 107 | if (task?.status !== 'running') return; |
| 108 | updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => t.status !== 'running' ? t : { |
| 109 | ...t, |
| 110 | status: 'completed', |
| 111 | endTime: Date.now() |
| 112 | }); |
| 113 | setAppState(prev => prev.ultraplanSessionUrl === url ? { |
| 114 | ...prev, |
| 115 | ultraplanSessionUrl: undefined |
| 116 | } : prev); |
| 117 | enqueuePendingNotification({ |
| 118 | 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'), |
| 119 | mode: 'task-notification' |
| 120 | }); |
| 121 | } else { |
| 122 | // Teleport: set pendingChoice so REPL mounts UltraplanChoiceDialog. |
| 123 | // The dialog owns archive + URL clear on choice. Guard on task status |
| 124 | // so a poll that resolves after stopUltraplan doesn't resurrect the |
| 125 | // dialog for a killed session. |
| 126 | setAppState(prev => { |
| 127 | const task = prev.tasks?.[taskId]; |
| 128 | if (!task || task.status !== 'running') return prev; |
| 129 | return { |
| 130 | ...prev, |
| 131 | ultraplanPendingChoice: { |
no test coverage detected