Skip to content

feat(terminal): interactive PTY shell in the embedded terminal - #406

Merged
yuga-hashimoto merged 3 commits into
mainfrom
claude/github-issues-review-08495d
Oct 27, 2026
Merged

yuga-hashimoto merged 3 commits into
mainfrom
claude/github-issues-review-08495d

Conversation

@yuga-hashimoto

Copy link
Copy Markdown
Owner

Summary

Closes #329. The embedded terminal is now an interactive shell on a real PTY instead of one bounded sh -lc per line.

Before: every submitted line ran as its own sh -lc that was force-killed after 30 s, only the last 4,000 characters of output were read back, there was no stdin / streaming / Ctrl+C, and it always started in /root regardless of the workspace it was opened from.

Now: one shell stays alive for as long as the terminal screen is open.

  • PTY (LocalTerminalLauncher): the sandbox's /usr/bin/script -qefc allocates the pty — the same mechanism the Claude Code and Antigravity sign-in flows already use. The shell gets a real controlling terminal, so line editing, password prompts (no echo) and Ctrl+C (the line discipline turns 0x03 into SIGINT for the foreground job) behave. script reports a 0x0 window when fed pipes, so stty rows/cols sets the size the screen measured before the shell starts (clamped to the same bounds the emulator uses).
  • Streaming output (ProcessTerminalSession): read as it is produced, batched per burst; no timeout and no 4 KB cap.
  • ANSI rendering (AnsiTerminalEmulator, plain Kotlin): a line-oriented VT100/xterm subset — SGR colours (16 / 256 / truecolour, bold, italic, underline, inverse), \r rewrites (progress bars), \b, tabs, erase line/display, cursor movement, insert/delete characters, clear, auto-wrap, and 10,000 lines of scrollback. Sequences it does not know are consumed, never printed. The stream is treated as untrusted: numeric parameters are clamped, sequences with invalid parameters are ignored, CAN/SUB abort a sequence in progress, and the style table is bounded.
  • Input (TerminalScreen): the typed line is sent with Enter (an empty line too); Esc, Tab, ^C, ^D and the arrow keys have their own key row that always fits the screen width. Tab and the arrows send the text still in the input field first, since they act on the shell's line editor. Output is followed until the reader scrolls away by hand, and again when they scroll back, type, press a key or clear.
  • Starts in the workspace it was opened from (terminal?directory=…, falling back to /root if cd fails); the workspace folder is bound at /workspace.
  • Lifecycle: leaving the screen ends the shell and everything it started — killProcessTree (renamed from killAntigravityProcessTree, which is not Antigravity specific) walks /proc and kills the guest tree, because killing the proot root alone leaves script/bash and any dev server running (see the on-device finding in ProcessTree.kt). A RuntimeWorkTracker lease is held only while the shell is alive so the device does not suspend under a build or dev server. The runtime access lock is held only while the process is spawned — the lock is fair, so keeping it for a shell that may stay open for hours would make the next runtime update (and every command queued behind it) wait for the user to close the terminal.
  • 12 new strings in all 8 locales (the two unused terminal_banner / terminal_exit_code strings are removed from all of them); README and docs/LOCAL_RUNTIME.md updated.

