Skip to content

Commit f0feec8

Browse files
committed
feat(terminal): extract web terminal runtime
Move browser terminal UI/runtime modules and tests into packages/terminal, keep app compatibility facades, and preserve agent TTY raw mode by skipping shell recovery under Claude/Codex-style agent ancestors. Verification: bun run check; bun run test; bun run build; git diff --check; Playwright Claude Code smoke test with PTY remaining -icanon -echo after tool calls.
1 parent 8a6c018 commit f0feec8

76 files changed

Lines changed: 4421 additions & 4077 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bun.lock

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@
8383
"react": "^19.2.6",
8484
"react-dom": "^19.2.6",
8585
"react-reconciler": "^0.33.0",
86-
"ts-morph": "^28.0.0",
87-
"xterm": "^5.3.0",
88-
"xterm-addon-fit": "^0.8.0"
86+
"ts-morph": "^28.0.0"
8987
},
9088
"devDependencies": {
9189
"@biomejs/biome": "^2.4.16",

packages/app/src/lib/core/templates-prompt.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,65 @@ const dockerGitTerminalSanitizeShell = String.raw`docker_git_terminal_write_esca
2121
fi
2222
return 1
2323
}
24+
docker_git_terminal_process_args() {
25+
ps -o args= -p "$1" 2>/dev/null || true
26+
}
27+
docker_git_terminal_parent_pid() {
28+
ps -o ppid= -p "$1" 2>/dev/null | tr -d '[:space:]'
29+
}
30+
docker_git_terminal_command_basename() {
31+
local command_line="$1"
32+
printf "%s\n" "$command_line" | awk '{ name = $1; sub(/^.*\//, "", name); print name; exit }'
33+
}
34+
docker_git_terminal_is_agent_command() {
35+
local command_name
36+
command_name="$(docker_git_terminal_command_basename "$1")"
37+
case "$command_name" in
38+
.docker-git-claude-real|claude|codex|opencode|gemini|grok)
39+
return 0
40+
;;
41+
*)
42+
return 1
43+
;;
44+
esac
45+
}
46+
docker_git_terminal_has_agent_ancestor() {
47+
local pid="$1"
48+
local depth=0
49+
local command_line=""
50+
local parent_pid=""
51+
if [ -z "$pid" ]; then
52+
pid="$$"
53+
fi
54+
while [ -n "$pid" ] && [ "$pid" != "0" ] && [ "$depth" -lt 32 ]; do
55+
command_line="$(docker_git_terminal_process_args "$pid")"
56+
if docker_git_terminal_is_agent_command "$command_line"; then
57+
return 0
58+
fi
59+
parent_pid="$(docker_git_terminal_parent_pid "$pid")"
60+
if [ -z "$parent_pid" ] || [ "$parent_pid" = "$pid" ]; then
61+
return 1
62+
fi
63+
pid="$parent_pid"
64+
depth=$((depth + 1))
65+
done
66+
return 1
67+
}
68+
docker_git_terminal_should_sanitize() {
69+
if [ -n "$(printenv DOCKER_GIT_TERMINAL_FORCE_SANITIZE 2>/dev/null)" ]; then
70+
return 0
71+
fi
72+
if [ -n "$(printenv DOCKER_GIT_TERMINAL_DISABLE_SANITIZE 2>/dev/null)" ]; then
73+
return 1
74+
fi
75+
if docker_git_terminal_has_agent_ancestor "$$"; then
76+
return 1
77+
fi
78+
return 0
79+
}
2480
docker_git_terminal_sanitize() {
2581
# Recover interactive TTY settings after abrupt exits from fullscreen/raw-mode tools.
82+
docker_git_terminal_should_sanitize || return 0
2683
if [ -c /dev/tty ]; then
2784
{ stty sane < /dev/tty > /dev/tty; } 2>/dev/null || { stty sane < /dev/tty; } 2>/dev/null || true
2885
elif [ -t 0 ]; then
@@ -108,7 +165,7 @@ else
108165
PROMPT_COMMAND="docker_git_prompt_apply"
109166
fi
110167
docker_git_terminal_sanitize
111-
trap 'docker_git_terminal_sanitize' EXIT INT TERM`
168+
trap 'docker_git_terminal_sanitize' EXIT`
112169

113170
export const renderPromptScript = (): string => dockerGitPromptScript
114171

packages/app/src/lib/core/templates-zsh.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -100,16 +100,6 @@ docker_git_terminal_sanitize
100100
add-zsh-hook precmd docker_git_prompt_apply
101101
add-zsh-hook zshexit docker_git_terminal_on_exit
102102
103-
TRAPINT() {
104-
docker_git_terminal_sanitize
105-
return 130
106-
}
107-
108-
TRAPTERM() {
109-
docker_git_terminal_sanitize
110-
return 143
111-
}
112-
113103
HISTFILE="\${HISTFILE:-$HOME/.zsh_history}"
114104
HISTSIZE="\${HISTSIZE:-10000}"
115105
SAVEHIST="\${SAVEHIST:-20000}"
Lines changed: 1 addition & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -1,189 +1 @@
1-
import type { JSX } from "react"
2-
3-
import {
4-
closeButtonStyle,
5-
compactCloseButtonStyle,
6-
compactHeaderActionsStyle,
7-
compactHeaderStyle,
8-
compactHeaderTitleStyle,
9-
compactStatusStyle,
10-
headerActionsStyle,
11-
headerStatusStyle,
12-
headerStyle,
13-
headerSubtitleStyle,
14-
headerTitleStyle
15-
} from "./panel-terminal-styles.js"
16-
import type { TerminalPanelProps } from "./panel-terminal-types.js"
17-
import type { TerminalStatus } from "./terminal-panel-runtime.js"
18-
19-
type TerminalHeaderProps =
20-
& Pick<
21-
TerminalPanelProps,
22-
| "onApplyProject"
23-
| "onDetach"
24-
| "onKill"
25-
| "onOpenBrowser"
26-
| "onOpenSkiller"
27-
| "onOpenTaskManager"
28-
| "onOpenTerminal"
29-
| "session"
30-
>
31-
& {
32-
readonly compactHeaderMode: boolean
33-
readonly inlineImagePreviewsEnabled: boolean
34-
readonly onToggleInlineImagePreviews: () => void
35-
readonly status: TerminalStatus
36-
}
37-
38-
type TerminalActionDescriptor = {
39-
readonly compactLabel: string
40-
readonly label: string
41-
readonly onClick: (() => void) | undefined
42-
}
43-
44-
const TerminalHeaderTitle = (
45-
{
46-
compactHeaderMode,
47-
session,
48-
status
49-
}: Pick<TerminalPanelProps, "session"> & {
50-
readonly compactHeaderMode: boolean
51-
readonly status: TerminalStatus
52-
}
53-
): JSX.Element =>
54-
compactHeaderMode
55-
? (
56-
<div style={{ alignItems: "center", display: "flex", gap: "6px", minWidth: 0 }}>
57-
<div style={compactHeaderTitleStyle}>
58-
{session.browserProjectName ?? session.header}
59-
</div>
60-
<div style={compactStatusStyle(status)}>{status}</div>
61-
</div>
62-
)
63-
: (
64-
<div style={{ display: "flex", flexDirection: "column", gap: "4px", minWidth: 0, width: "100%" }}>
65-
<div style={headerTitleStyle}>{session.header}</div>
66-
<div style={headerStatusStyle(status)}>{status}</div>
67-
<div style={headerSubtitleStyle}>{session.subtitle}</div>
68-
</div>
69-
)
70-
71-
const TerminalActionButton = (
72-
{
73-
children,
74-
compactTypingMode,
75-
onClick,
76-
pressed,
77-
title
78-
}: {
79-
readonly children: string
80-
readonly compactTypingMode: boolean
81-
readonly onClick: () => void
82-
readonly pressed?: boolean
83-
readonly title?: string
84-
}
85-
): JSX.Element => (
86-
<button
87-
aria-pressed={pressed}
88-
onClick={onClick}
89-
style={compactTypingMode ? compactCloseButtonStyle : closeButtonStyle}
90-
title={title}
91-
type="button"
92-
>
93-
{children}
94-
</button>
95-
)
96-
97-
const optionalProjectActions = (
98-
props: TerminalHeaderProps
99-
): ReadonlyArray<TerminalActionDescriptor> => {
100-
if (props.session.browserProjectId === undefined) {
101-
return []
102-
}
103-
return [
104-
{ compactLabel: "Browser", label: "Open browser", onClick: props.onOpenBrowser },
105-
{ compactLabel: "Skiller", label: "Skiller", onClick: props.onOpenSkiller },
106-
{ compactLabel: "Apply", label: "Apply", onClick: props.onApplyProject },
107-
{ compactLabel: "Tasks", label: "Task manager", onClick: props.onOpenTaskManager },
108-
{ compactLabel: "New", label: "New terminal", onClick: props.onOpenTerminal }
109-
]
110-
}
111-
112-
const TerminalProjectActionButtons = (
113-
{
114-
actions,
115-
compactHeaderMode
116-
}: {
117-
readonly actions: ReadonlyArray<TerminalActionDescriptor>
118-
readonly compactHeaderMode: boolean
119-
}
120-
): JSX.Element => (
121-
<>
122-
{actions.map((action) =>
123-
action.onClick === undefined
124-
? null
125-
: (
126-
<TerminalActionButton
127-
key={action.label}
128-
compactTypingMode={compactHeaderMode}
129-
onClick={action.onClick}
130-
>
131-
{compactHeaderMode ? action.compactLabel : action.label}
132-
</TerminalActionButton>
133-
)
134-
)}
135-
</>
136-
)
137-
138-
const TerminalImageToggleButton = (
139-
{
140-
compactHeaderMode,
141-
inlineImagePreviewsEnabled,
142-
onToggleInlineImagePreviews
143-
}: Pick<TerminalHeaderProps, "compactHeaderMode" | "inlineImagePreviewsEnabled" | "onToggleInlineImagePreviews">
144-
): JSX.Element => {
145-
const label = inlineImagePreviewsEnabled ? "Images on" : "Images off"
146-
const compactLabel = inlineImagePreviewsEnabled ? "Img on" : "Img off"
147-
const title = inlineImagePreviewsEnabled
148-
? "Automatic image previews enabled"
149-
: "Automatic image previews disabled"
150-
151-
return (
152-
<TerminalActionButton
153-
compactTypingMode={compactHeaderMode}
154-
onClick={onToggleInlineImagePreviews}
155-
pressed={inlineImagePreviewsEnabled}
156-
title={title}
157-
>
158-
{compactHeaderMode ? compactLabel : label}
159-
</TerminalActionButton>
160-
)
161-
}
162-
163-
const TerminalHeaderActions = (props: TerminalHeaderProps): JSX.Element => (
164-
<div style={props.compactHeaderMode ? compactHeaderActionsStyle : headerActionsStyle}>
165-
<TerminalProjectActionButtons actions={optionalProjectActions(props)} compactHeaderMode={props.compactHeaderMode} />
166-
<TerminalImageToggleButton
167-
compactHeaderMode={props.compactHeaderMode}
168-
inlineImagePreviewsEnabled={props.inlineImagePreviewsEnabled}
169-
onToggleInlineImagePreviews={props.onToggleInlineImagePreviews}
170-
/>
171-
<TerminalActionButton compactTypingMode={props.compactHeaderMode} onClick={props.onDetach}>
172-
Detach
173-
</TerminalActionButton>
174-
<TerminalActionButton compactTypingMode={props.compactHeaderMode} onClick={props.onKill}>
175-
Kill
176-
</TerminalActionButton>
177-
</div>
178-
)
179-
180-
export const TerminalHeader = (props: TerminalHeaderProps): JSX.Element => (
181-
<div style={props.compactHeaderMode ? compactHeaderStyle : headerStyle}>
182-
<TerminalHeaderTitle
183-
compactHeaderMode={props.compactHeaderMode}
184-
session={props.session}
185-
status={props.status}
186-
/>
187-
<TerminalHeaderActions {...props} />
188-
</div>
189-
)
1+
export * from "@prover-coder-ai/docker-git-terminal/web/panel-terminal-header"

0 commit comments

Comments
 (0)