Skip to content

Commit 25fedfe

Browse files
committed
fix(web): preserve project terminal history
1 parent e7107a6 commit 25fedfe

13 files changed

Lines changed: 117 additions & 52 deletions
312 KB
Loading
325 KB
Loading

packages/api/src/services/terminal-sessions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -976,10 +976,12 @@ export const renderTmuxAttachCommand = (
976976
`if ! command -v tmux >/dev/null 2>&1; then printf '%s\\n' ${
977977
shellQuote(args.missingMessage ?? tmuxMissingMessage)
978978
} >&2; exit 127; fi`,
979-
`tmux has-session -t ${shellQuote(args.tmuxName)} 2>/dev/null || tmux new-session -d -s ${
979+
`tmux has-session -t ${shellQuote(args.tmuxName)} 2>/dev/null || tmux start-server \\; set-option -g history-limit 50000 \\; new-session -d -s ${
980980
shellQuote(args.tmuxName)
981981
} -c ${shellQuote(args.targetDir)}`,
982982
`tmux set-option -t ${shellQuote(args.tmuxName)} status off >/dev/null 2>&1 || true`,
983+
`tmux set-option -t ${shellQuote(args.tmuxName)} history-limit 50000 >/dev/null 2>&1 || true`,
984+
`tmux set-option -t ${shellQuote(args.tmuxName)} mouse on >/dev/null 2>&1 || true`,
983985
`exec tmux attach-session -t ${shellQuote(args.tmuxName)}`
984986
].join("; ")
985987
return `bash --noprofile --norc -lc ${shellQuote(script)}`

packages/api/tests/terminal-sessions.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,13 @@ describe("terminal sessions service", () => {
247247
expect(command).toContain("command -v tmux")
248248
expect(command).toContain("bash --noprofile --norc -lc")
249249
expect(command).toContain("tmux missing")
250-
expect(command).toContain("tmux new-session -d -s")
250+
expect(command).toContain("tmux start-server")
251+
expect(command).toContain("set-option -g history-limit 50000")
252+
expect(command).toContain("new-session -d -s")
251253
expect(command).toContain("tmux set-option")
252254
expect(command).toContain("status off")
255+
expect(command).toContain("history-limit 50000")
256+
expect(command).toContain("mouse on")
253257
expect(command).toContain("tmux attach-session -t")
254258
expect(command).toContain("docker-git-session-1")
255259
expect(command).toContain("/home/dev/project with spaces")

packages/app/src/docker-git/api-client-auth.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ import type {
2525
AuthGithubLoginCommand,
2626
AuthGithubLogoutCommand,
2727
AuthGithubStatusCommand,
28-
AuthGrokLogoutCommand,
29-
AuthGrokStatusCommand,
3028
AuthGitlabLoginCommand,
3129
AuthGitlabLogoutCommand,
32-
AuthGitlabStatusCommand
30+
AuthGitlabStatusCommand,
31+
AuthGrokLogoutCommand,
32+
AuthGrokStatusCommand
3333
} from "./frontend-lib/core/domain.js"
3434
import { resolvePathFromCwd } from "./frontend-lib/usecases/path-helpers.js"
3535
import type { ApiAuthRequiredError, ApiRequestError } from "./host-errors.js"

packages/app/src/docker-git/api-client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ export {
3131
githubLogin,
3232
githubLogout,
3333
githubStatus,
34-
grokLogout,
35-
grokStatus,
3634
gitlabLogin,
3735
gitlabLogout,
38-
gitlabStatus
36+
gitlabStatus,
37+
grokLogout,
38+
grokStatus
3939
} from "./api-client-auth.js"
4040
export {
4141
type ApiContainerTask,

packages/app/src/docker-git/program-auth.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Effect, Match, pipe } from "effect"
22

33
import {
4+
type ApiTerminalSession,
45
codexImport,
56
codexLogin,
67
codexLogout,
@@ -9,19 +10,19 @@ import {
910
githubLogin,
1011
githubLogout,
1112
githubStatus,
12-
grokLogout,
13-
grokStatus,
1413
gitlabLogin,
1514
gitlabLogout,
1615
gitlabStatus,
16+
grokLogout,
17+
grokStatus,
1718
type JsonValue,
1819
renderJsonPayload
1920
} from "./api-client.js"
2021
import { type ControllerRuntime, ensureControllerReady } from "./controller.js"
2122
import type { Command } from "./frontend-lib/core/domain.js"
22-
import type { ApiRequestError, CliError } from "./host-errors.js"
23+
import type { ApiRequestError, CliError, ControllerBootstrapError } from "./host-errors.js"
2324
import { terminalAuthTitle } from "./menu-auth-shared.js"
24-
import { attachTerminalSession } from "./terminal-session-client.js"
25+
import { attachTerminalSession, type TerminalSessionClientError } from "./terminal-session-client.js"
2526

2627
type OperationalCommand = Exclude<Command, { readonly _tag: "Help" }>
2728

@@ -45,7 +46,9 @@ export type RoutedAuthCommand = Extract<
4546
}
4647
>
4748

48-
const withControllerReady = <E, R>(effect: Effect.Effect<void, E, R>) =>
49+
const withControllerReady = <E extends CliError, R>(
50+
effect: Effect.Effect<void, E, R>
51+
): Effect.Effect<void, E | ControllerBootstrapError, R | ControllerRuntime> =>
4952
pipe(ensureControllerReady(), Effect.zipRight(effect))
5053

5154
const renderAuthPayload = (payload: JsonValue) => Effect.log(renderJsonPayload(payload))
@@ -57,6 +60,17 @@ const missingAuthTerminalSessionError = (provider: "GrokOauth"): ApiRequestError
5760
message: `Controller did not create a terminal session for ${provider}.`
5861
})
5962

63+
const attachGrokTerminalSession = (
64+
session: ApiTerminalSession | null
65+
): Effect.Effect<void, ApiRequestError | TerminalSessionClientError> =>
66+
session === null
67+
? Effect.fail(missingAuthTerminalSessionError("GrokOauth"))
68+
: attachTerminalSession({
69+
header: terminalAuthTitle("GrokOauth"),
70+
session,
71+
websocketPath: `/auth/terminal-sessions/${encodeURIComponent(session.id)}/ws`
72+
})
73+
6074
const routedAuthTags: Readonly<Record<string, true>> = {
6175
AuthCodexImport: true,
6276
AuthCodexLogin: true,
@@ -111,15 +125,7 @@ const handleGrokLoginCommand = (
111125
) =>
112126
withControllerReady(
113127
createAuthTerminalSession("GrokOauth", command.label).pipe(
114-
Effect.flatMap((session) =>
115-
session === null
116-
? Effect.fail(missingAuthTerminalSessionError("GrokOauth"))
117-
: attachTerminalSession({
118-
header: terminalAuthTitle("GrokOauth"),
119-
session,
120-
websocketPath: `/auth/terminal-sessions/${encodeURIComponent(session.id)}/ws`
121-
})
122-
)
128+
Effect.flatMap((session) => attachGrokTerminalSession(session))
123129
)
124130
)
125131

packages/app/src/web/app-ready-terminal-screen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import type { ReadyLayoutProps } from "./app-ready-layout.js"
77
import { Box, Text } from "./elements.js"
88
import { TaskPanel } from "./panel-tasks.js"
99
import { TerminalPanel } from "./panel-terminal.js"
10-
import type { TerminalExitInfo } from "./terminal-panel-runtime-types.js"
1110
import { type BrowserScreen, projectPickerScreen } from "./screen.js"
1211
import { shouldShowTerminalTabs } from "./terminal-mobile-layout.js"
12+
import type { TerminalExitInfo } from "./terminal-panel-runtime-types.js"
1313
import { terminalSessionId } from "./terminal-state.js"
1414
import { type ActiveTerminalSession, isPendingActiveTerminalSession, terminalTitleById } from "./terminal.js"
1515

@@ -521,6 +521,7 @@ export const TerminalScreen = (props: TerminalScreenProps): JSX.Element | null =
521521
<TerminalPane
522522
key={terminalSessionId(activeSession)}
523523
onApplyProjectById={props.onApplyProjectById}
524+
onAuthTerminalExitSuccess={props.onAuthTerminalExitSuccess}
524525
onCloseTaskManager={() => {
525526
setTerminalView("terminal")
526527
}}

packages/app/src/web/panel-terminal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import "xterm/css/xterm.css"
33
import { type CSSProperties, type JSX, useCallback, useEffect, useRef, useState } from "react"
44

55
import {
6-
type TerminalExitInfo,
76
isModifierOnlyTerminalKey,
87
type MobileTerminalKey,
98
mobileTerminalKeyInput,
@@ -12,6 +11,7 @@ import {
1211
import { resolveTerminalCompactHeaderMode, resolveTerminalTypingMode } from "./terminal-mobile-layout.js"
1312
import {
1413
type TerminalConnectionState,
14+
type TerminalExitInfo,
1515
type TerminalInputController,
1616
type TerminalStatus,
1717
useTerminalSessionLifecycle

packages/app/src/web/terminal-panel-runtime-core.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,18 @@ import type {
3030
TerminalSocketListenerArgs,
3131
TerminalSocketRef
3232
} from "./terminal-panel-runtime-types.js"
33-
import { installTerminalQuerySuppression } from "./terminal-query-suppression.js"
33+
import { installTerminalQuerySuppression, type TerminalQuerySuppressionOptions } from "./terminal-query-suppression.js"
3434
import { resolveTerminalReconnectDelay, terminalReconnectGraceMs } from "./terminal-reconnect.js"
3535
import { parseTerminalServerMessage, resolveTerminalWebSocketUrl } from "./terminal.js"
3636

3737
type TerminalClientMessage =
3838
| { readonly data: string; readonly type: "input" }
3939
| { readonly cols: number; readonly rows: number; readonly type: "resize" }
4040

41+
type TerminalRuntimeOptions = {
42+
readonly querySuppression?: TerminalQuerySuppressionOptions
43+
}
44+
4145
type TerminalInlineImageFetchError = {
4246
readonly _tag: "TerminalInlineImageFetchError"
4347
readonly message: string
@@ -76,16 +80,20 @@ const clearReconnectTimer = (lifecycle: TerminalLifecycleState): void => {
7680
}
7781
}
7882

79-
export const createTerminalRuntime = (host: HTMLDivElement): TerminalRuntime => {
83+
export const createTerminalRuntime = (
84+
host: HTMLDivElement,
85+
options: TerminalRuntimeOptions = {}
86+
): TerminalRuntime => {
8087
const terminal = new Terminal({
8188
allowProposedApi: true,
8289
convertEol: false,
8390
cursorBlink: true,
8491
fontFamily: "'IBM Plex Mono', 'SFMono-Regular', monospace",
8592
fontSize: 14,
93+
scrollback: 50_000,
8694
theme: { background: "#080a0d", foreground: "#f4f7fb" }
8795
})
88-
installTerminalQuerySuppression(terminal)
96+
installTerminalQuerySuppression(terminal, options.querySuppression)
8997
const fitAddon = new FitAddon()
9098
terminal.loadAddon(fitAddon)
9199
terminal.open(host)

0 commit comments

Comments
 (0)