Skip to content

Commit b7da92c

Browse files
Stage remote terminal launches and replay hydrated buffers
- Carry pending launch context into terminal open - Delay buffer replay until surface hydration is stable - Co-authored-by: codex <codex@users.noreply.github.com>
1 parent acdfb6a commit b7da92c

15 files changed

Lines changed: 697 additions & 171 deletions

apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex
4747
}
4848
}
4949

50-
var fontSize: Float = 12f
50+
var fontSize: Float = 10f
5151
set(value) {
5252
field = value
5353
textView.textSize = value
54+
inputView.textSize = max(value, 13f)
5455
emitResize()
5556
}
5657

apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -100,23 +100,26 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate {
100100
}
101101
}
102102

103-
var fontSize: CGFloat = 12 {
103+
var fontSize: CGFloat = 10 {
104104
didSet {
105+
guard oldValue != fontSize else { return }
105106
inputField.font = UIFont.monospacedSystemFont(ofSize: max(fontSize, 13), weight: .regular)
106-
resetSurface()
107+
refreshSurface()
107108
}
108109
}
109110

110111
var appearanceScheme: String = TerminalAppearanceScheme.dark.rawValue {
111112
didSet {
113+
guard oldValue != appearanceScheme else { return }
112114
appearance = TerminalAppearanceScheme(value: appearanceScheme)
113-
updateGhosttyTheme()
115+
refreshSurface()
114116
}
115117
}
116118

117119
var themeConfig: String = "" {
118120
didSet {
119-
updateGhosttyTheme()
121+
guard oldValue != themeConfig else { return }
122+
refreshSurface()
120123
}
121124
}
122125

@@ -353,6 +356,11 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate {
353356
setNeedsLayout()
354357
}
355358

359+
private func refreshSurface() {
360+
resetSurface()
361+
createSurfaceIfPossible()
362+
}
363+
356364
private func destroySurface() {
357365
if let surface {
358366
ghostty_surface_set_write_callback(surface, nil, nil)
@@ -525,19 +533,6 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate {
525533
terminalViewport.backgroundColor = backgroundColorValue
526534
}
527535

528-
private func updateGhosttyTheme() {
529-
guard let app, let surface else { return }
530-
guard let config = ghostty_config_new() else { return }
531-
loadThemeConfig(into: config)
532-
ghostty_config_finalize(config)
533-
ghostty_app_update_config(app, config)
534-
ghostty_surface_update_config(surface, config)
535-
ghostty_app_set_color_scheme(app, appearance.ghosttyColorScheme)
536-
ghostty_surface_set_color_scheme(surface, appearance.ghosttyColorScheme)
537-
ghostty_config_free(config)
538-
redrawSurface()
539-
}
540-
541536
private func loadThemeConfig(into config: ghostty_config_t) {
542537
guard let path = writeThemeConfigFile() else { return }
543538
path.withCString { cString in

apps/mobile/src/features/threads/ThreadRouteScreen.tsx

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@ import { connectionTone } from "../connection/connectionTone";
1818

1919
import { useRemoteCatalog } from "../../state/use-remote-catalog";
2020
import {
21-
getEnvironmentClient,
2221
useRemoteConnectionStatus,
2322
useRemoteEnvironmentState,
2423
} from "../../state/use-remote-environment-registry";
25-
import { terminalSessionManager, useKnownTerminalSessions } from "../../state/use-terminal-session";
24+
import { useKnownTerminalSessions } from "../../state/use-terminal-session";
2625
import { useSelectedThreadDetail } from "../../state/use-thread-detail";
2726
import { useThreadSelection } from "../../state/use-thread-selection";
2827
import { GitActionProgressOverlay } from "./GitActionProgressOverlay";
@@ -31,7 +30,10 @@ import {
3130
nextTerminalId,
3231
resolveProjectScriptTerminalId,
3332
} from "./terminalMenu";
34-
import { resolvePreferredThreadWorktreePath } from "./terminalLaunchContext";
33+
import {
34+
resolvePreferredThreadWorktreePath,
35+
stagePendingTerminalLaunch,
36+
} from "./terminalLaunchContext";
3537
import { ThreadDetailScreen } from "./ThreadDetailScreen";
3638
import { ThreadGitControls } from "./ThreadGitControls";
3739
import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer";
@@ -166,11 +168,6 @@ export function ThreadRouteScreen() {
166168
return;
167169
}
168170

169-
const client = getEnvironmentClient(selectedThread.environmentId);
170-
if (!client) {
171-
return;
172-
}
173-
174171
const targetTerminalId = resolveProjectScriptTerminalId({
175172
existingTerminalIds: terminalMenuSessions.map((session) => session.terminalId),
176173
hasRunningTerminal: terminalMenuSessions.some(
@@ -189,23 +186,20 @@ export function ThreadRouteScreen() {
189186
project: { cwd: selectedThreadProject.workspaceRoot },
190187
worktreePath: preferredWorktreePath,
191188
});
192-
const snapshot = await client.terminal.open({
193-
threadId: selectedThread.id,
194-
terminalId: targetTerminalId,
195-
cwd,
196-
worktreePath: preferredWorktreePath,
197-
env,
189+
stagePendingTerminalLaunch({
190+
target: {
191+
environmentId: selectedThread.environmentId,
192+
threadId: selectedThread.id,
193+
terminalId: targetTerminalId,
194+
},
195+
launch: {
196+
cwd,
197+
worktreePath: preferredWorktreePath,
198+
env,
199+
initialInput: `${script.command}\r`,
200+
},
198201
});
199202

200-
terminalSessionManager.syncSnapshot(
201-
{ environmentId: selectedThread.environmentId },
202-
snapshot,
203-
);
204-
await client.terminal.write({
205-
threadId: selectedThread.id,
206-
terminalId: targetTerminalId,
207-
data: `${script.command}\r`,
208-
});
209203
void router.push(buildThreadTerminalRoutePath(selectedThread, targetTerminalId));
210204
},
211205
[

apps/mobile/src/features/threads/ThreadTerminalRouteScreen.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ describe("resolveTerminalRouteBootstrap", () => {
1212
requestedTerminalId: null,
1313
currentTerminalId: "default",
1414
runningTerminalId: "term-2",
15+
currentTerminalStatus: "closed",
16+
hasCurrentTerminalHydration: false,
1517
}),
1618
).toEqual({
1719
kind: "redirect",
1820
terminalId: "term-2",
1921
});
2022
});
2123

22-
it("hydrates the current running terminal instead of skipping open", () => {
24+
it("hydrates the current running terminal when client state is not hydrated yet", () => {
2325
expect(
2426
resolveTerminalRouteBootstrap({
2527
hasThread: true,
@@ -28,13 +30,15 @@ describe("resolveTerminalRouteBootstrap", () => {
2830
requestedTerminalId: null,
2931
currentTerminalId: "default",
3032
runningTerminalId: "default",
33+
currentTerminalStatus: "running",
34+
hasCurrentTerminalHydration: false,
3135
}),
3236
).toEqual({
3337
kind: "open",
3438
});
3539
});
3640

37-
it("opens explicit terminal routes once so they replay existing history", () => {
41+
it("opens explicit terminal routes when the session still needs hydration", () => {
3842
expect(
3943
resolveTerminalRouteBootstrap({
4044
hasThread: true,
@@ -43,6 +47,8 @@ describe("resolveTerminalRouteBootstrap", () => {
4347
requestedTerminalId: "term-2",
4448
currentTerminalId: "term-2",
4549
runningTerminalId: "term-2",
50+
currentTerminalStatus: "running",
51+
hasCurrentTerminalHydration: false,
4652
}),
4753
).toEqual({
4854
kind: "open",
@@ -58,6 +64,42 @@ describe("resolveTerminalRouteBootstrap", () => {
5864
requestedTerminalId: null,
5965
currentTerminalId: "default",
6066
runningTerminalId: "default",
67+
currentTerminalStatus: "running",
68+
hasCurrentTerminalHydration: true,
69+
}),
70+
).toEqual({
71+
kind: "idle",
72+
});
73+
});
74+
75+
it("stays idle when the current running terminal is already hydrated in client state", () => {
76+
expect(
77+
resolveTerminalRouteBootstrap({
78+
hasThread: true,
79+
hasWorkspaceRoot: true,
80+
hasOpened: false,
81+
requestedTerminalId: null,
82+
currentTerminalId: "default",
83+
runningTerminalId: "default",
84+
currentTerminalStatus: "running",
85+
hasCurrentTerminalHydration: true,
86+
}),
87+
).toEqual({
88+
kind: "idle",
89+
});
90+
});
91+
92+
it("stays idle for explicit running terminal routes that already have hydrated output", () => {
93+
expect(
94+
resolveTerminalRouteBootstrap({
95+
hasThread: true,
96+
hasWorkspaceRoot: true,
97+
hasOpened: false,
98+
requestedTerminalId: "term-2",
99+
currentTerminalId: "term-2",
100+
runningTerminalId: "term-2",
101+
currentTerminalStatus: "running",
102+
hasCurrentTerminalHydration: true,
61103
}),
62104
).toEqual({
63105
kind: "idle",

0 commit comments

Comments
 (0)