- Carry pending launch context into terminal open - Delay buffer replay until surface hydration is stable - Co-authored-by: codex <codex@users.noreply.github.com>
Unified · Split
| @@ -47,10 +47,11 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex · 46 unmodified lines | |||
| 47 | } | 47 | } |
| 48 | } | 48 | } |
| 49 | 49 | ||
| 50 | var fontSize: Float = 12f | 50 | var fontSize: Float = 10f |
| 51 | set(value) { | 51 | set(value) { |
| 52 | field = value | 52 | field = value |
| 53 | textView.textSize = value | 53 | textView.textSize = value |
| 54 | inputView.textSize = max(value, 13f) | ||
| 54 | emitResize() | 55 | emitResize() |
| 55 | } | 56 | } |
| 56 | 57 | ||
| @@ -100,23 +100,26 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate { · 99 unmodified lines | |||
| 100 | } | 100 | } |
| 101 | } | 101 | } |
| 102 | 102 | ||
| 103 | var fontSize: CGFloat = 12 { | 103 | var fontSize: CGFloat = 10 { |
| 104 | didSet { | 104 | didSet { |
| 105 | guard oldValue != fontSize else { return } | ||
| 105 | inputField.font = UIFont.monospacedSystemFont(ofSize: max(fontSize, 13), weight: .regular) | 106 | inputField.font = UIFont.monospacedSystemFont(ofSize: max(fontSize, 13), weight: .regular) |
| 106 | resetSurface() | 107 | refreshSurface() |
| 107 | } | 108 | } |
| 108 | } | 109 | } |
| 109 | 110 | ||
| 110 | var appearanceScheme: String = TerminalAppearanceScheme.dark.rawValue { | 111 | var appearanceScheme: String = TerminalAppearanceScheme.dark.rawValue { |
| 111 | didSet { | 112 | didSet { |
| 113 | guard oldValue != appearanceScheme else { return } | ||
| 112 | appearance = TerminalAppearanceScheme(value: appearanceScheme) | 114 | appearance = TerminalAppearanceScheme(value: appearanceScheme) |
| 113 | updateGhosttyTheme() | 115 | refreshSurface() |
| 114 | } | 116 | } |
| 115 | } | 117 | } |
| 116 | 118 | ||
| 117 | var themeConfig: String = "" { | 119 | var themeConfig: String = "" { |
| 118 | didSet { | 120 | didSet { |
| 119 | updateGhosttyTheme() | 121 | guard oldValue != themeConfig else { return } |
| 122 | refreshSurface() | ||
| 120 | } | 123 | } |
| 121 | } | 124 | } |
| 122 | 125 | ||
| @@ -353,6 +356,11 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate { · 230 unmodified lines | |||
| 353 | setNeedsLayout() | 356 | setNeedsLayout() |
| 354 | } | 357 | } |
| 355 | 358 | ||
| 359 | private func refreshSurface() { | ||
| 360 | resetSurface() | ||
| 361 | createSurfaceIfPossible() | ||
| 362 | } | ||
| 363 | |||
| 356 | private func destroySurface() { | 364 | private func destroySurface() { |
| 357 | if let surface { | 365 | if let surface { |
| 358 | ghostty_surface_set_write_callback(surface, nil, nil) | 366 | ghostty_surface_set_write_callback(surface, nil, nil) |
| @@ -525,19 +533,6 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate { · 166 unmodified lines | |||
| 525 | terminalViewport.backgroundColor = backgroundColorValue | 533 | terminalViewport.backgroundColor = backgroundColorValue |
| 526 | } | 534 | } |
| 527 | 535 | ||
| 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 | |||
| 541 | private func loadThemeConfig(into config: ghostty_config_t) { | 536 | private func loadThemeConfig(into config: ghostty_config_t) { |
| 542 | guard let path = writeThemeConfigFile() else { return } | 537 | guard let path = writeThemeConfigFile() else { return } |
| 543 | path.withCString { cString in | 538 | path.withCString { cString in |
| @@ -18,11 +18,10 @@ import { connectionTone } from "../connection/connectionTone"; · 17 unmodified lines | |||
| 18 | 18 | ||
| 19 | import { useRemoteCatalog } from "../../state/use-remote-catalog"; | 19 | import { useRemoteCatalog } from "../../state/use-remote-catalog"; |
| 20 | import { | 20 | import { |
| 21 | getEnvironmentClient, | ||
| 22 | useRemoteConnectionStatus, | 21 | useRemoteConnectionStatus, |
| 23 | useRemoteEnvironmentState, | 22 | useRemoteEnvironmentState, |
| 24 | } from "../../state/use-remote-environment-registry"; | 23 | } from "../../state/use-remote-environment-registry"; |
| 25 | import { terminalSessionManager, useKnownTerminalSessions } from "../../state/use-terminal-session"; | 24 | import { useKnownTerminalSessions } from "../../state/use-terminal-session"; |
| 26 | import { useSelectedThreadDetail } from "../../state/use-thread-detail"; | 25 | import { useSelectedThreadDetail } from "../../state/use-thread-detail"; |
| 27 | import { useThreadSelection } from "../../state/use-thread-selection"; | 26 | import { useThreadSelection } from "../../state/use-thread-selection"; |
| 28 | import { GitActionProgressOverlay } from "./GitActionProgressOverlay"; | 27 | import { GitActionProgressOverlay } from "./GitActionProgressOverlay"; |
| @@ -31,7 +30,10 @@ import { · 2 unmodified lines | |||
| 31 | nextTerminalId, | 30 | nextTerminalId, |
| 32 | resolveProjectScriptTerminalId, | 31 | resolveProjectScriptTerminalId, |
| 33 | } from "./terminalMenu"; | 32 | } from "./terminalMenu"; |
| 34 | import { resolvePreferredThreadWorktreePath } from "./terminalLaunchContext"; | 33 | import { |
| 34 | resolvePreferredThreadWorktreePath, | ||
| 35 | stagePendingTerminalLaunch, | ||
| 36 | } from "./terminalLaunchContext"; | ||
| 35 | import { ThreadDetailScreen } from "./ThreadDetailScreen"; | 37 | import { ThreadDetailScreen } from "./ThreadDetailScreen"; |
| 36 | import { ThreadGitControls } from "./ThreadGitControls"; | 38 | import { ThreadGitControls } from "./ThreadGitControls"; |
| 37 | import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; | 39 | import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; |
| @@ -166,11 +168,6 @@ export function ThreadRouteScreen() { · 128 unmodified lines | |||
| 166 | return; | 168 | return; |
| 167 | } | 169 | } |
| 168 | 170 | ||
| 169 | const client = getEnvironmentClient(selectedThread.environmentId); | ||
| 170 | if (!client) { | ||
| 171 | return; | ||
| 172 | } | ||
| 173 | |||
| 174 | const targetTerminalId = resolveProjectScriptTerminalId({ | 171 | const targetTerminalId = resolveProjectScriptTerminalId({ |
| 175 | existingTerminalIds: terminalMenuSessions.map((session) => session.terminalId), | 172 | existingTerminalIds: terminalMenuSessions.map((session) => session.terminalId), |
| 176 | hasRunningTerminal: terminalMenuSessions.some( | 173 | hasRunningTerminal: terminalMenuSessions.some( |
| @@ -189,23 +186,20 @@ export function ThreadRouteScreen() { · 12 unmodified lines | |||
| 189 | project: { cwd: selectedThreadProject.workspaceRoot }, | 186 | project: { cwd: selectedThreadProject.workspaceRoot }, |
| 190 | worktreePath: preferredWorktreePath, | 187 | worktreePath: preferredWorktreePath, |
| 191 | }); | 188 | }); |
| 192 | const snapshot = await client.terminal.open({ | 189 | stagePendingTerminalLaunch({ |
| 193 | threadId: selectedThread.id, | 190 | target: { |
| 194 | terminalId: targetTerminalId, | 191 | environmentId: selectedThread.environmentId, |
| 195 | cwd, | 192 | threadId: selectedThread.id, |
| 196 | worktreePath: preferredWorktreePath, | 193 | terminalId: targetTerminalId, |
| 197 | env, | 194 | }, |
| 195 | launch: { | ||
| 196 | cwd, | ||
| 197 | worktreePath: preferredWorktreePath, | ||
| 198 | env, | ||
| 199 | initialInput: `${script.command}\r`, | ||
| 200 | }, | ||
| 198 | }); | 201 | }); |
| 199 | 202 | ||
| 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 | }); | ||
| 209 | void router.push(buildThreadTerminalRoutePath(selectedThread, targetTerminalId)); | 203 | void router.push(buildThreadTerminalRoutePath(selectedThread, targetTerminalId)); |
| 210 | }, | 204 | }, |
| 211 | [ | 205 | [ |
| @@ -12,6 +12,8 @@ describe("resolveTerminalRouteBootstrap", () => { · 11 unmodified lines | |||
| 12 | requestedTerminalId: null, | 12 | requestedTerminalId: null, |
| 13 | currentTerminalId: "default", | 13 | currentTerminalId: "default", |
| 14 | runningTerminalId: "term-2", | 14 | runningTerminalId: "term-2", |
| 15 | currentTerminalStatus: "closed", | ||
| 16 | hasCurrentTerminalHydration: false, | ||
| 15 | }), | 17 | }), |
| 16 | ).toEqual({ | 18 | ).toEqual({ |
| 17 | kind: "redirect", | 19 | kind: "redirect", |
| @@ -19,7 +21,7 @@ describe("resolveTerminalRouteBootstrap", () => { · 1 unmodified line | |||
| 19 | }); | 21 | }); |
| 20 | }); | 22 | }); |
| 21 | 23 | ||
| 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", () => { |
| 23 | expect( | 25 | expect( |
| 24 | resolveTerminalRouteBootstrap({ | 26 | resolveTerminalRouteBootstrap({ |
| 25 | hasThread: true, | 27 | hasThread: true, |
| @@ -28,13 +30,15 @@ describe("resolveTerminalRouteBootstrap", () => { · 2 unmodified lines | |||
| 28 | requestedTerminalId: null, | 30 | requestedTerminalId: null, |
| 29 | currentTerminalId: "default", | 31 | currentTerminalId: "default", |
| 30 | runningTerminalId: "default", | 32 | runningTerminalId: "default", |
| 33 | currentTerminalStatus: "running", | ||
| 34 | hasCurrentTerminalHydration: false, | ||
| 31 | }), | 35 | }), |
| 32 | ).toEqual({ | 36 | ).toEqual({ |
| 33 | kind: "open", | 37 | kind: "open", |
| 34 | }); | 38 | }); |
| 35 | }); | 39 | }); |
| 36 | 40 | ||
| 37 | it("opens explicit terminal routes once so they replay existing history", () => { | 41 | it("opens explicit terminal routes when the session still needs hydration", () => { |
| 38 | expect( | 42 | expect( |
| 39 | resolveTerminalRouteBootstrap({ | 43 | resolveTerminalRouteBootstrap({ |
| 40 | hasThread: true, | 44 | hasThread: true, |
| @@ -43,6 +47,8 @@ describe("resolveTerminalRouteBootstrap", () => { · 2 unmodified lines | |||
| 43 | requestedTerminalId: "term-2", | 47 | requestedTerminalId: "term-2", |
| 44 | currentTerminalId: "term-2", | 48 | currentTerminalId: "term-2", |
| 45 | runningTerminalId: "term-2", | 49 | runningTerminalId: "term-2", |
| 50 | currentTerminalStatus: "running", | ||
| 51 | hasCurrentTerminalHydration: false, | ||
| 46 | }), | 52 | }), |
| 47 | ).toEqual({ | 53 | ).toEqual({ |
| 48 | kind: "open", | 54 | kind: "open", |
| @@ -58,6 +64,42 @@ describe("resolveTerminalRouteBootstrap", () => { · 9 unmodified lines | |||
| 58 | requestedTerminalId: null, | 64 | requestedTerminalId: null, |
| 59 | currentTerminalId: "default", | 65 | currentTerminalId: "default", |
| 60 | runningTerminalId: "default", | 66 | 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, | ||
| 61 | }), | 103 | }), |
| 62 | ).toEqual({ | 104 | ).toEqual({ |
| 63 | kind: "idle", | 105 | kind: "idle", |
| @@ -1,7 +1,7 @@ | |||
| 1 | import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts"; | 1 | import { DEFAULT_TERMINAL_ID } from "@t3tools/contracts"; |
| 2 | import { Stack, useLocalSearchParams, useRouter } from "expo-router"; | 2 | import { Stack, useLocalSearchParams, useRouter } from "expo-router"; |
| 3 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; | 3 | import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 4 | import { Pressable, ScrollView, Text as RNText, View } from "react-native"; | 4 | import { Pressable, ScrollView, Text as RNText, View, useColorScheme } from "react-native"; |
| 5 | import { KeyboardStickyView } from "react-native-keyboard-controller"; | 5 | import { KeyboardStickyView } from "react-native-keyboard-controller"; |
| 6 | import Animated, { useAnimatedKeyboard, useAnimatedStyle } from "react-native-reanimated"; | 6 | import Animated, { useAnimatedKeyboard, useAnimatedStyle } from "react-native-reanimated"; |
| 7 | 7 | ||
| @@ -21,9 +21,19 @@ import { · 13 unmodified lines | |||
| 21 | import { useThreadSelection } from "../../state/use-thread-selection"; | 21 | import { useThreadSelection } from "../../state/use-thread-selection"; |
| 22 | import { useSelectedThreadDetail } from "../../state/use-thread-detail"; | 22 | import { useSelectedThreadDetail } from "../../state/use-thread-detail"; |
| 23 | import { TerminalSurface } from "../../native/terminal/NativeTerminalSurface"; | 23 | import { TerminalSurface } from "../../native/terminal/NativeTerminalSurface"; |
| 24 | import { getPierreTerminalTheme } from "../../native/terminal/terminalTheme"; | ||
| 24 | import { loadPreferences, savePreferencesPatch } from "../../lib/storage"; | 25 | import { loadPreferences, savePreferencesPatch } from "../../lib/storage"; |
| 26 | import { | ||
| 27 | getTerminalBufferReplayKey, | ||
| 28 | getTerminalSurfaceReplayBuffer, | ||
| 29 | TERMINAL_BUFFER_REPLAY_STABILITY_DELAY_MS, | ||
| 30 | } from "./terminalBufferReplay"; | ||
| 25 | import { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap"; | 31 | import { resolveTerminalRouteBootstrap } from "./terminalRouteBootstrap"; |
| 26 | import { resolveTerminalOpenLocation } from "./terminalLaunchContext"; | 32 | import { |
| 33 | resolveTerminalOpenLocation, | ||
| 34 | stagePendingTerminalLaunch, | ||
| 35 | takePendingTerminalLaunch, | ||
| 36 | } from "./terminalLaunchContext"; | ||
| 27 | import { | 37 | import { |
| 28 | basename, | 38 | basename, |
| 29 | buildTerminalMenuSessions, | 39 | buildTerminalMenuSessions, |
| @@ -38,6 +48,12 @@ import { · 8 unmodified lines | |||
| 38 | TERMINAL_FONT_SIZE_STEP, | 48 | TERMINAL_FONT_SIZE_STEP, |
| 39 | normalizeTerminalFontSize, | 49 | normalizeTerminalFontSize, |
| 40 | } from "./terminalPreferences"; | 50 | } from "./terminalPreferences"; |
| 51 | import { | ||
| 52 | cacheTerminalFontSize, | ||
| 53 | cacheTerminalGridSize, | ||
| 54 | getCachedTerminalFontSize, | ||
| 55 | getCachedTerminalGridSize, | ||
| 56 | } from "./terminalUiState"; | ||
| 41 | 57 | ||
| 42 | const DEFAULT_TERMINAL_COLS = 80; | 58 | const DEFAULT_TERMINAL_COLS = 80; |
| 43 | const DEFAULT_TERMINAL_ROWS = 24; | 59 | const DEFAULT_TERMINAL_ROWS = 24; |
| @@ -154,9 +170,14 @@ function applyCtrlModifier(input: string): string { · 110 unmodified lines | |||
| 154 | return input; | 170 | return input; |
| 155 | } | 171 | } |
| 156 | 172 | ||
| 173 | function withAlpha(hexColor: string, alpha: string): string { | ||
| 174 | return /^#[0-9a-f]{6}$/i.test(hexColor) ? `${hexColor}${alpha}` : hexColor; | ||
| 175 | } | ||
| 176 | |||
| 157 | export function ThreadTerminalRouteScreen() { | 177 | export function ThreadTerminalRouteScreen() { |
| 158 | const router = useRouter(); | 178 | const router = useRouter(); |
| 159 | const keyboard = useAnimatedKeyboard(); | 179 | const keyboard = useAnimatedKeyboard(); |
| 180 | const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; | ||
| 160 | const { isLoadingSavedConnection } = useRemoteEnvironmentState(); | 181 | const { isLoadingSavedConnection } = useRemoteEnvironmentState(); |
| 161 | const params = useLocalSearchParams<{ | 182 | const params = useLocalSearchParams<{ |
| 162 | environmentId?: string | string[]; | 183 | environmentId?: string | string[]; |
| @@ -166,21 +187,45 @@ export function ThreadTerminalRouteScreen() { · 3 unmodified lines | |||
| 166 | const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = | 187 | const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = |
| 167 | useThreadSelection(); | 188 | useThreadSelection(); |
| 168 | const selectedThreadDetail = useSelectedThreadDetail(); | 189 | const selectedThreadDetail = useSelectedThreadDetail(); |
| 190 | const routeEnvironmentId = firstRouteParam(params.environmentId); | ||
| 191 | const routeThreadId = firstRouteParam(params.threadId); | ||
| 169 | const requestedTerminalId = firstRouteParam(params.terminalId); | 192 | const requestedTerminalId = firstRouteParam(params.terminalId); |
| 193 | const terminalId = requestedTerminalId ?? DEFAULT_TERMINAL_ID; | ||
| 194 | const cachedFontSize = getCachedTerminalFontSize(); | ||
| 195 | const cachedRouteGridSize = | ||
| 196 | routeEnvironmentId && routeThreadId | ||
| 197 | ? getCachedTerminalGridSize({ | ||
| 198 | environmentId: routeEnvironmentId, | ||
| 199 | threadId: routeThreadId, | ||
| 200 | terminalId, | ||
| 201 | }) | ||
| 202 | : null; | ||
| 170 | const knownSessions = useKnownTerminalSessions({ | 203 | const knownSessions = useKnownTerminalSessions({ |
| 171 | environmentId: selectedThread?.environmentId ?? null, | 204 | environmentId: selectedThread?.environmentId ?? null, |
| 172 | threadId: selectedThread?.id ?? null, | 205 | threadId: selectedThread?.id ?? null, |
| 173 | }); | 206 | }); |
| 174 | const [lastGridSize, setLastGridSize] = useState({ | 207 | const [lastGridSize, setLastGridSize] = useState( |
| 175 | cols: DEFAULT_TERMINAL_COLS, | 208 | cachedRouteGridSize ?? { |
| 176 | rows: DEFAULT_TERMINAL_ROWS, | 209 | cols: DEFAULT_TERMINAL_COLS, |
| 177 | }); | 210 | rows: DEFAULT_TERMINAL_ROWS, |
| 178 | const [pendingModifier, setPendingModifier] = useState<PendingModifier | null>(null); | 211 | }, |
| 179 | const [fontSize, setFontSize] = useState(DEFAULT_TERMINAL_FONT_SIZE); | 212 | ); |
| 213 | const [fontSize, setFontSize] = useState(cachedFontSize ?? DEFAULT_TERMINAL_FONT_SIZE); | ||
| 180 | const hasOpenedRef = useRef(false); | 214 | const hasOpenedRef = useRef(false); |
| 181 | const hasLoadedFontPreferenceRef = useRef(false); | 215 | const bufferReplayTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| 182 | 216 | const lastBufferReplayKeyRef = useRef<string | null>(null); | |
| 183 | const terminalId = requestedTerminalId ?? DEFAULT_TERMINAL_ID; | 217 | const [readyBufferReplayKey, setReadyBufferReplayKey] = useState<string | null>(null); |
| 218 | const [hasResolvedFontPreference, setHasResolvedFontPreference] = useState( | ||
| 219 | cachedFontSize !== null, | ||
| 220 | ); | ||
| 221 | const [hasMeasuredSurface, setHasMeasuredSurface] = useState(cachedRouteGridSize !== null); | ||
| 222 | const [pendingModifierState, setPendingModifierState] = useState<{ | ||
| 223 | readonly terminalId: string; | ||
| 224 | readonly value: PendingModifier | null; | ||
| 225 | }>({ | ||
| 226 | terminalId, | ||
| 227 | value: null, | ||
| 228 | }); | ||
| 184 | const target = useTerminalSessionTarget({ | 229 | const target = useTerminalSessionTarget({ |
| 185 | environmentId: selectedThread?.environmentId ?? null, | 230 | environmentId: selectedThread?.environmentId ?? null, |
| 186 | threadId: selectedThread?.id ?? null, | 231 | threadId: selectedThread?.id ?? null, |
| @@ -194,6 +239,18 @@ export function ThreadTerminalRouteScreen() { · 7 unmodified lines | |||
| 194 | : terminalId, | 239 | : terminalId, |
| 195 | [selectedThread, terminalId], | 240 | [selectedThread, terminalId], |
| 196 | ); | 241 | ); |
| 242 | const bufferReplayKey = useMemo( | ||
| 243 | () => getTerminalBufferReplayKey({ terminalKey, fontSize }), | ||
| 244 | [fontSize, terminalKey], | ||
| 245 | ); | ||
| 246 | if (lastBufferReplayKeyRef.current === null) { | ||
| 247 | lastBufferReplayKeyRef.current = bufferReplayKey; | ||
| 248 | } | ||
| 249 | const terminalSurfaceBuffer = getTerminalSurfaceReplayBuffer({ | ||
| 250 | buffer: terminal.buffer, | ||
| 251 | replayKey: bufferReplayKey, | ||
| 252 | readyReplayKey: readyBufferReplayKey, | ||
| 253 | }); | ||
| 197 | const isRunning = terminal.status === "running" || terminal.status === "starting"; | 254 | const isRunning = terminal.status === "running" || terminal.status === "starting"; |
| 198 | const cwd = terminal.snapshot?.cwd ?? selectedThreadProject?.workspaceRoot ?? null; | 255 | const cwd = terminal.snapshot?.cwd ?? selectedThreadProject?.workspaceRoot ?? null; |
| 199 | const hostPlatform = useMemo( | 256 | const hostPlatform = useMemo( |
| @@ -219,6 +276,9 @@ export function ThreadTerminalRouteScreen() { · 19 unmodified lines | |||
| 219 | }), | 276 | }), |
| 220 | [terminal.hasRunningSubprocess, terminal.status], | 277 | [terminal.hasRunningSubprocess, terminal.status], |
| 221 | ); | 278 | ); |
| 279 | const terminalTheme = getPierreTerminalTheme(appearanceScheme); | ||
| 280 | const pendingModifier = | ||
| 281 | pendingModifierState.terminalId === terminalId ? pendingModifierState.value : null; | ||
| 222 | const headerTitle = useMemo(() => { | 282 | const headerTitle = useMemo(() => { |
| 223 | const topLineParts = [ | 283 | const topLineParts = [ |
| 224 | selectedEnvironmentConnection?.environmentLabel ?? null, | 284 | selectedEnvironmentConnection?.environmentLabel ?? null, |
| @@ -301,24 +361,59 @@ export function ThreadTerminalRouteScreen() { · 76 unmodified lines | |||
| 301 | return; | 361 | return; |
| 302 | } | 362 | } |
| 303 | 363 | ||
| 304 | const launchLocation = resolveTerminalOpenLocation({ | 364 | const pendingLaunchTarget = { |
| 305 | terminalSnapshot: terminal.snapshot, | 365 | environmentId: selectedThread.environmentId, |
| 306 | activeSessionSnapshot: activeKnownSession?.state.snapshot ?? null, | ||
| 307 | workspaceRoot: selectedThreadProject.workspaceRoot, | ||
| 308 | threadShellWorktreePath: selectedThread.worktreePath ?? null, | ||
| 309 | threadDetailWorktreePath: selectedThreadDetail?.worktreePath ?? null, | ||
| 310 | }); | ||
| 311 | |||
| 312 | const snapshot = await client.terminal.open({ | ||
| 313 | threadId: selectedThread.id, | 366 | threadId: selectedThread.id, |
| 314 | terminalId, | 367 | terminalId, |
| 315 | cwd: launchLocation.cwd, | 368 | }; |
| 316 | worktreePath: launchLocation.worktreePath, | 369 | const pendingLaunch = takePendingTerminalLaunch(pendingLaunchTarget); |
| 317 | cols: lastGridSize.cols, | 370 | |
| 318 | rows: lastGridSize.rows, | 371 | try { |
| 319 | }); | 372 | const launchLocation = pendingLaunch |
| 373 | ? { | ||
| 374 | cwd: pendingLaunch.cwd, | ||
| 375 | worktreePath: pendingLaunch.worktreePath, | ||
| 376 | } | ||
| 377 | : resolveTerminalOpenLocation({ | ||
| 378 | terminalSnapshot: terminal.snapshot, | ||
| 379 | activeSessionSnapshot: activeKnownSession?.state.snapshot ?? null, | ||
| 380 | workspaceRoot: selectedThreadProject.workspaceRoot, | ||
| 381 | threadShellWorktreePath: selectedThread.worktreePath ?? null, | ||
| 382 | threadDetailWorktreePath: selectedThreadDetail?.worktreePath ?? null, | ||
| 383 | }); | ||
| 384 | |||
| 385 | const snapshot = await client.terminal.open({ | ||
| 386 | threadId: selectedThread.id, | ||
| 387 | terminalId, | ||
| 388 | cwd: launchLocation.cwd, | ||
| 389 | worktreePath: launchLocation.worktreePath, | ||
| 390 | cols: lastGridSize.cols, | ||
| 391 | rows: lastGridSize.rows, | ||
| 392 | env: pendingLaunch?.env, | ||
| 393 | }); | ||
| 394 | |||
| 395 | terminalSessionManager.syncSnapshot( | ||
| 396 | { environmentId: selectedThread.environmentId }, | ||
| 397 | snapshot, | ||
| 398 | ); | ||
| 320 | 399 | ||
| 321 | terminalSessionManager.syncSnapshot({ environmentId: selectedThread.environmentId }, snapshot); | 400 | if (pendingLaunch?.initialInput) { |
| 401 | await client.terminal.write({ | ||
| 402 | threadId: selectedThread.id, | ||
| 403 | terminalId, | ||
| 404 | data: pendingLaunch.initialInput, | ||
| 405 | }); | ||
| 406 | } | ||
| 407 | } catch (error) { | ||
| 408 | if (pendingLaunch) { | ||
| 409 | stagePendingTerminalLaunch({ | ||
| 410 | target: pendingLaunchTarget, | ||
| 411 | launch: pendingLaunch, | ||
| 412 | }); | ||
| 413 | } | ||
| 414 | |||
| 415 | throw error; | ||
| 416 | } | ||
| 322 | }, [ | 417 | }, [ |
| 323 | lastGridSize.cols, | 418 | lastGridSize.cols, |
| 324 | lastGridSize.rows, | 419 | lastGridSize.rows, |
| @@ -334,6 +429,74 @@ export function ThreadTerminalRouteScreen() { · 9 unmodified lines | |||
| 334 | hasOpenedRef.current = false; | 429 | hasOpenedRef.current = false; |
| 335 | }, [terminalKey]); | 430 | }, [terminalKey]); |
| 336 | 431 | ||
| 432 | const clearBufferReplayTimer = useCallback(() => { | ||
| 433 | if (bufferReplayTimerRef.current !== null) { | ||
| 434 | clearTimeout(bufferReplayTimerRef.current); | ||
| 435 | bufferReplayTimerRef.current = null; | ||
| 436 | } | ||
| 437 | }, []); | ||
| 438 | |||
| 439 | const scheduleBufferReplayReady = useCallback(() => { | ||
| 440 | clearBufferReplayTimer(); | ||
| 441 | const replayKey = bufferReplayKey; | ||
| 442 | bufferReplayTimerRef.current = setTimeout(() => { | ||
| 443 | bufferReplayTimerRef.current = null; | ||
| 444 | setReadyBufferReplayKey(replayKey); | ||
| 445 | }, TERMINAL_BUFFER_REPLAY_STABILITY_DELAY_MS); | ||
| 446 | }, [bufferReplayKey, clearBufferReplayTimer]); | ||
| 447 | |||
| 448 | useEffect(() => { | ||
| 449 | if (lastBufferReplayKeyRef.current === bufferReplayKey) { | ||
| 450 | return; | ||
| 451 | } | ||
| 452 | |||
| 453 | lastBufferReplayKeyRef.current = bufferReplayKey; | ||
| 454 | clearBufferReplayTimer(); | ||
| 455 | setReadyBufferReplayKey(null); | ||
| 456 | }, [bufferReplayKey, clearBufferReplayTimer]); | ||
| 457 | |||
| 458 | useEffect(() => clearBufferReplayTimer, [clearBufferReplayTimer]); | ||
| 459 | |||
| 460 | useEffect(() => { | ||
| 461 | if (!routeEnvironmentId || !routeThreadId) { | ||
| 462 | setLastGridSize({ | ||
| 463 | cols: DEFAULT_TERMINAL_COLS, | ||
| 464 | rows: DEFAULT_TERMINAL_ROWS, | ||
| 465 | }); | ||
| 466 | return; | ||
| 467 | } | ||
| 468 | |||
| 469 | setLastGridSize( | ||
| 470 | getCachedTerminalGridSize({ | ||
| 471 | environmentId: routeEnvironmentId, | ||
| 472 | threadId: routeThreadId, | ||
| 473 | terminalId, | ||
| 474 | }) ?? { | ||
| 475 | cols: DEFAULT_TERMINAL_COLS, | ||
| 476 | rows: DEFAULT_TERMINAL_ROWS, | ||
| 477 | }, | ||
| 478 | ); | ||
| 479 | setHasMeasuredSurface( | ||
| 480 | getCachedTerminalGridSize({ | ||
| 481 | environmentId: routeEnvironmentId, | ||
| 482 | threadId: routeThreadId, | ||
| 483 | terminalId, | ||
| 484 | }) !== null, | ||
| 485 | ); | ||
| 486 | }, [routeEnvironmentId, routeThreadId, terminalId]); | ||
| 487 | |||
| 488 | useEffect(() => { | ||
| 489 | setHasMeasuredSurface( | ||
| 490 | routeEnvironmentId !== null && | ||
| 491 | routeThreadId !== null && | ||
| 492 | getCachedTerminalGridSize({ | ||
| 493 | environmentId: routeEnvironmentId, | ||
| 494 | threadId: routeThreadId, | ||
| 495 | terminalId, | ||
| 496 | }) !== null, | ||
| 497 | ); | ||
| 498 | }, [fontSize, routeEnvironmentId, routeThreadId, terminalId]); | ||
| 499 | |||
| 337 | useEffect(() => { | 500 | useEffect(() => { |
| 338 | let cancelled = false; | 501 | let cancelled = false; |
| 339 | 502 | ||
| @@ -343,15 +506,15 @@ export function ThreadTerminalRouteScreen() { · 3 unmodified lines | |||
| 343 | return; | 506 | return; |
| 344 | } | 507 | } |
| 345 | 508 | ||
| 346 | setFontSize(normalizeTerminalFontSize(preferences.terminalFontSize)); | 509 | setFontSize(cacheTerminalFontSize(preferences.terminalFontSize)); |
| 347 | hasLoadedFontPreferenceRef.current = true; | 510 | setHasResolvedFontPreference(true); |
| 348 | }) | 511 | }) |
| 349 | .catch(() => { | 512 | .catch(() => { |
| 350 | if (cancelled) { | 513 | if (cancelled) { |
| 351 | return; | 514 | return; |
| 352 | } | 515 | } |
| 353 | 516 | ||
| 354 | hasLoadedFontPreferenceRef.current = true; | 517 | setHasResolvedFontPreference(true); |
| 355 | }); | 518 | }); |
| 356 | 519 | ||
| 357 | return () => { | 520 | return () => { |
| @@ -360,16 +523,21 @@ export function ThreadTerminalRouteScreen() { · 2 unmodified lines | |||
| 360 | }, []); | 523 | }, []); |
| 361 | 524 | ||
| 362 | useEffect(() => { | 525 | useEffect(() => { |
| 363 | if (!hasLoadedFontPreferenceRef.current) { | 526 | if (!hasResolvedFontPreference) { |
| 364 | return; | 527 | return; |
| 365 | } | 528 | } |
| 366 | 529 | ||
| 530 | cacheTerminalFontSize(fontSize); | ||
| 367 | void savePreferencesPatch({ | 531 | void savePreferencesPatch({ |
| 368 | terminalFontSize: normalizeTerminalFontSize(fontSize), | 532 | terminalFontSize: normalizeTerminalFontSize(fontSize), |
| 369 | }); | 533 | }); |
| 370 | }, [fontSize]); | 534 | }, [fontSize, hasResolvedFontPreference]); |
| 371 | 535 | ||
| 372 | useEffect(() => { | 536 | useEffect(() => { |
| 537 | if (!hasResolvedFontPreference || !hasMeasuredSurface) { | ||
| 538 | return; | ||
| 539 | } | ||
| 540 | |||
| 373 | const bootstrapAction = resolveTerminalRouteBootstrap({ | 541 | const bootstrapAction = resolveTerminalRouteBootstrap({ |
| 374 | hasThread: selectedThread !== null, | 542 | hasThread: selectedThread !== null, |
| 375 | hasWorkspaceRoot: Boolean(selectedThreadProject?.workspaceRoot), | 543 | hasWorkspaceRoot: Boolean(selectedThreadProject?.workspaceRoot), |
| @@ -377,6 +545,8 @@ export function ThreadTerminalRouteScreen() { · 1 unmodified line | |||
| 377 | requestedTerminalId, | 545 | requestedTerminalId, |
| 378 | currentTerminalId: terminalId, | 546 | currentTerminalId: terminalId, |
| 379 | runningTerminalId: runningSession?.target.terminalId ?? null, | 547 | runningTerminalId: runningSession?.target.terminalId ?? null, |
| 548 | currentTerminalStatus: terminal.status, | ||
| 549 | hasCurrentTerminalHydration: terminal.snapshot !== null || terminal.buffer.length > 0, | ||
| 380 | }); | 550 | }); |
| 381 | if (bootstrapAction.kind === "idle" || !selectedThread) { | 551 | if (bootstrapAction.kind === "idle" || !selectedThread) { |
| 382 | return; | 552 | return; |
| @@ -398,13 +568,14 @@ export function ThreadTerminalRouteScreen() { · 15 unmodified lines | |||
| 398 | runningSession, | 568 | runningSession, |
| 399 | selectedThread, | 569 | selectedThread, |
| 400 | selectedThreadProject?.workspaceRoot, | 570 | selectedThreadProject?.workspaceRoot, |
| 571 | terminal.buffer.length, | ||
| 572 | terminal.snapshot, | ||
| 573 | terminal.status, | ||
| 401 | terminalId, | 574 | terminalId, |
| 575 | hasMeasuredSurface, | ||
| 576 | hasResolvedFontPreference, | ||
| 402 | ]); | 577 | ]); |
| 403 | 578 | ||
| 404 | useEffect(() => { | ||
| 405 | setPendingModifier(null); | ||
| 406 | }, [terminalId]); | ||
| 407 | |||
| 408 | const writeInput = useCallback( | 579 | const writeInput = useCallback( |
| 409 | (data: string) => { | 580 | (data: string) => { |
| 410 | if (!selectedThread || !isRunning) { | 581 | if (!selectedThread || !isRunning) { |
| @@ -432,20 +603,34 @@ export function ThreadTerminalRouteScreen() { · 21 unmodified lines | |||
| 432 | } | 603 | } |
| 433 | 604 | ||
| 434 | if (pendingModifier === "ctrl") { | 605 | if (pendingModifier === "ctrl") { |
| 435 | setPendingModifier(null); | 606 | setPendingModifierState({ terminalId, value: null }); |
| 436 | writeInput(applyCtrlModifier(data)); | 607 | writeInput(applyCtrlModifier(data)); |
| 437 | } else if (pendingModifier === "meta") { | 608 | } else if (pendingModifier === "meta") { |
| 438 | setPendingModifier(null); | 609 | setPendingModifierState({ terminalId, value: null }); |
| 439 | writeInput(`\u001b${data}`); | 610 | writeInput(`\u001b${data}`); |
| 440 | } else { | 611 | } else { |
| 441 | writeInput(data); | 612 | writeInput(data); |
| 442 | } | 613 | } |
| 443 | }, | 614 | }, |
| 444 | [pendingModifier, writeInput], | 615 | [pendingModifier, terminalId, writeInput], |
| 445 | ); | 616 | ); |
| 446 | 617 | ||
| 447 | const handleResize = useCallback( | 618 | const handleResize = useCallback( |
| 448 | (size: { readonly cols: number; readonly rows: number }) => { | 619 | (size: { readonly cols: number; readonly rows: number }) => { |
| 620 | setHasMeasuredSurface(true); | ||
| 621 | if (readyBufferReplayKey !== bufferReplayKey) { | ||
| 622 | scheduleBufferReplayReady(); | ||
| 623 | } | ||
| 624 | if (routeEnvironmentId && routeThreadId) { | ||
| 625 | cacheTerminalGridSize( | ||
| 626 | { | ||
| 627 | environmentId: routeEnvironmentId, | ||
| 628 | threadId: routeThreadId, | ||
| 629 | terminalId, | ||
| 630 | }, | ||
| 631 | size, | ||
| 632 | ); | ||
| 633 | } | ||
| 449 | if (size.cols === lastGridSize.cols && size.rows === lastGridSize.rows) { | 634 | if (size.cols === lastGridSize.cols && size.rows === lastGridSize.rows) { |
| 450 | return; | 635 | return; |
| 451 | } | 636 | } |
| @@ -467,7 +652,18 @@ export function ThreadTerminalRouteScreen() { · 15 unmodified lines | |||
| 467 | rows: size.rows, | 652 | rows: size.rows, |
| 468 | }); | 653 | }); |
| 469 | }, | 654 | }, |
| 470 | [isRunning, lastGridSize.cols, lastGridSize.rows, selectedThread, terminalId], | 655 | [ |
| 656 | isRunning, | ||
| 657 | lastGridSize.cols, | ||
| 658 | lastGridSize.rows, | ||
| 659 | bufferReplayKey, | ||
| 660 | readyBufferReplayKey, | ||
| 661 | routeEnvironmentId, | ||
| 662 | routeThreadId, | ||
| 663 | scheduleBufferReplayReady, | ||
| 664 | selectedThread, | ||
| 665 | terminalId, | ||
| 666 | ], | ||
| 471 | ); | 667 | ); |
| 472 | 668 | ||
| 473 | const handleSelectTerminal = useCallback( | 669 | const handleSelectTerminal = useCallback( |
| @@ -496,7 +692,7 @@ export function ThreadTerminalRouteScreen() { · 22 unmodified lines | |||
| 496 | 692 | ||
| 497 | const adjustFontSize = useCallback((delta: number) => { | 693 | const adjustFontSize = useCallback((delta: number) => { |
| 498 | setTimeout(() => { | 694 | setTimeout(() => { |
| 499 | setFontSize((current) => normalizeTerminalFontSize(current + delta)); | 695 | setFontSize((current) => cacheTerminalFontSize(current + delta)); |
| 500 | }, 0); | 696 | }, 0); |
| 501 | }, []); | 697 | }, []); |
| 502 | 698 | ||
| @@ -511,11 +707,17 @@ export function ThreadTerminalRouteScreen() { · 8 unmodified lines | |||
| 511 | const handleToolbarActionPress = useCallback( | 707 | const handleToolbarActionPress = useCallback( |
| 512 | (action: TerminalToolbarAction) => { | 708 | (action: TerminalToolbarAction) => { |
| 513 | if (action.kind === "modifier") { | 709 | if (action.kind === "modifier") { |
| 514 | setPendingModifier((current) => (current === action.modifier ? null : action.modifier)); | 710 | setPendingModifierState((current) => ({ |
| 711 | terminalId, | ||
| 712 | value: | ||
| 713 | (current.terminalId === terminalId ? current.value : null) === action.modifier | ||
| 714 | ? null | ||
| 715 | : action.modifier, | ||
| 716 | })); | ||
| 515 | return; | 717 | return; |
| 516 | } | 718 | } |
| 517 | 719 | ||
| 518 | setPendingModifier(null); | 720 | setPendingModifierState({ terminalId, value: null }); |
| 519 | if (pendingModifier === "ctrl") { | 721 | if (pendingModifier === "ctrl") { |
| 520 | writeInput(applyCtrlModifier(action.data)); | 722 | writeInput(applyCtrlModifier(action.data)); |
| 521 | } else if (pendingModifier === "meta") { | 723 | } else if (pendingModifier === "meta") { |
| @@ -524,7 +726,7 @@ export function ThreadTerminalRouteScreen() { · 2 unmodified lines | |||
| 524 | writeInput(action.data); | 726 | writeInput(action.data); |
| 525 | } | 727 | } |
| 526 | }, | 728 | }, |
| 527 | [pendingModifier, writeInput], | 729 | [pendingModifier, terminalId, writeInput], |
| 528 | ); | 730 | ); |
| 529 | 731 | ||
| 530 | if (!selectedThread) { | 732 | if (!selectedThread) { |
| @@ -561,8 +763,8 @@ export function ThreadTerminalRouteScreen() { · 30 unmodified lines | |||
| 561 | headerBackButtonDisplayMode: "minimal", | 763 | headerBackButtonDisplayMode: "minimal", |
| 562 | headerBackTitle: "", | 764 | headerBackTitle: "", |
| 563 | headerShadowVisible: false, | 765 | headerShadowVisible: false, |
| 564 | headerStyle: { backgroundColor: "#0a0a0a" }, | 766 | headerStyle: { backgroundColor: terminalTheme.background }, |
| 565 | headerTintColor: "#f5f5f5", | 767 | headerTintColor: terminalTheme.foreground, |
| 566 | headerTitleAlign: "center", | 768 | headerTitleAlign: "center", |
| 567 | title: "", | 769 | title: "", |
| 568 | headerTitle: () => ( | 770 | headerTitle: () => ( |
| @@ -576,7 +778,7 @@ export function ThreadTerminalRouteScreen() { · 7 unmodified lines | |||
| 576 | <RNText | 778 | <RNText |
| 577 | numberOfLines={1} | 779 | numberOfLines={1} |
| 578 | style={{ | 780 | style={{ |
| 579 | color: "#f5f5f5", | 781 | color: terminalTheme.foreground, |
| 580 | fontFamily: "DMSans_700Bold", | 782 | fontFamily: "DMSans_700Bold", |
| 581 | fontSize: 13, | 783 | fontSize: 13, |
| 582 | lineHeight: 16, | 784 | lineHeight: 16, |
| @@ -588,7 +790,7 @@ export function ThreadTerminalRouteScreen() { · 5 unmodified lines | |||
| 588 | ellipsizeMode="middle" | 790 | ellipsizeMode="middle" |
| 589 | numberOfLines={1} | 791 | numberOfLines={1} |
| 590 | style={{ | 792 | style={{ |
| 591 | color: "#8f8f94", | 793 | color: terminalTheme.mutedForeground, |
| 592 | fontFamily: "Menlo", | 794 | fontFamily: "Menlo", |
| 593 | fontSize: 11, | 795 | fontSize: 11, |
| 594 | lineHeight: 14, | 796 | lineHeight: 14, |
| @@ -658,10 +860,10 @@ export function ThreadTerminalRouteScreen() { · 63 unmodified lines | |||
| 658 | </Stack.Toolbar.Menu> | 860 | </Stack.Toolbar.Menu> |
| 659 | </Stack.Toolbar> | 861 | </Stack.Toolbar> |
| 660 | 862 | ||
| 661 | <View style={{ flex: 1, backgroundColor: "#050505" }}> | 863 | <View style={{ flex: 1, backgroundColor: terminalTheme.background }}> |
| 662 | <Animated.View style={[{ flex: 1 }, terminalContainerAnimatedStyle]}> | 864 | <Animated.View style={[{ flex: 1 }, terminalContainerAnimatedStyle]}> |
| 663 | <TerminalSurface | 865 | <TerminalSurface |
| 664 | buffer={terminal.buffer} | 866 | buffer={terminalSurfaceBuffer} |
| 665 | fontSize={fontSize} | 867 | fontSize={fontSize} |
| 666 | isRunning={isRunning} | 868 | isRunning={isRunning} |
| 667 | onInput={handleInput} | 869 | onInput={handleInput} |
| @@ -674,8 +876,8 @@ export function ThreadTerminalRouteScreen() { · 6 unmodified lines | |||
| 674 | <KeyboardStickyView style={{ position: "absolute", bottom: 0, left: 0, right: 0 }}> | 876 | <KeyboardStickyView style={{ position: "absolute", bottom: 0, left: 0, right: 0 }}> |
| 675 | <View | 877 | <View |
| 676 | style={{ | 878 | style={{ |
| 677 | backgroundColor: "#0a0a0a", | 879 | backgroundColor: terminalTheme.background, |
| 678 | borderTopColor: "rgba(255,255,255,0.08)", | 880 | borderTopColor: terminalTheme.border, |
| 679 | borderTopWidth: 1, | 881 | borderTopWidth: 1, |
| 680 | minHeight: TERMINAL_ACCESSORY_HEIGHT, | 882 | minHeight: TERMINAL_ACCESSORY_HEIGHT, |
| 681 | paddingBottom: 4, | 883 | paddingBottom: 4, |
| @@ -698,11 +900,13 @@ export function ThreadTerminalRouteScreen() { · 16 unmodified lines | |||
| 698 | style={({ pressed }) => ({ | 900 | style={({ pressed }) => ({ |
| 699 | alignItems: "center", | 901 | alignItems: "center", |
| 700 | backgroundColor: active | 902 | backgroundColor: active |
| 701 | ? "rgba(74, 222, 128, 0.18)" | 903 | ? withAlpha(terminalTheme.palette[10] ?? terminalTheme.foreground, "2e") |
| 702 | : pressed | 904 | : pressed |
| 703 | ? "rgba(255,255,255,0.12)" | 905 | ? withAlpha(terminalTheme.foreground, "1f") |
| 704 | : "rgba(255,255,255,0.06)", | 906 | : withAlpha(terminalTheme.foreground, "12"), |
| 705 | borderColor: active ? "rgba(74, 222, 128, 0.36)" : "rgba(255,255,255,0.08)", | 907 | borderColor: active |
| 908 | ? withAlpha(terminalTheme.palette[10] ?? terminalTheme.foreground, "52") | ||
| 909 | : terminalTheme.border, | ||
| 706 | borderRadius: 12, | 910 | borderRadius: 12, |
| 707 | borderWidth: 1, | 911 | borderWidth: 1, |
| 708 | justifyContent: "center", | 912 | justifyContent: "center", |
| @@ -713,7 +917,9 @@ export function ThreadTerminalRouteScreen() { · 4 unmodified lines | |||
| 713 | > | 917 | > |
| 714 | <RNText | 918 | <RNText |
| 715 | style={{ | 919 | style={{ |
| 716 | color: active ? "#bbf7d0" : "#f5f5f5", | 920 | color: active |
| 921 | ? (terminalTheme.palette[10] ?? terminalTheme.foreground) | ||
| 922 | : terminalTheme.foreground, | ||
| 717 | fontFamily: "DMSans_700Bold", | 923 | fontFamily: "DMSans_700Bold", |
| 718 | fontSize: 12, | 924 | fontSize: 12, |
| 719 | fontWeight: "700", | 925 | fontWeight: "700", |
| @@ -0,0 +1,43 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { getTerminalBufferReplayKey, getTerminalSurfaceReplayBuffer } from "./terminalBufferReplay"; | ||
| 4 | |||
| 5 | describe("terminalBufferReplay", () => { | ||
| 6 | it("keys replay readiness by terminal identity and font metrics", () => { | ||
| 7 | expect( | ||
| 8 | getTerminalBufferReplayKey({ | ||
| 9 | terminalKey: "env-1:thread-1:default", | ||
| 10 | fontSize: 10, | ||
| 11 | }), | ||
| 12 | ).toBe("env-1:thread-1:default:10"); | ||
| 13 | }); | ||
| 14 | |||
| 15 | it("holds back terminal history until the measured surface is ready", () => { | ||
| 16 | const replayKey = getTerminalBufferReplayKey({ | ||
| 17 | terminalKey: "env-1:thread-1:default", | ||
| 18 | fontSize: 10, | ||
| 19 | }); | ||
| 20 | |||
| 21 | expect( | ||
| 22 | getTerminalSurfaceReplayBuffer({ | ||
| 23 | buffer: "fastfetch output", | ||
| 24 | replayKey, | ||
| 25 | readyReplayKey: null, | ||
| 26 | }), | ||
| 27 | ).toBe(""); | ||
| 28 | expect( | ||
| 29 | getTerminalSurfaceReplayBuffer({ | ||
| 30 | buffer: "fastfetch output", | ||
| 31 | replayKey, | ||
| 32 | readyReplayKey: "env-1:thread-1:default:11", | ||
| 33 | }), | ||
| 34 | ).toBe(""); | ||
| 35 | expect( | ||
| 36 | getTerminalSurfaceReplayBuffer({ | ||
| 37 | buffer: "fastfetch output", | ||
| 38 | replayKey, | ||
| 39 | readyReplayKey: replayKey, | ||
| 40 | }), | ||
| 41 | ).toBe("fastfetch output"); | ||
| 42 | }); | ||
| 43 | }); | ||
| @@ -0,0 +1,16 @@ | |||
| 1 | export const TERMINAL_BUFFER_REPLAY_STABILITY_DELAY_MS = 180; | ||
| 2 | |||
| 3 | export function getTerminalBufferReplayKey(input: { | ||
| 4 | readonly terminalKey: string; | ||
| 5 | readonly fontSize: number; | ||
| 6 | }): string { | ||
| 7 | return `${input.terminalKey}:${input.fontSize}`; | ||
| 8 | } | ||
| 9 | |||
| 10 | export function getTerminalSurfaceReplayBuffer(input: { | ||
| 11 | readonly buffer: string; | ||
| 12 | readonly replayKey: string; | ||
| 13 | readonly readyReplayKey: string | null; | ||
| 14 | }): string { | ||
| 15 | return input.readyReplayKey === input.replayKey ? input.buffer : ""; | ||
| 16 | } | ||
| @@ -1,8 +1,11 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | 1 | import { describe, expect, it } from "vitest"; |
| 2 | 2 | ||
| 3 | import { | 3 | import { |
| 4 | peekPendingTerminalLaunch, | ||
| 4 | resolvePreferredThreadWorktreePath, | 5 | resolvePreferredThreadWorktreePath, |
| 5 | resolveTerminalOpenLocation, | 6 | resolveTerminalOpenLocation, |
| 7 | stagePendingTerminalLaunch, | ||
| 8 | takePendingTerminalLaunch, | ||
| 6 | } from "./terminalLaunchContext"; | 9 | } from "./terminalLaunchContext"; |
| 7 | 10 | ||
| 8 | describe("resolvePreferredThreadWorktreePath", () => { | 11 | describe("resolvePreferredThreadWorktreePath", () => { |
| @@ -59,3 +62,68 @@ describe("resolveTerminalOpenLocation", () => { · 50 unmodified lines | |||
| 59 | }); | 62 | }); |
| 60 | }); | 63 | }); |
| 61 | }); | 64 | }); |
| 65 | |||
| 66 | describe("pending terminal launches", () => { | ||
| 67 | it("stages and consumes launch details for a specific terminal target", () => { | ||
| 68 | const target = { | ||
| 69 | environmentId: "env-1", | ||
| 70 | threadId: "thread-1", | ||
| 71 | terminalId: "term-2", | ||
| 72 | }; | ||
| 73 | |||
| 74 | stagePendingTerminalLaunch({ | ||
| 75 | target, | ||
| 76 | launch: { | ||
| 77 | cwd: "/repo/worktrees/feature", | ||
| 78 | worktreePath: "/repo/worktrees/feature", | ||
| 79 | env: { FOO: "bar" }, | ||
| 80 | initialInput: "pnpm dev\r", | ||
| 81 | }, | ||
| 82 | }); | ||
| 83 | |||
| 84 | expect(peekPendingTerminalLaunch(target)).toEqual({ | ||
| 85 | cwd: "/repo/worktrees/feature", | ||
| 86 | worktreePath: "/repo/worktrees/feature", | ||
| 87 | env: { FOO: "bar" }, | ||
| 88 | initialInput: "pnpm dev\r", | ||
| 89 | }); | ||
| 90 | expect(takePendingTerminalLaunch(target)).toEqual({ | ||
| 91 | cwd: "/repo/worktrees/feature", | ||
| 92 | worktreePath: "/repo/worktrees/feature", | ||
| 93 | env: { FOO: "bar" }, | ||
| 94 | initialInput: "pnpm dev\r", | ||
| 95 | }); | ||
| 96 | expect(peekPendingTerminalLaunch(target)).toBeNull(); | ||
| 97 | }); | ||
| 98 | |||
| 99 | it("keeps pending launches isolated per terminal target", () => { | ||
| 100 | const primaryTarget = { | ||
| 101 | environmentId: "env-1", | ||
| 102 | threadId: "thread-1", | ||
| 103 | terminalId: "term-2", | ||
| 104 | }; | ||
| 105 | const otherTarget = { | ||
| 106 | environmentId: "env-1", | ||
| 107 | threadId: "thread-1", | ||
| 108 | terminalId: "term-3", | ||
| 109 | }; | ||
| 110 | |||
| 111 | stagePendingTerminalLaunch({ | ||
| 112 | target: primaryTarget, | ||
| 113 | launch: { | ||
| 114 | cwd: "/repo/root", | ||
| 115 | worktreePath: null, | ||
| 116 | initialInput: "pnpm i\r", | ||
| 117 | }, | ||
| 118 | }); | ||
| 119 | |||
| 120 | expect(peekPendingTerminalLaunch(otherTarget)).toBeNull(); | ||
| 121 | expect(takePendingTerminalLaunch(otherTarget)).toBeNull(); | ||
| 122 | expect(takePendingTerminalLaunch(primaryTarget)).toEqual({ | ||
| 123 | cwd: "/repo/root", | ||
| 124 | worktreePath: null, | ||
| 125 | env: undefined, | ||
| 126 | initialInput: "pnpm i\r", | ||
| 127 | }); | ||
| 128 | }); | ||
| 129 | }); | ||
| @@ -3,6 +3,55 @@ interface TerminalSnapshotLike { · 2 unmodified lines | |||
| 3 | readonly worktreePath: string | null; | 3 | readonly worktreePath: string | null; |
| 4 | } | 4 | } |
| 5 | 5 | ||
| 6 | interface PendingTerminalLaunchTarget { | ||
| 7 | readonly environmentId: string; | ||
| 8 | readonly threadId: string; | ||
| 9 | readonly terminalId: string; | ||
| 10 | } | ||
| 11 | |||
| 12 | export interface PendingTerminalLaunch { | ||
| 13 | readonly cwd: string; | ||
| 14 | readonly worktreePath: string | null; | ||
| 15 | readonly env?: Record<string, string>; | ||
| 16 | readonly initialInput?: string; | ||
| 17 | } | ||
| 18 | |||
| 19 | const pendingTerminalLaunches = new Map<string, PendingTerminalLaunch>(); | ||
| 20 | |||
| 21 | function pendingTerminalLaunchKey(input: PendingTerminalLaunchTarget): string { | ||
| 22 | return `${input.environmentId}:${input.threadId}:${input.terminalId}`; | ||
| 23 | } | ||
| 24 | |||
| 25 | export function stagePendingTerminalLaunch(input: { | ||
| 26 | readonly target: PendingTerminalLaunchTarget; | ||
| 27 | readonly launch: PendingTerminalLaunch; | ||
| 28 | }) { | ||
| 29 | pendingTerminalLaunches.set(pendingTerminalLaunchKey(input.target), { | ||
| 30 | cwd: input.launch.cwd, | ||
| 31 | worktreePath: input.launch.worktreePath, | ||
| 32 | env: input.launch.env ? { ...input.launch.env } : undefined, | ||
| 33 | initialInput: input.launch.initialInput, | ||
| 34 | }); | ||
| 35 | } | ||
| 36 | |||
| 37 | export function peekPendingTerminalLaunch( | ||
| 38 | target: PendingTerminalLaunchTarget, | ||
| 39 | ): PendingTerminalLaunch | null { | ||
| 40 | return pendingTerminalLaunches.get(pendingTerminalLaunchKey(target)) ?? null; | ||
| 41 | } | ||
| 42 | |||
| 43 | export function takePendingTerminalLaunch( | ||
| 44 | target: PendingTerminalLaunchTarget, | ||
| 45 | ): PendingTerminalLaunch | null { | ||
| 46 | const key = pendingTerminalLaunchKey(target); | ||
| 47 | const launch = pendingTerminalLaunches.get(key) ?? null; | ||
| 48 | if (launch) { | ||
| 49 | pendingTerminalLaunches.delete(key); | ||
| 50 | } | ||
| 51 | |||
| 52 | return launch; | ||
| 53 | } | ||
| 54 | |||
| 6 | export function resolvePreferredThreadWorktreePath(input: { | 55 | export function resolvePreferredThreadWorktreePath(input: { |
| 7 | readonly threadShellWorktreePath: string | null; | 56 | readonly threadShellWorktreePath: string | null; |
| 8 | readonly threadDetailWorktreePath: string | null; | 57 | readonly threadDetailWorktreePath: string | null; |
| @@ -5,6 +5,8 @@ export function resolveTerminalRouteBootstrap(input: { · 4 unmodified lines | |||
| 5 | readonly requestedTerminalId: string | null; | 5 | readonly requestedTerminalId: string | null; |
| 6 | readonly currentTerminalId: string; | 6 | readonly currentTerminalId: string; |
| 7 | readonly runningTerminalId: string | null; | 7 | readonly runningTerminalId: string | null; |
| 8 | readonly currentTerminalStatus: "starting" | "running" | "exited" | "error" | "closed"; | ||
| 9 | readonly hasCurrentTerminalHydration: boolean; | ||
| 8 | }): | 10 | }): |
| 9 | | { readonly kind: "idle" } | 11 | | { readonly kind: "idle" } |
| 10 | | { readonly kind: "redirect"; readonly terminalId: string } | 12 | | { readonly kind: "redirect"; readonly terminalId: string } |
| @@ -21,5 +23,12 @@ export function resolveTerminalRouteBootstrap(input: { · 10 unmodified lines | |||
| 21 | return { kind: "redirect", terminalId: input.runningTerminalId }; | 23 | return { kind: "redirect", terminalId: input.runningTerminalId }; |
| 22 | } | 24 | } |
| 23 | 25 | ||
| 26 | if ( | ||
| 27 | (input.currentTerminalStatus === "running" || input.currentTerminalStatus === "starting") && | ||
| 28 | input.hasCurrentTerminalHydration | ||
| 29 | ) { | ||
| 30 | return { kind: "idle" }; | ||
| 31 | } | ||
| 32 | |||
| 24 | return { kind: "open" }; | 33 | return { kind: "open" }; |
| 25 | } | 34 | } |
| @@ -0,0 +1,52 @@ | |||
| 1 | import { beforeEach, describe, expect, it } from "vitest"; | ||
| 2 | |||
| 3 | import { | ||
| 4 | cacheTerminalFontSize, | ||
| 5 | cacheTerminalGridSize, | ||
| 6 | getCachedTerminalFontSize, | ||
| 7 | getCachedTerminalGridSize, | ||
| 8 | resetTerminalUiStateCaches, | ||
| 9 | } from "./terminalUiState"; | ||
| 10 | |||
| 11 | describe("terminalUiState", () => { | ||
| 12 | beforeEach(() => { | ||
| 13 | resetTerminalUiStateCaches(); | ||
| 14 | }); | ||
| 15 | |||
| 16 | it("caches terminal font size using the shared normalization rules", () => { | ||
| 17 | expect(getCachedTerminalFontSize()).toBeNull(); | ||
| 18 | expect(cacheTerminalFontSize(8.5)).toBe(8.5); | ||
| 19 | expect(getCachedTerminalFontSize()).toBe(8.5); | ||
| 20 | expect(cacheTerminalFontSize(100)).toBe(14); | ||
| 21 | expect(getCachedTerminalFontSize()).toBe(14); | ||
| 22 | }); | ||
| 23 | |||
| 24 | it("stores terminal grid sizes per terminal target", () => { | ||
| 25 | const primaryTarget = { | ||
| 26 | environmentId: "env-1", | ||
| 27 | threadId: "thread-1", | ||
| 28 | terminalId: "default", | ||
| 29 | }; | ||
| 30 | const otherTarget = { | ||
| 31 | environmentId: "env-1", | ||
| 32 | threadId: "thread-1", | ||
| 33 | terminalId: "term-2", | ||
| 34 | }; | ||
| 35 | |||
| 36 | expect(getCachedTerminalGridSize(primaryTarget)).toBeNull(); | ||
| 37 | expect( | ||
| 38 | cacheTerminalGridSize(primaryTarget, { | ||
| 39 | cols: 107.9, | ||
| 40 | rows: 33.2, | ||
| 41 | }), | ||
| 42 | ).toEqual({ | ||
| 43 | cols: 107, | ||
| 44 | rows: 33, | ||
| 45 | }); | ||
| 46 | expect(getCachedTerminalGridSize(primaryTarget)).toEqual({ | ||
| 47 | cols: 107, | ||
| 48 | rows: 33, | ||
| 49 | }); | ||
| 50 | expect(getCachedTerminalGridSize(otherTarget)).toBeNull(); | ||
| 51 | }); | ||
| 52 | }); | ||
| @@ -0,0 +1,50 @@ | |||
| 1 | import { DEFAULT_TERMINAL_FONT_SIZE, normalizeTerminalFontSize } from "./terminalPreferences"; | ||
| 2 | |||
| 3 | export interface TerminalGridSize { | ||
| 4 | readonly cols: number; | ||
| 5 | readonly rows: number; | ||
| 6 | } | ||
| 7 | |||
| 8 | export interface TerminalUiStateTarget { | ||
| 9 | readonly environmentId: string; | ||
| 10 | readonly threadId: string; | ||
| 11 | readonly terminalId: string; | ||
| 12 | } | ||
| 13 | |||
| 14 | const terminalGridSizeCache = new Map<string, TerminalGridSize>(); | ||
| 15 | let cachedTerminalFontSize: number | null = null; | ||
| 16 | |||
| 17 | function terminalUiStateKey(target: TerminalUiStateTarget): string { | ||
| 18 | return `${target.environmentId}:${target.threadId}:${target.terminalId}`; | ||
| 19 | } | ||
| 20 | |||
| 21 | export function getCachedTerminalFontSize(): number | null { | ||
| 22 | return cachedTerminalFontSize; | ||
| 23 | } | ||
| 24 | |||
| 25 | export function cacheTerminalFontSize(value: number | null | undefined): number { | ||
| 26 | const normalized = normalizeTerminalFontSize(value ?? DEFAULT_TERMINAL_FONT_SIZE); | ||
| 27 | cachedTerminalFontSize = normalized; | ||
| 28 | return normalized; | ||
| 29 | } | ||
| 30 | |||
| 31 | export function getCachedTerminalGridSize(target: TerminalUiStateTarget): TerminalGridSize | null { | ||
| 32 | return terminalGridSizeCache.get(terminalUiStateKey(target)) ?? null; | ||
| 33 | } | ||
| 34 | |||
| 35 | export function cacheTerminalGridSize( | ||
| 36 | target: TerminalUiStateTarget, | ||
| 37 | size: TerminalGridSize, | ||
| 38 | ): TerminalGridSize { | ||
| 39 | const normalized = { | ||
| 40 | cols: Math.max(1, Math.floor(size.cols)), | ||
| 41 | rows: Math.max(1, Math.floor(size.rows)), | ||
| 42 | }; | ||
| 43 | terminalGridSizeCache.set(terminalUiStateKey(target), normalized); | ||
| 44 | return normalized; | ||
| 45 | } | ||
| 46 | |||
| 47 | export function resetTerminalUiStateCaches() { | ||
| 48 | cachedTerminalFontSize = null; | ||
| 49 | terminalGridSizeCache.clear(); | ||
| 50 | } | ||
| @@ -14,7 +14,7 @@ import { AppText as Text } from "../../components/AppText"; · 13 unmodified lines | |||
| 14 | import { resolveNativeTerminalSurfaceView } from "./nativeTerminalModule"; | 14 | import { resolveNativeTerminalSurfaceView } from "./nativeTerminalModule"; |
| 15 | import { | 15 | import { |
| 16 | buildGhosttyThemeConfig, | 16 | buildGhosttyThemeConfig, |
| 17 | getGitHubTerminalTheme, | 17 | getPierreTerminalTheme, |
| 18 | type TerminalTheme, | 18 | type TerminalTheme, |
| 19 | } from "./terminalTheme"; | 19 | } from "./terminalTheme"; |
| 20 | 20 | ||
| @@ -53,7 +53,7 @@ function estimateGridSize(input: { · 32 unmodified lines | |||
| 53 | const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: TerminalSurfaceProps) { | 53 | const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: TerminalSurfaceProps) { |
| 54 | const fontSize = props.fontSize ?? 12; | 54 | const fontSize = props.fontSize ?? 12; |
| 55 | const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; | 55 | const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; |
| 56 | const theme = props.theme ?? getGitHubTerminalTheme(appearanceScheme); | 56 | const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme); |
| 57 | const statusLabel = props.isRunning | 57 | const statusLabel = props.isRunning |
| 58 | ? "Native terminal unavailable. Using text fallback." | 58 | ? "Native terminal unavailable. Using text fallback." |
| 59 | : "Open terminal to start a shell."; | 59 | : "Open terminal to start a shell."; |
| @@ -165,7 +165,7 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter · 105 unmodified lines | |||
| 165 | export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurfaceProps) { | 165 | export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurfaceProps) { |
| 166 | const fontSize = props.fontSize ?? 12; | 166 | const fontSize = props.fontSize ?? 12; |
| 167 | const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; | 167 | const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; |
| 168 | const theme = props.theme ?? getGitHubTerminalTheme(appearanceScheme); | 168 | const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme); |
| 169 | const { onInput, onResize } = props; | 169 | const { onInput, onResize } = props; |
| 170 | const NativeTerminalSurfaceView = resolveNativeTerminalSurfaceView(); | 170 | const NativeTerminalSurfaceView = resolveNativeTerminalSurfaceView(); |
| 171 | const handleNativeInput = useCallback( | 171 | const handleNativeInput = useCallback( |
| @@ -1,36 +1,36 @@ | |||
| 1 | import { describe, expect, it } from "vitest"; | 1 | import { describe, expect, it } from "vitest"; |
| 2 | 2 | ||
| 3 | import { buildGhosttyThemeConfig, getGitHubTerminalTheme } from "./terminalTheme"; | 3 | import { buildGhosttyThemeConfig, getPierreTerminalTheme } from "./terminalTheme"; |
| 4 | 4 | ||
| 5 | describe("getGitHubTerminalTheme", () => { | 5 | describe("getPierreTerminalTheme", () => { |
| 6 | it("returns the GitHub light terminal palette", () => { | 6 | it("returns the Pierre light terminal palette", () => { |
| 7 | expect(getGitHubTerminalTheme("light")).toMatchObject({ | 7 | expect(getPierreTerminalTheme("light")).toMatchObject({ |
| 8 | background: "#ffffff", | 8 | background: "#f2f2f7", |
| 9 | foreground: "#586069", | 9 | foreground: "#6C6C71", |
| 10 | cursorForeground: "#005cc5", | 10 | cursorForeground: "#009fff", |
| 11 | cursorBackground: "#d1d5da", | 11 | cursorBackground: "#f2f2f7", |
| 12 | }); | 12 | }); |
| 13 | }); | 13 | }); |
| 14 | 14 | ||
| 15 | it("returns the GitHub dark terminal palette", () => { | 15 | it("returns the Pierre dark terminal palette", () => { |
| 16 | expect(getGitHubTerminalTheme("dark")).toMatchObject({ | 16 | expect(getPierreTerminalTheme("dark")).toMatchObject({ |
| 17 | background: "#24292e", | 17 | background: "#0a0a0a", |
| 18 | foreground: "#d1d5da", | 18 | foreground: "#adadb1", |
| 19 | cursorForeground: "#79b8ff", | 19 | cursorForeground: "#009fff", |
| 20 | cursorBackground: "#586069", | 20 | cursorBackground: "#0a0a0a", |
| 21 | }); | 21 | }); |
| 22 | }); | 22 | }); |
| 23 | }); | 23 | }); |
| 24 | 24 | ||
| 25 | describe("buildGhosttyThemeConfig", () => { | 25 | describe("buildGhosttyThemeConfig", () => { |
| 26 | it("serializes theme colors into a ghostty config file", () => { | 26 | it("serializes theme colors into a ghostty config file", () => { |
| 27 | const config = buildGhosttyThemeConfig(getGitHubTerminalTheme("dark")); | 27 | const config = buildGhosttyThemeConfig(getPierreTerminalTheme("dark")); |
| 28 | 28 | ||
| 29 | expect(config).toContain("background = #24292e"); | 29 | expect(config).toContain("background = #0a0a0a"); |
| 30 | expect(config).toContain("foreground = #d1d5da"); | 30 | expect(config).toContain("foreground = #adadb1"); |
| 31 | expect(config).toContain("cursor-color = #79b8ff"); | 31 | expect(config).toContain("cursor-color = #009fff"); |
| 32 | expect(config).toContain("palette = 0=#586069"); | 32 | expect(config).toContain("palette = 0=#141415"); |
| 33 | expect(config).toContain("palette = 15=#fafbfc"); | 33 | expect(config).toContain("palette = 15=#c6c6c8"); |
| 34 | expect(config.endsWith("\n")).toBe(true); | 34 | expect(config.endsWith("\n")).toBe(true); |
| 35 | }); | 35 | }); |
| 36 | }); | 36 | }); |
| @@ -10,63 +10,64 @@ export interface TerminalTheme { · 9 unmodified lines | |||
| 10 | readonly palette: readonly string[]; | 10 | readonly palette: readonly string[]; |
| 11 | } | 11 | } |
| 12 | 12 | ||
| 13 | const GITHUB_LIGHT_THEME: TerminalTheme = { | 13 | const PIERRE_LIGHT_THEME: TerminalTheme = { |
| 14 | // Sourced from GitHub's official VS Code theme terminal palette. | 14 | // Pierre terminal palette with the app's shared screen background. |
| 15 | background: "#ffffff", | 15 | background: "#f2f2f7", |
| 16 | foreground: "#586069", | 16 | foreground: "#6C6C71", |
| 17 | mutedForeground: "#6a737d", | 17 | mutedForeground: "#8E8E95", |
| 18 | border: "rgba(27,31,35,0.12)", | 18 | border: "#eeeeef", |
| 19 | cursorForeground: "#005cc5", | 19 | cursorForeground: "#009fff", |
| 20 | cursorBackground: "#d1d5da", | 20 | cursorBackground: "#f2f2f7", |
| 21 | palette: [ | 21 | palette: [ |
| 22 | "#24292e", | 22 | "#1F1F21", |
| 23 | "#d73a49", | 23 | "#ff2e3f", |
| 24 | "#28a745", | 24 | "#0dbe4e", |
| 25 | "#dbab09", | 25 | "#ffca00", |
| 26 | "#0366d6", | 26 | "#009fff", |
| 27 | "#5a32a3", | 27 | "#c635e4", |
| 28 | "#1b7c83", | 28 | "#08c0ef", |
| 29 | "#6a737d", | 29 | "#c6c6c8", |
| 30 | "#959da5", | 30 | "#1F1F21", |
| 31 | "#cb2431", | 31 | "#ff2e3f", |
| 32 | "#22863a", | 32 | "#0dbe4e", |
| 33 | "#b08800", | 33 | "#ffca00", |
| 34 | "#005cc5", | 34 | "#009fff", |
| 35 | "#5a32a3", | 35 | "#c635e4", |
| 36 | "#3192aa", | 36 | "#08c0ef", |
| 37 | "#d1d5da", | 37 | "#c6c6c8", |
| 38 | ], | 38 | ], |
| 39 | }; | 39 | }; |
| 40 | 40 | ||
| 41 | const GITHUB_DARK_THEME: TerminalTheme = { | 41 | const PIERRE_DARK_THEME: TerminalTheme = { |
| 42 | background: "#24292e", | 42 | // Pierre terminal palette with the app's shared screen background. |
| 43 | foreground: "#d1d5da", | 43 | background: "#0a0a0a", |
| 44 | mutedForeground: "#959da5", | 44 | foreground: "#adadb1", |
| 45 | border: "rgba(240,246,252,0.12)", | 45 | mutedForeground: "#8E8E95", |
| 46 | cursorForeground: "#79b8ff", | 46 | border: "#2e2e30", |
| 47 | cursorBackground: "#586069", | 47 | cursorForeground: "#009fff", |
| 48 | cursorBackground: "#0a0a0a", | ||
| 48 | palette: [ | 49 | palette: [ |
| 49 | "#586069", | 50 | "#141415", |
| 50 | "#ea4a5a", | 51 | "#ff2e3f", |
| 51 | "#34d058", | 52 | "#0dbe4e", |
| 52 | "#ffea7f", | 53 | "#ffca00", |
| 53 | "#2188ff", | 54 | "#009fff", |
| 54 | "#b392f0", | 55 | "#c635e4", |
| 55 | "#39c5cf", | 56 | "#08c0ef", |
| 56 | "#d1d5da", | 57 | "#c6c6c8", |
| 57 | "#959da5", | 58 | "#141415", |
| 58 | "#f97583", | 59 | "#ff2e3f", |
| 59 | "#85e89d", | 60 | "#0dbe4e", |
| 60 | "#ffea7f", | 61 | "#ffca00", |
| 61 | "#79b8ff", | 62 | "#009fff", |
| 62 | "#b392f0", | 63 | "#c635e4", |
| 63 | "#56d4dd", | 64 | "#08c0ef", |
| 64 | "#fafbfc", | 65 | "#c6c6c8", |
| 65 | ], | 66 | ], |
| 66 | }; | 67 | }; |
| 67 | 68 | ||
| 68 | export function getGitHubTerminalTheme(scheme: TerminalAppearanceScheme): TerminalTheme { | 69 | export function getPierreTerminalTheme(scheme: TerminalAppearanceScheme): TerminalTheme { |
| 69 | return scheme === "light" ? GITHUB_LIGHT_THEME : GITHUB_DARK_THEME; | 70 | return scheme === "light" ? PIERRE_LIGHT_THEME : PIERRE_DARK_THEME; |
| 70 | } | 71 | } |
| 71 | 72 | ||
| 72 | export function buildGhosttyThemeConfig(theme: TerminalTheme): string { | 73 | export function buildGhosttyThemeConfig(theme: TerminalTheme): string { |