mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-16 20:40:01 +00:00
* feat(shared): steer capability gates and live steered signal schemas - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession gate which agents can deliver queued messages into the active turn (pi, codex, cursor ACP; legacy stream-json cursor excluded) - AgentState.steeringActive, DecryptedMessage.steered and messages-consumed live signal (never persisted by the hub) * feat(cli): queue reservations and steered messages-consumed option - MessageQueue2 gains takeByLocalId/restoreReservation/ beginReservationDispatch/commitReservation so an async steer can reserve a queued row without racing the main loop's turn/start drain - emitMessagesConsumed accepts steered: true to mark mid-turn delivery * feat(codex): mid-turn steer via app-server turn/steer (#888) - CodexAppServerClient.steerTurn + TurnSteerParams/Response types - CodexRemoteLauncher registers the steer-queued-message RPC handler: reserves the queued row, validates it against the active turn (no control commands, matching mode hash), injects via turn/steer with an epoch guard that invalidates in-flight steers on abort/cleanup - steeringActive agent state tracks the active-turn window - hub syncEngine gate opens to codex; messages-consumed relays steered * feat(web): Steered badge and steer gating for codex sessions - HappyUserMessage shows a ↳ Steered badge fed by the live messages-consumed steered signal, preserved across server echoes and refetches (mergeMessages carries the optimistic marker) - SessionChat gates canSteer via isSteeringSupportedForSession instead of the pi-only check - clearStaleQueuedStatus normalizes a queued status on an invoked message - fix(web): drop duplicate showSessionSummaryInChat in markdown test (upstream typecheck breakage) * feat(acp): split request dispatch from completion and add soft steer - AcpStdioTransport.sendRequestWithDispatch separates stdin-accepted dispatch from the JSON-RPC response, keeping sendRequest behavior unchanged - AcpSdkBackend tracks concurrent session/prompt requests with an activePromptRequests counter (main prompt + soft steers); response completion stays pending until every concurrent prompt settles - beginSoftSteerPrompt kicks off a concurrent session/prompt (Cursor GUI Send semantics — no cancel, no handler swap) returning {dispatched, completed}; softSteerPrompt awaits the full response for direct callers * feat(cursor): mid-turn soft steer via concurrent session/prompt (#888) - CursorAcpRemoteLauncher registers the steer-queued-message RPC handler: reserves the queued row, rejects control commands and mode mismatches, then soft-injects via beginSoftSteerPrompt without canceling the in-flight turn - Acks the hub once stdin accepts the inject (not on turn completion) to stay inside the 30s RPC window; the launcher stays busy until the concurrent prompt settles so handlers are not swapped mid-inject - steeringActive agent state mirrors the active-turn window; abort and cleanup reset it and invalidate pending steers - Legacy stream-json Cursor sessions register a steer handler that reports unsupported * fix(codex,shared): address bot findings on steer gate and ambiguous turn/steer - STEERING_SUPPORTED_FLAVORS / isSteeringSupportedForSession advertise codex and pi only; cursor joins when its soft-steer handler lands (#1609) - turn/steer now splits dispatch (stdin accepted) from completion (turn finished): the hub RPC acks once dispatch succeeds — never on the concurrent turn's completion, which can exceed the 30s RPC window - queue row commits only after the turn settles; a rejected/aborted steer restores the row so the message still delivers via turn/start, and a dispatched steer is never restored (no duplicate delivery) - steer carries clientUserMessageId (echoed as userMessage.clientId) so ambiguous transport failures can reconcile the thread later - client tests cover dispatch/complete split and stdin-write failure * feat(shared): advertise cursor in the steer gate now that its handler lands Cursor ACP sessions pass the web and hub steer gates; legacy stream-json cursor sessions stay excluded. * fix(codex): reconcile dispatched steers before restoring; align error copy - A dispatched turn/steer whose completion fails (disconnect / protocol error) is now reconciled via thread/read by clientUserMessageId before the queued row is restored — the instruction is only re-delivered by turn/start when the thread never received it - Reconcile targets the pinned steer thread, not whichever turn is current when completion fails - syncEngine unsupported-flavor error now matches the capability gate (Pi and Codex only until the cursor handler lands) - launcher tests cover steer success (ack on dispatch), reconcile-accepted and reconcile-rejected outcomes * fix(codex): consume the row at dispatch; drop background reconcile - The hub RPC acks and the queue row is consumed as soon as stdin accepts turn/steer; completion is background-only logging. A dispatched steer is never restored, so the same localId cannot be re-delivered via turn/start after the caller was told the steer succeeded - Dispatch failure (stdin write error) still restores the row and reports failure - steer.completed rejection is always handled (no unhandled rejection on the dispatch-failure path) - tests updated: completion failure after dispatch keeps the row consumed; dispatch failure restores it * fix(cursor): consume the row at dispatch; keep waiters for prompt gating - The hub RPC acks and the queue row is consumed as soon as stdin accepts the concurrent session/prompt; completion is background-only. A dispatched steer is never restored (no duplicate via the next prompt) - softSteerWaiters are registered before awaiting dispatch so the main loop's finally cannot start the next prompt mid-inject; they still gate prompt handover on completion - tests updated: post-dispatch ACP rejection keeps the row consumed * fix(cursor,hub): never hang teardown on unresolved soft steer; align diagnostics - Prompt-finally waits for soft-steer completion only when not exiting; the outer finally no longer waits at all — cleanup() disconnects the ACP transport, which rejects pending requests and settles the waiters - syncEngine gate diagnostics and JSDoc name all supported flavors (Pi, Codex, Cursor ACP) - regression test: Switch with an unresolved soft-steer completion still reaches teardown * fix(codex): distinguish definite rejection from indeterminate completion - Transport-level failures (timeout, abort, disconnect, spawn, protocol) carry an indeterminate marker; explicit JSON-RPC error responses do not - After a dispatched steer, turn completion resolves → commit + consumed; a definite app-server rejection restores the row (instruction was never accepted, so turn/start cannot duplicate it); an indeterminate outcome leaves the row reserved so it can never be delivered twice - Completion handling registers before awaiting dispatch so the dispatch-failure path cannot leak an unhandled rejection - client/launcher tests cover explicit rejection (restore), indeterminate outcome (row stays reserved) and dispatch failure * fix(codex): reconcile indeterminate steers instead of a permanent reservation - After an indeterminate completion (disconnect/protocol), reconcile the thread by clientUserMessageId immediately: accepted → commit + consumed, provably rejected → restore, still unreadable → keep the reservation and retry from the main-loop top on later passes (post-reconnect) - A row never sits in dispatching forever: the hub cannot stamp it invoked while the instruction may never have been accepted - tests: indeterminate keeps reserved while thread unreadable; accepted reconciliation consumes; rejected path restores * fix(cursor): abort drops soft-steer waiters so the next prompt never blocks - Ordinary Abort (shouldExit false) now clears softSteerWaiters: the prompt finally cannot wait forever on a soft steer whose completion is unbounded; the ACP cancel rejects in-flight requests, and cleanup() settles leftovers on session end - regression test: unresolved soft-steer completion after Abort no longer blocks the next prompt * fix(codex): accept all thread item shapes; retry reconcile; ack through abort - Reconcile matcher accepts userMessage/user_message with clientId/ client_id, matching the shapes the thread parser supports — an accepted steer can no longer be misclassified as rejected - A pending reconciliation schedules a wakeLoop retry, so a temporary app-server outage cannot strand the reservation behind waitForTurnOrRecovery - The success-path ACK no longer checks the steer epoch: the hub already reported steered on dispatch, so commit + messages-consumed must reach it even when an abort resets the queue in between * fix(cursor,acp): abort force-settles soft-steer bookkeeping - AcpSdkBackend.abortSoftSteers() drops the concurrent-prompt counter and notifies response-complete so the next turn's waitForResponseComplete() cannot block on a soft steer that will never settle after abort - handleAbort calls it before clearing the waiters; the main prompt's own finishPromptRequest stays guarded by Math.max(0, ...) - unit tests cover counter release and no-op when idle * fix(codex): reinit reconnected app-server; keep reconcile retries alive - thread/read after a disconnect auto-connects a fresh app-server, which must be initialized before any request — reconcile now ensures connect + initialize (isConnected getter added to the client) - every still-unknown loop-top reconciliation schedules the next retry, so recovery without external traffic is eventually observed - launcher mock gains isConnected * test(acp): match finishPromptRequest epoch signature in whitebox test * fix(codex): timer-driven reconciliation; init tracking; abort-safe ACK - Reconciliation runs on a self-rescheduling 1s timer independent of the main loop (wakes it too), so idle loops and waitForTurnOrRecovery still observe app-server recovery; abort clears nothing implicitly — the ACK path commits and consumes even when the reservation was cancelled - Absence of a durable client id is ambiguous: unmatched reads stay 'unknown' and keep retrying instead of restoring the row - CodexAppServerClient tracks initialized state (reset on disconnect/exit) so ensureAppServerInitialized re-initializes a fresh process before thread/read; initialize failures leave the flag false for the next retry - tests: accepted reconciliation via scheduled timer, indeterminate keeps reserved, explicit rejection restores * fix(codex): bind reconciliation to the launcher lifecycle - runSteerReconciliation clears any armed retry timer on entry and never installs a second one, so loop-top and timer-driven passes cannot multiply - shuttingDown is set when the main loop ends: timers are cleared and the pending map is dropped, so an unresolved steer can never respawn an app-server after cleanup (remote-to-local switch included) * fix(cursor): abort releases an in-progress soft-steer wait - The prompt-finally wait races Promise.allSettled against the abort signal: an Abort that clears the waiters now also releases a wait that already started, so the launcher always reaches the next queued prompt * fix(codex): report steered only after app-server acceptance - The handler now awaits steer.completed (the inject-acceptance response): an explicit JSON-RPC rejection surfaces as failed and restores the row for the normal turn/start path instead of a false steered - Transport failure after dispatch reports 'Steer outcome is being reconciled' and keeps the row reserved while the timer-driven thread reconciliation runs - dispatch-failure path also swallows the paired completion rejection * fix(cursor,acp): commit on ACP acceptance; distinguish transport failures - AcpStdioTransport marks transport-level failures (timeout, closed, stdin write) as indeterminate; explicit JSON-RPC error responses are not - The steer handler commits + consumes on completion (ACP acceptance) and restores the row on an explicit rejection; an indeterminate transport failure keeps the row reserved so a delivered instruction is never re-sent, and the ACK reaches the hub even when abort reset the queue - launcher/transport tests updated for the three outcomes * fix(steer): tri-state cancel, clear-safe reservations, bounded acceptance wait - MessageQueue2.cancelByLocalId returns 'in-flight' for a dispatching steer reservation: the hub neither deletes the row nor stamps invoked_at (new CancelMessageResponse 'busy' status; web restores the optimistic row); pushIsolateAndClear and reset/close share cancelReservations so /clear-style commands cannot have a rejected steer resurrect a discarded prompt - turn/steer acceptance wait bounded at 25s (< hub 30s RPC timeout): a lost response is indeterminate and funnels into thread reconciliation instead of stranding the reservation - tests updated for the tri-state cancel contract * test(cursor): match tri-state cancel contract for dispatching steers * fix(cursor): drop duplicate promptInFlight declaration after upstream merge * fix(codex,web): busy-aware edit flow; bound reconciliation reads - QueuedMessagesBar edit flow treats a 'busy' cancel as unsuccessful: it never prefills the composer when the row is inside an async steer, so a second client cannot send a duplicate - reconcileSteerByClientId bounds thread/read with a 5s timeout so a connected-but-silent app-server cannot hold the reservation in-flight indefinitely * fix(steer): inFlight-dominated cancel acks; bounded reconciliation - hub cancel-queued-message acks check inFlight before removed: a stale duplicate socket reporting removed can no longer delete the durable row while another socket is dispatching the steer - reconciliation entries expire after 60s and mark delivered: after the rejection window, a dispatched steer that the app-server never proved (client ids dropped on restart) is committed instead of polling thread/read forever - pre-dispatch failures (abort before write included) never enter reconciliation — they restore the row and report failure * fix(cursor,acp,web): indeterminate close marks, steer gating precision - AcpStdioTransport.rejectAllPending marks close/protocol failures indeterminate, so an accepted-but-close-interrupted soft steer restores nothing (no duplicate delivery) - the abort race in the soft-steer wait removes its listener in finally (no accumulation across repeated waits) - SessionChat gates the Steer button on agentState.steeringActive for codex/cursor instead of the queued-grace thinking flag, so Steer is not exposed before the launcher can accept it - codex pre-dispatch abort never enters reconciliation (merged from #1606) * fix(steer): persist indeterminate outcomes without replay * fix(cursor): hold ambiguous steers for explicit resolution * fix(steer): make ambiguous delivery restart-safe * fix(cursor): make ambiguous delivery restart-safe * fix(steer): recover crash-held rows and preserve retry dedup * fix(steer): ack retries and bound stdin dispatch * fix(cursor): reject steers when prompt generation changes * fix(steer): reconcile indeterminate dispatches and serialize retries * fix(cursor): preserve soft-steer reservations across abort * fix(codex): classify stdin callback failures as indeterminate * fix(steer): recheck indeterminate cancels after ACK * fix(steer): close retry and abort races * fix(cursor): hold ambiguous dispatch failures * fix(steer): serialize live retries and abort admission * fix(steer): distinguish live dispatching from unknown * fix(cursor): bound ACP dispatch acknowledgements * fix(steer): keep ACK failures held and reconcile busy cancel * fix(cursor): preserve state when dispatch ACK is uncertain * fix(steer): distinguish held cancel from removal * fix(store): combine schema v24 migrations * fix(cursor): distinguish held cancel from removal * fix(store): reserve schema v25 for steer delivery state * fix(cursor): suppress late ACP updates after abort * fix(steer): keep held cancel state and notify requeue * fix(cursor): isolate late updates after abort * fix(steer): release explicitly cancelled unknown reservations * test(cursor): cover explicit held cancellation * fix(codex): reject cancelled reservations before native steer * fix(cursor): reject cancelled reservations before ACP steer * fix(codex): make reservation restore atomic with state * fix(cursor): make reservation restore atomic with state * fix(codex): terminate abandoned transport writes * fix(cursor): hard-stop abandoned writes and update native queue state * fix(steer): own abandoned app-server lifecycle and consume races * fix(cursor): isolate aborts and add native retry resolution * fix(codex): confirm dispatch and recover abandoned turns * test(codex): mock abandoned transport callback * fix(native): reconcile retry responses * fix(codex): clear visible turn state on transport loss * fix(steer): claim retries and cover native delivery state * fix(native): resync busy cancel outcomes * fix(native): preserve indeterminate state on Android hydration * fix(steer): make retry claims single-winner * fix(cursor): hold restore failures for explicit resolution * fix(steer): serialize concurrent retry claims * fix(socket): tolerate missing steer-state ACK callbacks * fix(native): serialize retry operations * docs(web): document unknown steer delivery and retry controls * fix(steer): handle retry failures and abort-before-connect * fix(cursor): drain foreground prompt after soft-steer abort * fix(steer): reinitialize after transport loss and finish iOS retry errors * fix(steer): preserve indeterminate rows across reconnect gaps * test(web): mock indeterminate queued recovery state * fix(steer): recover consumed ACK tombstones * fix(steer): expose consumed cancel tombstones * fix(cursor): drain soft steers before handler replacement * fix(cursor): preserve buffered output on abort
230 lines
7.8 KiB
TypeScript
230 lines
7.8 KiB
TypeScript
import { z } from 'zod'
|
|
|
|
/**
|
|
* @description The legacy payload type identifier used for all generic agent messages.
|
|
* Changing this value will affect the communication schema between CLI, Hub, and Web.
|
|
* A migration plan is required if this literal is ever modified.
|
|
*/
|
|
export const AGENT_MESSAGE_PAYLOAD_TYPE = 'codex' as const
|
|
|
|
export const AGENT_FLAVORS = ['agy', 'claude', 'codex', 'copilot', 'cursor', 'gemini', 'grok', 'kimi', 'opencode', 'pi'] as const
|
|
export type AgentFlavor = typeof AGENT_FLAVORS[number]
|
|
export const AgentFlavorSchema = z.enum(AGENT_FLAVORS)
|
|
|
|
// Flavors offered when CREATING a new session. Gemini CLI is intentionally
|
|
// excluded: Google sunset the consumer Gemini CLI (2026-06-18) so it can no
|
|
// longer be launched. It is kept in AGENT_FLAVORS / AgentFlavorSchema above so
|
|
// existing stored Gemini sessions still validate and remain viewable.
|
|
export const CREATABLE_AGENT_FLAVORS: readonly AgentFlavor[] = AGENT_FLAVORS.filter(
|
|
(flavor) => flavor !== 'gemini'
|
|
)
|
|
|
|
export const AGY_PERMISSION_MODES = ['request-review', 'always-proceed'] as const
|
|
export type AgyPermissionMode = typeof AGY_PERMISSION_MODES[number]
|
|
|
|
export const CLAUDE_PERMISSION_MODES = ['default', 'acceptEdits', 'auto', 'bypassPermissions', 'plan'] as const
|
|
export type ClaudePermissionMode = typeof CLAUDE_PERMISSION_MODES[number]
|
|
|
|
export const CODEX_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
|
|
export type CodexPermissionMode = typeof CODEX_PERMISSION_MODES[number]
|
|
|
|
export const CODEX_COLLABORATION_MODES = ['default', 'plan'] as const
|
|
export type CodexCollaborationMode = typeof CODEX_COLLABORATION_MODES[number]
|
|
|
|
export const GEMINI_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
|
|
export type GeminiPermissionMode = typeof GEMINI_PERMISSION_MODES[number]
|
|
|
|
export const KIMI_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
|
|
export type KimiPermissionMode = typeof KIMI_PERMISSION_MODES[number]
|
|
|
|
export const COPILOT_PERMISSION_MODES = ['default', 'read-only', 'safe-yolo', 'yolo'] as const
|
|
export type CopilotPermissionMode = typeof COPILOT_PERMISSION_MODES[number]
|
|
|
|
export const GROK_PERMISSION_MODES = ['default', 'auto', 'plan', 'bypassPermissions'] as const
|
|
export type GrokPermissionMode = typeof GROK_PERMISSION_MODES[number]
|
|
|
|
export const OPENCODE_PERMISSION_MODES = ['default', 'plan', 'yolo'] as const
|
|
export type OpencodePermissionMode = typeof OPENCODE_PERMISSION_MODES[number]
|
|
|
|
export const CURSOR_PERMISSION_MODES = ['default', 'plan', 'ask', 'debug', 'autoReview', 'yolo'] as const
|
|
export type CursorPermissionMode = typeof CURSOR_PERMISSION_MODES[number]
|
|
|
|
export const PERMISSION_MODES = [
|
|
'default',
|
|
'acceptEdits',
|
|
'auto',
|
|
'bypassPermissions',
|
|
'plan',
|
|
'ask',
|
|
'debug',
|
|
'autoReview',
|
|
'read-only',
|
|
'safe-yolo',
|
|
'yolo',
|
|
'request-review',
|
|
'always-proceed'
|
|
] as const
|
|
export type PermissionMode = typeof PERMISSION_MODES[number]
|
|
|
|
|
|
export const PERMISSION_MODE_LABELS: Record<PermissionMode, string> = {
|
|
default: 'Default',
|
|
acceptEdits: 'Accept Edits',
|
|
auto: 'Auto',
|
|
plan: 'Plan Mode',
|
|
ask: 'Ask Mode',
|
|
debug: 'Debug Mode',
|
|
autoReview: 'Auto-review',
|
|
bypassPermissions: 'Yolo',
|
|
'read-only': 'Read Only',
|
|
'safe-yolo': 'Safe Yolo',
|
|
yolo: 'Yolo',
|
|
'request-review': 'Request Review',
|
|
'always-proceed': 'Always Proceed'
|
|
}
|
|
|
|
export type PermissionModeTone = 'neutral' | 'info' | 'warning' | 'danger'
|
|
|
|
export const PERMISSION_MODE_TONES: Record<PermissionMode, PermissionModeTone> = {
|
|
default: 'neutral',
|
|
acceptEdits: 'warning',
|
|
auto: 'warning',
|
|
plan: 'info',
|
|
ask: 'info',
|
|
debug: 'info',
|
|
autoReview: 'warning',
|
|
bypassPermissions: 'danger',
|
|
'read-only': 'warning',
|
|
'safe-yolo': 'warning',
|
|
yolo: 'danger',
|
|
'request-review': 'neutral',
|
|
'always-proceed': 'danger'
|
|
}
|
|
|
|
export type PermissionModeOption = {
|
|
mode: PermissionMode
|
|
label: string
|
|
tone: PermissionModeTone
|
|
}
|
|
|
|
export type CodexCollaborationModeOption = {
|
|
mode: CodexCollaborationMode
|
|
label: string
|
|
}
|
|
|
|
export const CODEX_COLLABORATION_MODE_LABELS: Record<CodexCollaborationMode, string> = {
|
|
default: 'Default',
|
|
plan: 'Plan'
|
|
}
|
|
|
|
export function getPermissionModeLabel(mode: PermissionMode): string {
|
|
return PERMISSION_MODE_LABELS[mode]
|
|
}
|
|
|
|
export function getPermissionModeTone(mode: PermissionMode): PermissionModeTone {
|
|
return PERMISSION_MODE_TONES[mode]
|
|
}
|
|
|
|
export function getCodexCollaborationModeLabel(mode: CodexCollaborationMode): string {
|
|
return CODEX_COLLABORATION_MODE_LABELS[mode]
|
|
}
|
|
|
|
export function getPermissionModesForFlavor(flavor?: string | null): readonly PermissionMode[] {
|
|
if (flavor === 'codex') {
|
|
return CODEX_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'gemini') {
|
|
return GEMINI_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'kimi') {
|
|
return KIMI_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'copilot') {
|
|
return COPILOT_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'grok') {
|
|
return GROK_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'opencode') {
|
|
return OPENCODE_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'agy') {
|
|
return AGY_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'cursor') {
|
|
return CURSOR_PERMISSION_MODES
|
|
}
|
|
if (flavor === 'pi') {
|
|
// Pi RPC mode has no runtime permission switching (always auto-approve);
|
|
// no permission modes are offered.
|
|
return []
|
|
}
|
|
return CLAUDE_PERMISSION_MODES
|
|
}
|
|
|
|
export function getPermissionModeOptionsForFlavor(flavor?: string | null): PermissionModeOption[] {
|
|
return getPermissionModesForFlavor(flavor).map((mode) => ({
|
|
mode,
|
|
label: getPermissionModeLabel(mode),
|
|
tone: getPermissionModeTone(mode)
|
|
}))
|
|
}
|
|
|
|
export function isPermissionModeAllowedForFlavor(mode: PermissionMode, flavor?: string | null): boolean {
|
|
return getPermissionModesForFlavor(flavor).includes(mode)
|
|
}
|
|
|
|
export function getCodexCollaborationModeOptions(): CodexCollaborationModeOption[] {
|
|
return CODEX_COLLABORATION_MODES.map((mode) => ({
|
|
mode,
|
|
label: getCodexCollaborationModeLabel(mode)
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* Flavors that can deliver a queued message into an active turn on demand
|
|
* (per-message "Steer" from the waiting queue), without waiting for full-turn end.
|
|
*
|
|
* Steer = soft mid-turn delivery (same idea as Cursor GUI default "Send"):
|
|
* - Pi: native steer over the Pi runtime (first-class since #1466)
|
|
* - Codex: app-server `turn/steer` (true mid-turn inject)
|
|
* - Cursor ACP: concurrent `session/prompt` soft-send (no cancel). Legacy
|
|
* stream-json Cursor sessions are NOT steerable — gate with
|
|
* {@link isSteeringSupportedForSession}.
|
|
*
|
|
* Claude / others: not supported (no reachable soft-steer path) — UI hides Steer.
|
|
*/
|
|
export const STEERING_SUPPORTED_FLAVORS = ['codex', 'cursor', 'pi'] as const
|
|
|
|
export function isSteeringSupportedForFlavor(flavor?: string | null): boolean {
|
|
return (STEERING_SUPPORTED_FLAVORS as readonly string[]).includes(flavor ?? '')
|
|
}
|
|
|
|
/**
|
|
* Session-aware steer gate. Prefer this over {@link isSteeringSupportedForFlavor}
|
|
* when metadata is available: legacy Cursor stream-json sessions advertise
|
|
* flavor `cursor` but cannot steer.
|
|
*
|
|
* Matches CLI legacy detection: explicit `stream-json`, or a pre-ACP session
|
|
* that has `cursorSessionId` without `cursorSessionProtocol: 'acp'`.
|
|
*/
|
|
export function isSteeringSupportedForSession(metadata?: {
|
|
flavor?: string | null
|
|
cursorSessionId?: string | null
|
|
cursorSessionProtocol?: 'acp' | 'stream-json' | null
|
|
} | null): boolean {
|
|
if (metadata?.flavor === 'codex' || metadata?.flavor === 'pi') {
|
|
return true
|
|
}
|
|
if (metadata?.flavor !== 'cursor') {
|
|
return false
|
|
}
|
|
if (metadata.cursorSessionProtocol === 'stream-json') {
|
|
return false
|
|
}
|
|
if (!metadata.cursorSessionProtocol && metadata.cursorSessionId) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|