feat(remote-control): 优化 Web 展示、状态同步与桥接控制流程 (#288)

Co-authored-by: chengzifeng <chengzifeng@meituan.com>
This commit is contained in:
Cheng Zi Feng
2026-04-17 16:21:27 +08:00
committed by GitHub
parent b5c299f5d2
commit 72a2093cd6
64 changed files with 4138 additions and 312 deletions

View File

@@ -49,6 +49,8 @@ import {
makeResultMessage,
isEligibleBridgeMessage,
extractTitleText,
shouldReportRunningForMessage,
shouldReportRunningForMessages,
BoundedUUIDSet,
} from './bridgeMessaging.js'
import { logBridgeSkip } from './debugUtils.js'
@@ -72,6 +74,7 @@ import type {
import type { StdoutMessage } from '../entrypoints/sdk/controlTypes.js'
import type { SDKResultSuccess } from '../entrypoints/sdk/coreTypes.js'
import type { PermissionMode } from '../utils/permissions/PermissionMode.js'
import { setSessionMetadataChangedListener } from '../utils/sessionState.js'
/**
* StdoutMessage with optional session_id. The transport layer accepts
@@ -321,6 +324,18 @@ export async function initEnvLessBridgeCore(
})
}
// Mirror external metadata updates from the live REPL into the bridge's
// CCR worker channel. Without this, proactive wait/sleep only changes local
// UI state and the web session detail falls back to the generic working
// spinner because automation_state never reaches remote-control.
setSessionMetadataChangedListener(
metadata => {
if (tornDown) return
transport.reportMetadata(metadata)
},
{ replayCurrent: true },
)
// ── 5. JWT refresh scheduler ────────────────────────────────────────────
// Schedule a callback 5min before expiry (per response.expires_in). On fire,
// re-fetch /bridge with OAuth → rebuild transport with fresh credentials.
@@ -625,7 +640,7 @@ export async function initEnvLessBridgeCore(
...m,
session_id: sessionId,
})) as TransportMessage[]
if (msgs.some(m => m.type === 'user')) {
if (shouldReportRunningForMessages(msgs)) {
transport.reportState('running')
}
logForDebugging(
@@ -655,13 +670,13 @@ export async function initEnvLessBridgeCore(
})) as TransportMessage[]
if (events.length === 0) return
// Mid-turn init: if Remote Control is enabled while a query is running,
// the last eligible message is a user prompt or tool_result (both 'user'
// type). Without this the init PUT's 'idle' sticks until the next user-
// type message forwards via writeMessages — which for a pure-text turn
// is never (only assistant chunks stream post-init). Check eligible (pre-
// cap), not capped: the cap may truncate to a user message even when the
// actual trailing message is assistant.
if (eligible.at(-1)?.type === 'user') {
// the last eligible message may be a real user prompt or tool_result.
// Hidden slash-command scaffolding and pure reminder wrappers should not
// resurrect a completed turn into "running". Check eligible (pre-cap),
// not capped: the cap may truncate to a user message even when the actual
// trailing message is assistant.
const lastEligible = eligible.at(-1)
if (lastEligible && shouldReportRunningForMessage(lastEligible)) {
transport.reportState('running')
}
logForDebugging(`[remote-bridge] Flushing ${events.length} history events`)
@@ -817,10 +832,11 @@ export async function initEnvLessBridgeCore(
})) as TransportMessage[]
// v2 does not derive worker_status from events server-side (unlike v1
// session-ingress session_status_updater.go). Push it from here so the
// CCR web session list shows Running instead of stuck on Idle. A user
// message in the batch marks turn start. CCRClient.reportState dedupes
// consecutive same-state pushes.
if (filtered.some(m => m.type === 'user')) {
// CCR web session list shows Running instead of stuck on Idle. Only
// work-starting user messages mark turn start; hidden local-command
// scaffolding and pure reminders should not re-open a completed turn.
// CCRClient.reportState dedupes consecutive same-state pushes.
if (shouldReportRunningForMessages(filtered)) {
transport.reportState('running')
}
logForDebugging(`[remote-bridge] Sending ${filtered.length} message(s)`)