@@ -103,6 +103,9 @@ import {
103103} from "./workspace-view-persistence" ;
104104import { createWorkspaceSessionActions } from "./session-actions" ;
105105import { useWorkspaceArtifactsSync } from "./workspace-sync-hooks" ;
106+ import {
107+ subscribeWorkspaceInputError ,
108+ } from "../../command" ;
106109import { startWorkspaceLaunch } from "./workspace-launch-actions" ;
107110import {
108111 browseWorkspaceOverlayDirectory ,
@@ -129,9 +132,9 @@ import {
129132 withWorkspaceFileSearchDropdownStyle
130133} from "./file-search-actions" ;
131134import { startSessionRuntime } from "../../services/http/session-runtime.service.ts" ;
135+ import { writeTerminal as writeTerminalRequest } from "../../services/http/terminal.service" ;
132136import {
133137 consumeTerminalChannelInputFragment ,
134- sendTerminalChannelInput ,
135138} from "../../services/terminal-channel/client.ts" ;
136139import { withFallback } from "../../services/http/client" ;
137140import {
@@ -224,6 +227,12 @@ import type {
224227
225228const withServiceFallback = async < T , > ( operation : ( ) => Promise < T > , fallback : T ) : Promise < T > => withFallback ( operation , fallback ) ;
226229
230+ const STALE_SESSION_INPUT_ERRORS = new Set ( [
231+ "terminal_runtime_not_found" ,
232+ "session_runtime_unbound" ,
233+ "terminal_stdin_closed" ,
234+ ] ) ;
235+
227236type WorkspaceScreenProps = {
228237 locale : Locale ;
229238 appSettings : AppSettings ;
@@ -700,30 +709,40 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
700709 }
701710
702711 if ( uiState && runtimeSnapshot ) {
703- updateState ( ( current ) => applyWorkspaceBootstrapResult (
704- current ,
705- bootstrap ,
706- locale ,
707- appSettings ,
708- {
709- deviceId,
710- clientId,
711- uiState,
712- runtimeSnapshot,
713- } ,
714- ) ) ;
712+ updateState ( ( current ) => {
713+ const next = applyWorkspaceBootstrapResult (
714+ current ,
715+ bootstrap ,
716+ locale ,
717+ appSettings ,
718+ {
719+ deviceId,
720+ clientId,
721+ uiState,
722+ runtimeSnapshot,
723+ } ,
724+ ) ;
725+ return next . tabs . some ( ( tab ) => tab . id === routeWorkspaceId )
726+ ? { ...next , activeTabId : routeWorkspaceId }
727+ : next ;
728+ } ) ;
715729 } else {
716- updateState ( ( current ) => applyWorkspaceBootstrapResult (
717- current ,
718- bootstrap ,
719- locale ,
720- appSettings ,
721- {
722- deviceId,
723- clientId,
724- uiState,
725- } ,
726- ) ) ;
730+ updateState ( ( current ) => {
731+ const next = applyWorkspaceBootstrapResult (
732+ current ,
733+ bootstrap ,
734+ locale ,
735+ appSettings ,
736+ {
737+ deviceId,
738+ clientId,
739+ uiState,
740+ } ,
741+ ) ;
742+ return next . tabs . some ( ( tab ) => tab . id === routeWorkspaceId )
743+ ? { ...next , activeTabId : routeWorkspaceId }
744+ : next ;
745+ } ) ;
727746 }
728747 } else {
729748 updateState ( ( current ) => applyWorkspaceBootstrapResult (
@@ -753,7 +772,12 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
753772 switchWorkspaceLocally ( routeWorkspaceId ) ;
754773 void withServiceFallback ( ( ) => activateWorkspaceRequest ( routeWorkspaceId , deviceId , clientId ) , null ) . then ( ( uiState ) => {
755774 if ( ! uiState || cancelled || ! isWorkspaceSyncVersionCurrent ( routeWorkspaceId , syncVersion ) ) return ;
756- updateState ( ( current ) => applyWorkbenchUiState ( current , uiState ) ) ;
775+ updateState ( ( current ) => {
776+ const next = applyWorkbenchUiState ( current , uiState ) ;
777+ return next . tabs . some ( ( tab ) => tab . id === routeWorkspaceId )
778+ ? { ...next , activeTabId : routeWorkspaceId }
779+ : next ;
780+ } ) ;
757781 } ) ;
758782 }
759783 void ensureWorkspaceTerminal ( routeWorkspaceId ) ;
@@ -800,15 +824,20 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
800824 navigate ( "/workspace" , { replace : true } ) ;
801825 return ;
802826 }
803- updateState ( ( current ) => applyWorkspaceRuntimeSnapshot (
804- current ,
805- runtimeSnapshot ,
806- locale ,
807- appSettings ,
808- deviceId ,
809- clientId ,
810- uiState ,
811- ) ) ;
827+ updateState ( ( current ) => {
828+ const next = applyWorkspaceRuntimeSnapshot (
829+ current ,
830+ runtimeSnapshot ,
831+ locale ,
832+ appSettings ,
833+ deviceId ,
834+ clientId ,
835+ uiState ,
836+ ) ;
837+ return next . tabs . some ( ( tab ) => tab . id === routeWorkspaceId )
838+ ? { ...next , activeTabId : routeWorkspaceId }
839+ : next ;
840+ } ) ;
812841 void ensureWorkspaceTerminal ( routeWorkspaceId ) ;
813842 } ) ( ) ;
814843 return ( ) => {
@@ -1821,9 +1850,41 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
18211850 return { tab : nextTab , session : nextSession } ;
18221851 } ;
18231852
1853+ const isSessionRuntimeWritable = ( session : Session | null | undefined ) => (
1854+ Boolean ( session ?. terminalRuntimeId ) && session ?. runtimeLiveness === "attached"
1855+ ) ;
1856+
1857+ const refreshWorkspaceRuntimeSnapshot = useCallback ( async ( workspaceId : string ) => {
1858+ const syncVersion = advanceWorkspaceSyncVersion ( workspaceId ) ;
1859+ const runtimeSnapshot = await attachWorkspaceRuntimeWithRetry (
1860+ workspaceId ,
1861+ deviceId ,
1862+ clientId ,
1863+ withServiceFallback ,
1864+ {
1865+ force : true ,
1866+ successReuseMs : 0 ,
1867+ } ,
1868+ ) ;
1869+ if ( runtimeSnapshot && isWorkspaceSyncVersionCurrent ( workspaceId , syncVersion ) ) {
1870+ updateState ( ( current ) => applyWorkspaceRuntimeSnapshot (
1871+ current ,
1872+ runtimeSnapshot ,
1873+ locale ,
1874+ appSettings ,
1875+ deviceId ,
1876+ clientId ,
1877+ ) ) ;
1878+ }
1879+ if ( ! isWorkspaceSyncVersionCurrent ( workspaceId , syncVersion ) ) return null ;
1880+ return stateRef . current . tabs . find ( ( tab ) => tab . id === workspaceId ) ?? null ;
1881+ } , [ appSettings , clientId , deviceId , locale , updateState ] ) ;
1882+
18241883 const sendAgentRawChunk = async ( tab : Tab , session : Session , input : string ) => {
18251884 if ( ! guardWorkspaceMutation ( "agent_input" , tab . id , session . id ) ) return false ;
1826- if ( ! session . terminalRuntimeId ) return false ;
1885+ if ( ! session . terminalRuntimeId || session . runtimeLiveness !== "attached" || ! session . terminalId ) return false ;
1886+ const numericTerminalId = Number ( session . terminalId . replace ( "term-" , "" ) ) ;
1887+ if ( ! Number . isFinite ( numericTerminalId ) ) return false ;
18271888 const lastActiveAt = Date . now ( ) ;
18281889 updateTab ( tab . id , ( current ) => ( {
18291890 ...current ,
@@ -1832,8 +1893,12 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
18321893 )
18331894 } ) ) ;
18341895 void syncSessionPatch ( tab . id , session . id , { last_active_at : lastActiveAt } ) ;
1835- sendTerminalChannelInput ( tab . id , tab . controller . deviceId , tab . controller . clientId , tab . controller . fencingToken , session . terminalRuntimeId , input ) ;
1836- return true ;
1896+ try {
1897+ await writeTerminalRequest ( tab . id , tab . controller , numericTerminalId , input ) ;
1898+ return true ;
1899+ } catch {
1900+ return false ;
1901+ }
18371902 } ;
18381903
18391904 const onRecoverActiveSession = async ( ) => {
@@ -2373,13 +2438,28 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
23732438 let sessionSnapshot = materialized ?. session ?? activeSessionSnapshot ;
23742439 if ( ! tabSnapshot || ! sessionSnapshot ) return null ;
23752440
2376- if ( ! sessionSnapshot . terminalRuntimeId ) {
2441+ if ( ! isSessionRuntimeWritable ( sessionSnapshot ) ) {
2442+ const needsRefresh = Boolean ( sessionSnapshot . terminalRuntimeId )
2443+ || sessionSnapshot . runtimeLiveness === "runtime_missing"
2444+ || sessionSnapshot . runtimeLiveness === "provider_exited"
2445+ || ! sessionSnapshot . runtimeLiveness ;
2446+ if ( needsRefresh ) {
2447+ const refreshedTab = await refreshWorkspaceRuntimeSnapshot ( tabSnapshot . id ) ;
2448+ if ( refreshedTab ) {
2449+ tabSnapshot = refreshedTab ;
2450+ sessionSnapshot = refreshedTab . sessions . find ( ( session ) => session . id === sessionSnapshot . id ) ?? sessionSnapshot ;
2451+ }
2452+ }
2453+ }
2454+
2455+ if ( ! isSessionRuntimeWritable ( sessionSnapshot ) ) {
23772456 const started = await startSessionRuntimeInPane ( paneId , tabSnapshot , sessionSnapshot ) ;
23782457 if ( ! started ) return null ;
23792458 tabSnapshot = started . tab ;
23802459 sessionSnapshot = started . session ;
23812460 }
23822461
2462+ if ( ! isSessionRuntimeWritable ( sessionSnapshot ) ) return null ;
23832463 touchSession ( tabSnapshot . id , sessionSnapshot . id ) ;
23842464 return { tab : tabSnapshot , session : sessionSnapshot } ;
23852465 } ;
@@ -2450,6 +2530,33 @@ export default function WorkspaceScreen({ locale, appSettings, onOpenSettings }:
24502530 focusWorkspaceAgentPane ( paneId ) ;
24512531 } ;
24522532
2533+ useEffect ( ( ) => {
2534+ const unsubscribe = subscribeWorkspaceInputError ( ( payload ) => {
2535+ if ( ! STALE_SESSION_INPUT_ERRORS . has ( payload . error ) ) return ;
2536+ const tab = stateRef . current . tabs . find ( ( item ) => item . id === payload . workspace_id ) ;
2537+ if ( ! tab ) return ;
2538+ void ( async ( ) => {
2539+ let refreshedTab = await refreshWorkspaceRuntimeSnapshot ( tab . id ) ;
2540+ if ( ! refreshedTab ) {
2541+ refreshedTab = stateRef . current . tabs . find ( ( item ) => item . id === tab . id ) ?? null ;
2542+ }
2543+ if ( ! refreshedTab || stateRef . current . activeTabId !== refreshedTab . id ) return ;
2544+ const paneSessionId = findPaneSessionId ( refreshedTab . paneLayout , refreshedTab . activePaneId ) ?? refreshedTab . activeSessionId ;
2545+ const session = refreshedTab . sessions . find ( ( item ) => item . id === paneSessionId ) ;
2546+ if ( ! session || session . unavailableReason || isSessionRuntimeWritable ( session ) ) return ;
2547+ const restarted = await startSessionRuntimeInPane ( refreshedTab . activePaneId , refreshedTab , session ) ;
2548+ if ( ! restarted ) {
2549+ addToast ( {
2550+ id : createId ( "toast" ) ,
2551+ text : `Session input recovery failed: ${ payload . error } ` ,
2552+ sessionId : session . id ,
2553+ } ) ;
2554+ }
2555+ } ) ( ) ;
2556+ } ) ;
2557+ return ( ) => unsubscribe ( ) ;
2558+ } , [ addToast , refreshWorkspaceRuntimeSnapshot ] ) ;
2559+
24532560 useEffect ( ( ) => {
24542561 return ( ) => {
24552562 for ( const timer of agentTerminalInputFlushTimerRef . current . values ( ) ) {
0 commit comments