Not in this PR

  • Full-screen programs (vim, htop): no alternate screen, scroll region or mouse. The cursor-position query (ESC[6n) is not answered.
  • Resizing an already-open shell: the size is fixed when it starts, so rotating the device does not re-wrap it. (A resize would need stty against the pty slave from a second process.)
  • Double-width (CJK) characters are one cell wide, so readline editing of Japanese input can mis-position the cursor. Written down under "known limits" in the docs.
  • Mouse-wheel / TalkBack scrolling does not count as "the reader scrolled away" for output following (only a touch drag does). Judging by canScrollForward at the wrong moment is what broke following when the keyboard opened, so it is deliberately not generalised blindly.
  • The unfinished multi-terminal tab placeholder in the workspace explorer (TerminalTabPlaceholder) — mentioned in the issue as context, not as an expectation.
  • A runtime update or delete while a terminal is open is not coordinated with it, and an OpenCode runtime stop/restart ends the terminal's shell too (it kills every proot under the runtime directory) — the screen then shows "Session ended" with a Restart button. Documented.

Test plan

  • ./gradlew spotlessCheck :app:testGithubDebugUnitTest :app:lintGithubDebug detekt — green on the final commit: 1562 unit tests, 0 failures (8 skipped = pre-existing live OpenCodeV2LiveTest), lint passes. Note the root detekt task reports NO-SOURCE in this repo, so it checks nothing.
  • New unit tests: AnsiTerminalEmulatorTest (36: wrap/deferred wrap, \r, erase/insert/delete, cursor clamping, colours incl. sequences split across chunks, scrollback cap, immutable snapshots, overflow/negative/CAN/style-bound hardening), LocalTerminalLauncherTest (8: command shape, stty size and bounds, cd quoting/injection, relative path ignored), ProcessTerminalSessionTest (14, against a real sh over pipes: streaming, input order, exit code, UTF-8 both ways, close ends the tree before the root, a failing tree walk still destroys the root, close does not block, an ended shell is not walked), TerminalViewModelTest (24: size handshake and clamping, streaming, submit/keys incl. flushing typed text, drain-before-exit, lease acquire/release, close, a shell opened after close is ended, failures, restart, clear), RoutesTest (+2: terminal route round trip).
  • Mutation-checked: removing the CSI parameter clamp fails 2 emulator tests; reverting the output-following logic to "can scroll forward" fails 3 instrumented tests.
  • New instrumented TerminalScreenInstrumentedTest (real TerminalScreen, 12 cases: reports a usable measured size, draws output, key sequences, typed input, disabled until running, ended / not-installed messaging, restart, clear, follows output, keeps following when the keyboard shrinks the viewport, stops when the reader scrolls up and resumes at the end, input returns to the end) — 12/12 on an API 36 emulator. Also looked at a screenshot of the screen driven by real emulator output (colours, bold, inverse, underline, \r\e[2K rewrite, Japanese text, block cursor, 54 columns measured on a 411 dp-wide display); that surfaced the key row overflowing the width, fixed here.
  • i18n key parity: same check as .github/workflows/i18n-check.yml (ported from GNU grep -P), 1152 required keys, all 7 translations complete.
  • The pipes-in / pty-out mechanism checked by hand with macOS script: input is echoed, ^C interrupts a foreground sleep 30 and the shell survives, and the pty reports 0 rows; 0 columns unless stty sets it — the reason for the stty in the launcher.
  • Not tried against the real proot/Alpine runtime on a device. The Linux environment was not installed on the emulator (it is a large download), so script -qefc … bash -l inside proot, Ctrl+C through it, and the whole-tree kill on leaving the screen are exercised only through the unit tests (a real sh over pipes, the tree kill behind an injected function) and the macOS check above. Worth a quick manual pass: open a workspace terminal, run pwd, read -p x y, sleep 100 + ^C, start a dev server, then leave the screen and confirm with ps from another terminal that nothing is left.
  • Soft-keyboard IME composition (Japanese) into the input field not tried by hand.

Pre-PR review

The named repo-reviewer subagent (.opencode/agents/repo-reviewer.md) is not registered in Claude Code, so the review was run by a read-only general-purpose subagent given that definition. Two rounds:

  1. Commit e4b236f8 → REQUEST_CHANGES with two blockers (the shell's process tree was not ended on close; an absurd CSI parameter overflowed and crashed the app). Both are fixed in 106fc24d, together with the suggestions that mattered (a shell opened after the screen was closed leaked; emulator/pty size bounds; Tab/arrows vs typed text; output following broke when the keyboard opened; publish ordering; dead strings; docs).
  2. Commit 106fc24d → APPROVE (report below, verbatim).

Commit 4c917a06 was added after the approval and applies only suggestions from that report (no walk of an already-ended shell's tree, snapshot() inside the safety net, negative CSI parameters ignored, input/keys/clear return to the end of the output, docs). It was not re-reviewed.

判定: APPROVE

(再レビュー。範囲は `git diff origin/main...HEAD` 全体で、前回レビュー時点 e4b236f8 に修正コミット 106fc24d を加えたもの。merge-base は origin/main の先頭 414c7806 と一致。106fc24d の全 hunk を読み、周辺コードと突き合わせた。Gradle やテストは実行していない。)

前回のブロッカー 2 件は解消している。下の提案はいずれも新たなブロッカーではない。

## ブロッカー
なし

## 提案(非ブロッキング)
1. app/src/main/java/com/yugahashimoto/andcode/runtime/local/ProcessTerminalSession.kt:100-119 — シェルが自然終了した場合も、`TerminalViewModel.run()` の finally(:146-149)が必ず `opened.close()` を呼ぶ。その結果、`killProcessTree` が回収済み(reap 済み)の pid に対して走る。
   - 影響: 子孫は init に付け替え済みなので `/proc/<pid>/task/<pid>/children` が無く、`readDirectChildPids` のフォールバックで /proc 全走査(PPid 一致)になる。pid が再利用されていれば、無関係なプロセス木(同 uid の OpenCode ランタイムの proot など)を SIGKILL しうる。
   - 発生条件: 回収から close までは数 ms しかなく、実際に起きる確率は無視できる。ただしこれは全シェル終了で通る常用パスなので、ガードは安く済む。
   - 修正案: `if (process.isAlive) killTree(process)` を try の中に置く(`AntigravityAuthCoordinator.terminateAsync` が同じ形)。`destroyForcibly()` は finally で無条件のままにする。「終了済みなら killTree を呼ばない」テストを 1 件追加する。
2. app/src/main/java/com/yugahashimoto/andcode/feature/workspace/TerminalViewModel.kt:178-191 — `publish` の安全網で `try` が囲むのは `terminal.change()` だけで、`terminal.snapshot()`(:187。`Row.build` を通る)は外にある。feed が途中で例外になった場合、`feed()` 末尾の `revision++` が飛ぶだけで済むため実害は小さいが、snapshot まで try に入れると網が完全になる。この安全網自体のテストはない(エミュレータが注入不可のため)。パラメータ丸めによりクラッシュ経路は塞がれているので、優先度は低い。
3. app/src/main/java/com/yugahashimoto/andcode/feature/workspace/TerminalScreen.kt:178-193 — 追従ロジックの限界が 2 点ある。IME 縮小時の追従外れという前回の不具合は解消している(`LaunchedEffect` のキーに `constraints.maxHeight` を追加し、追従解除の判定を「ドラッグ終了」に限定した)。
   - (a) 追従の解除・再開は `DragInteraction.Start` を見ているので、ドラッグ以外のユーザースクロールでは `followOutput` が更新されない。具体的には、マウスホイール/トラックパッド(DeX、ChromeOS)、TalkBack のスクロール操作、ハードウェアキーの PageUp。これらで履歴を遡っても、次の出力で末尾へ引き戻される。旧ロジック(`canScrollForward` 判定)では守れていた操作なので、アクセシビリティ面でわずかに後退している。
     - 修正案: 自前の `scrollToItem` を囲む `programmatic` フラグを持ち、`isScrollInProgress` が true→false になったとき、programmatic でなければ `followOutput = !canScrollForward` とする(ドラッグ限定をやめる)。
   - (b) 遡って読んだ後に送信・キー送信・クリアをしても追従が再開しない。Termux などは入力で最下部へ戻る。`clear()` で行数が 1 に縮んだ後も、`followOutput == false` なら次の出力が追従されない。
     - 修正案: `onSubmit`/`onKey`/`onClear` で `followOutput = true` にする(状態を TerminalScreen に引き上げる)。
4. TerminalViewModel.kt:178-191,227 — `publish` は最大 64KB の feed 中ずっと `lock` を保持する。Main スレッドから呼ばれる `runningSession()`(submit/sendKey)・`clear()`・`close()` が数 ms 待たされうる。デッドロックはない(Java モニターは再入可能で、`_state.update` の collector 再開が Main 上でインライン実行されても同スレッドなので問題ない)。重い出力中に入力が引っかかるなら、feed を lock 外に出し、シーケンス番号で順序保証する案がある。現状は許容範囲。
5. docs/LOCAL_RUNTIME.md「入力」行(:233 付近)— Tab/矢印は入力欄の文字を先に送る挙動(`TerminalKey.editsLine`)が追記されていない。また、instrumented テストは `Box.height(640.dp)` 固定なので、画面高が 640dp 未満の端末では `assertIsDisplayed` が不安定になりうる(API 36 エミュレータでは問題なし)。どちらも軽微。
6. 軽微な挙動変化: パラメータを `coerceIn(0, 9999)` にしたため、`ESC[-1m` のような負値の SGR が 0(リセット)になる(以前は無視)。実害なし。

## チェック済み項目
- **(1) close() と新スレッド**(ProcessTerminalSession.kt:100-119):
  - 順序は、`closed` の CAS で冪等 → `input.shutdownNow()` → daemon スレッド(`terminal-close`)で `killTree(process)` → `finally` で `destroyForcibly()` となっている。呼び出し元は `onCleared` の Main スレッドだが、/proc 走査を含む処理は Main で動かない。
  - 二重 destroy: `killProcessTree` が root の pid に `android.os.Process.killProcess` を送った後に `destroyForcibly()` が走るが、終了済みまたはゾンビへのシグナルは無害。Java 側も `hasExited` で守っている。
  - exit スレッド: 終了後に `input.shutdown()` を呼ぶが、すでに `shutdownNow()` 済みなら no-op。書き込み側は `closed` チェックと `RejectedExecutionException` の握りつぶしで保護されている。
  - 出力側: 以前は孫の `script` がパイプを握って EOF が来ない可能性があった。ツリー kill により EOF が確実に届くようになった。
  - `killTree` が `RuntimeException` でも、`Error` でも、`destroyForcibly()` は実行される(finally)。
  - 例外時の `Log.w` は `isReturnDefaultValues = true` で JVM テストでも安全(build.gradle:208)。
  - 自然終了パスの pid 再利用リスクは提案 1。
- **(1) テスト安定性**(ProcessTerminalSessionTest):
  - 新規 3 件は時間依存が小さい。「ツリー kill が root 破棄より前」は、`killTree` が `isAlive == true` を記録した後に `destroyForcibly()` が走る順序で、`exitCode` 到達後に比較するので happens-before が成立する。
  - 「例外でも root 破棄」は `exitCode` 到達後に `isAlive` を見るので確定的。
  - 「呼び出し元をブロックしない」は閾値 2 秒の壁時計依存だが、threshold は十分に緩い。latch は finally で必ず解放される。
  - 既存の close 系テストは、`close()` の非同期化に合わせて `exitCode` を待ってから `isAlive` を確認する形に直っている(:158-160 のコメントも更新済み)。
  - open ヘルパーの既定 `killTree = {}` により、他のテストは実 `killProcessTree`(android.os.Process 依存)を呼ばない。
- **(2) TerminalViewModel の lock 設計**(TerminalViewModel.kt):
  - `adopt`・`close`・`publish`・`runningSession` はすべて同一の `lock` を使う。lock 内で行うのは代入と emulator 操作と `_state.update`(非ブロッキング)のみ。`candidate.close()`・`ending?.close()` は lock の外で呼ばれる。デッドロック経路はない。
  - `close()` が先でも `adopt` が先でも取りこぼしはない。
    - `adopt` が先: `session` に入った候補を `close()` が閉じる。
    - `close()` が先: `closed` を見た `adopt` が候補を閉じる。
    - `viewModelScope` の cancel が `onCleared()` より先でも、`withContext` が結果を捨てる前に `adopt` が `session` に入れるか、後の `close()` が拾う。
    - テスト `a shell that opens after the screen was closed is ended, not leaked` が、`whileOpening` で open 中に `close()` を呼ぶ形でこれを再現している。
  - `adopt` が投げる `CancellationException` は、`withContext` 経由で `run()` の `catch (cancelled)` に再スローされる。launch は静かにキャンセル完了し、SupervisorJob の親には波及しない。`finally` で lease も解放される。
  - 補足: `close()` は `sessionJob.cancel()` の前に `closed` を立てる。`closed` は戻らない設計で、「画面を閉じる」専用と読める。close() 後の `open()` も、adopt で即 close されるだけで漏れない。
  - `run()` の `finally` による `session = null` は lock 下で、`opened` との参照一致を見る。
  - `open()` の clamp は `LocalTerminalLauncher.MIN/MAX_*` と共通。テスト `sizes the emulator and the pty to the same bounds` で下限・上限が `TerminalOpenRequest` に入ることを確認している。
- **ブロッカー 2(AnsiTerminalEmulator)**:
  - `coerceIn(0, 9999)` 後は、`length + count`(≤ 240+9999)・`column + count`・`cursorColumn + count`・`cursorRow + count`・`position(row=count)` のすべてでオーバーフローしない。`ESC[2147483647@` は `insert(…, 9999, columns)` に落ち、`newLength = min(…, maxLength)`、`moved < 0` となって空白埋めで済む。
  - CAN/SUB は CSI・OSC/DCS の両方で GROUND に戻り、ESCAPE/STRING_ESCAPE では `escape()` が GROUND にして `else` で無視するので問題ない。
  - スタイル上限: `styleIds[next] ?: internStyle(next)` で、既知スタイルは上限後も引ける。上限到達後の新規スタイルは `DEFAULT_STYLE`(0)に落ちる。`style` 変数は実スタイルのまま残るので、後続の相対 SGR は矛盾しない。
  - 新規テスト 4 件は手でトレースして期待値と一致を確認した。
    - 巨大パラメータ: 6 通りの値 × 15 通りの終端文字。
    - `insert` 巨大 count: 桁数 10 で `Z` + 9 空白の 10 文字になる。
    - CAN/SUB: `a\e[31\x18b` → `ab`(span なし)、`\e]0;ti\x1ahi` → `hi`。
    - スタイル上限: 5000 通りの RGB で、最後の行の span が空になる。
- **(3) Screen の追従ロジック**:
  - `LaunchedEffect(listState)` の中で、ドラッグ開始フラグと `snapshotFlow { isScrollInProgress }` が同一 dispatcher 上にあり競合しない。プログラム側の `scrollToItem` は `dragging == false` なので判定を汚さない。
  - ドラッグ中の `scrollToItem` は `MutatorMutex` の優先度でキャンセルされ、`LaunchedEffect` が静かに終わるだけで、終了後に判定される。
  - IME のアニメーションで `maxHeight` が毎フレーム変わっても、軽い再起動で済む。`rememberLazyListState` が保存可能なため、回転でも安定している。
  - 限界は提案 3。
- **(4) 新規テスト**: ViewModel 5 件は `StandardTestDispatcher` 上で決定的(`sessionJob` は `runCurrent` までに代入済み)。RoutesTest の往復テストは Base64URL に `=` が含まれないので `substringAfter` が成立する。instrumented 追加 3 件の大枠は妥当(旧ロジックでの失敗も報告されている)。
- **(5) 回帰**:
  - `killAntigravityProcessTree` / `AntigravityProcessTree` は、リポジトリ全体(.git・build を除く)で grep して参照ゼロ。`AntigravityAuthCoordinator.kt:415`、`AntigravityRuntime.kt:523`、`AntigravityAbortTracker.kt` の KDoc は、すべて `killProcessTree` に更新済み。
  - `ProcessTree.kt` は git の rename(類似度 83%)。シグネチャ `internal fun killProcessTree(process: Process)` は不変で、Antigravity 側の挙動は変わらない。
  - `terminal_banner` / `terminal_exit_code` は全 8 ロケールで 2 行ずつ削除され(各ファイルの diff を確認)、`<!-- Terminal -->` の周辺も崩れていない。リポジトリ全体(app・docs・README・scripts)で参照ゼロ。
- **i18n(前回確認分は維持)**: 追加 12 キーは全 8 ロケールに存在したままで、削除は無関係のキーに及んでいない。キー列の `Esc`/`Tab` は「キーキャップ表記で非翻訳」とコメントで明示された(TerminalScreen.kt:219-221)。
- **ナビ/DI**: `viewModel(key = "terminal")` に変更され、コメントも実態(エントリごとのスコープ)に修正された。Koin 側に `TerminalViewModel` の経路がないことは前回確認のとおり。
- **docs**: LOCAL_RUNTIME.md のライフサイクル・電力・ランタイム停止との関係・既知の制限の記述は、実装と整合している。ツリー kill の説明は `ProcessTree.kt` の実機検証メモと一致する。
- **ハードルール**: 1(i18n)は上記のとおり。3(テスト)は新規テストが追加されている。6 は merge-base が origin/main の先頭のまま。2(spotless/detekt)は実行済み申告を前提とした。

(主な確認対象: app/src/main/java/com/yugahashimoto/andcode/{runtime/local/ProcessTerminalSession.kt, runtime/local/ProcessTree.kt, feature/workspace/TerminalViewModel.kt, feature/workspace/TerminalScreen.kt, feature/terminal/AnsiTerminalEmulator.kt, feature/terminal/TerminalModel.kt, ui/navigation/WorkspaceNavGraph.kt}、各テストファイル、docs/LOCAL_RUNTIME.md)

🤖 Generated with Claude Code

yuga-hashimoto and others added 3 commits October 27, 2026 23:11
The terminal ran each line as its own `sh -lc`, killed it after 30 s and
kept only the last 4,000 characters of output, so a dev server, a log
follower or an interactive prompt could not be used. It also always
started in /root, whatever workspace it was opened from (#329).

It now keeps one shell alive for as long as the screen is open:

- PTY: the sandbox's `script -qefc` allocates the pty (the same trick the
  Claude Code and Antigravity sign-in flows use), so the shell has a real
  controlling terminal - line editing, password prompts and Ctrl+C work.
  `stty` sets the size the screen measured, since `script` otherwise
  reports 0x0 over pipes.
- Streaming: output is read as it is produced and interpreted by
  AnsiTerminalEmulator, a line-oriented VT100/xterm subset (SGR colours,
  \r rewrites, line editing, clear, wrapping, 10,000 lines of
  scrollback). Unknown sequences are dropped instead of printed.
- Input: the typed line is sent with Enter; Esc, Tab, Ctrl+C, Ctrl+D and
  the arrow keys have their own key row.
- Starts in the workspace it was opened from, falling back to /root; the
  workspace folder is bound at /workspace.
- A runtime work lease is held while the shell lives so the device does
  not suspend under a build. The runtime access lock is held only while
  the process is spawned, so a long-lived shell cannot stall an update.
- The shell and anything it started end when the screen is left.

Full-screen programs (vim, htop), resizing an open shell and the
multi-terminal tab placeholder are not part of this change.

Closes #329

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ut parser

Pre-PR review of the interactive terminal found two blockers and several
smaller problems.

Blockers:
- Closing the screen only called destroyForcibly() on proot. A proot guest
  outlives that (the repo's own on-device finding, ProcessTree.kt): the
  `script`/`bash` and any dev server stayed running while the runtime lease
  was released. close() now walks /proc and kills the tree end first
  (killProcessTree, renamed from killAntigravityProcessTree since it is not
  Antigravity specific) on a background thread, then destroys the root even
  if the walk failed.
- A CSI parameter such as ESC[2147483647@ overflowed Int arithmetic in the
  emulator and threw out of the collector, crashing the app from any
  `cat` of a hostile file. Numeric parameters are clamped to 9,999, a
  feed() failure is logged instead of propagated, CAN/SUB abort an escape
  sequence in progress, and the interned style table is bounded.

Also fixed:
- A shell that finished opening after the screen was closed was never
  owned by anyone and kept running; it is now ended on arrival.
- The emulator was sized from the raw measured width while the pty was
  clamped to 20..240 columns; both use the same bounds now.
- Tab and the arrow keys act on the shell's line editor, so text still in
  the input field is sent first.
- Output following stopped when the soft keyboard shrank the viewport (it
  judged "at the end" by canScrollForward at that moment). It now follows
  until the reader drags away and resumes when they drag back to the end.
- State publishes happen under the emulator lock so a late snapshot cannot
  bring back text that `clear` removed.
- Removed the now unused terminal_banner / terminal_exit_code strings from
  all locales; documented the remaining limits (CJK width, OpenCode
  restarts ending the shell, the lease being held while a shell is open).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- close() no longer walks the process tree of a shell that already ended:
  its pid has been reaped and may belong to something else.
- The output safety net now also covers snapshot(), and a CSI sequence
  that carries a negative parameter is ignored whole (ESC[-1m used to be
  read as ESC[0m and reset the colours).
- Typing, pressing a key and clearing bring the view back to the end of
  the output after the reader scrolled away. Scrolling away by hand still
  stops the following; mouse-wheel and TalkBack scrolling do not, which
  is written down in the docs rather than guessed at.
- Documented that Tab and the arrow keys send the pending input first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@yuga-hashimoto
yuga-hashimoto merged commit 5432d68 into main Oct 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Embedded terminal is not a real terminal: no PTY, 30s hard timeout, 4KB output cap

1 participant