Skip to content

fix(desktop): v1 terminal typing dead after tab switch when cold restore cached#167

Merged
MocA-Love merged 2 commits intomainfrom
fix/v1-terminal-cold-restore-cache-desync
Apr 15, 2026
Merged

fix(desktop): v1 terminal typing dead after tab switch when cold restore cached#167
MocA-Love merged 2 commits intomainfrom
fix/v1-terminal-cold-restore-cache-desync

Conversation

@MocA-Love
Copy link
Copy Markdown
Owner

@MocA-Love MocA-Love commented Apr 14, 2026

症状

v1 ターミナルで文字が打てなくなる致命バグ。最新 dmg でテスト中に発覚。

  • ターミナル表示はされる(過去の scrollback が見える)
  • しかし 入力が一切反映されない
  • タブ切り替えを挟むと高確率で発生

根本原因

Upstream PR superset-sh#3348 (`feat(desktop): port v2 hide-attach xterm pattern to v1 terminal`) で導入された、v1 terminal cache と cold restore の状態不整合。

PR superset-sh#3389 (`fix(desktop): v1 terminal — emit legacy marker + gate reattach on streamReady`) で isReattach 判定を `streamReady` ベースに厳密化したが、cold restore 経路が誤って `streamReady=true` を立てるので本質的には直っていない

不整合の流れ

  1. ユーザーが v1 ターミナルを開く
  2. `useTerminalLifecycle.ts` の `createOrAttach` onSuccess で無条件に `v1TerminalCache.startStream()` / `setStreamReady()` / `markTerminalSessionReady()` が呼ばれる
  3. 直後の `result.isColdRestore` チェックで `setIsRestoredMode(true)` して return
  4. 結果: backend session は存在しないのに、cache 側だけ `streamReady=true` 扱い
  5. `PersistentTabRenderer.tsx` は terminal pane を persistent views に含めていないので、タブ切替で React が Terminal を unmount する
  6. ユーザーが同じターミナルタブに戻る → 再 mount
  7. `isReattach = cachedBeforeCreate?.streamReady === true && cachedBeforeCreate.subscription !== null` が true
  8. `createOrAttach` が完全にスキップされる
  9. ユーザーが typing → renderer は `electronTrpcClient.terminal.write.mutate` を呼ぶ
  10. main 側は session 不在で throw
  11. `writeRef` が callback なしで呼ばれているため、失敗は 握りつぶされる
  12. UI 上は「表示あり、入力反映なし」

致命度

  • Terminal は PersistentTabRenderer の persistent view 対象外 → タブ切替のたびに unmount される
  • `coldRestoreState` は module-level Map で remount をまたいで残る → cache の誤った ready 状態が固定化
  • `writeRef` にエラー表面化機構がない → ユーザーには原因不明

修正内容

1. `useTerminalLifecycle.ts` createOrAttach onSuccess

cold restore 判定より 後ろ に `startStream` / `setStreamReady` / `markTerminalSessionReady` を移動。実 backend session がある場合のみ cache を ready にマーク。

```diff
onSuccess: (result) => {
if (!isAttachActive()) return;
if (activeAttachRequestId !== requestId) return;
setConnectionError(null);
clearPaneInitialDataRef.current(paneId);

  • v1TerminalCache.startStream(paneId);

  • v1TerminalCache.setStreamReady(paneId);

  • markTerminalSessionReady(paneId);

    const storedColdRestore = coldRestoreState.get(paneId);
    if (storedColdRestore?.isRestored) { ... return; }

    if (result.isColdRestore) { ... return; }

  • // Real backend session is live — safe to start the stream

  • // subscription and unblock waiters.

  • v1TerminalCache.startStream(paneId);

  • v1TerminalCache.setStreamReady(paneId);

  • markTerminalSessionReady(paneId);

    pendingInitialStateRef.current = result;
    ```

2. `useTerminalLifecycle.ts` isReattach 判定

`coldRestoreState.has(paneId)` の間は isReattach を強制的に false にし、remount 時に必ず full attach 経路を通すようにする belt-and-suspenders。

```diff
const cachedBeforeCreate = v1TerminalCache.get(paneId);
+const hasPendingColdRestore = coldRestoreState.has(paneId);
const isReattach =
cachedBeforeCreate?.streamReady === true &&

  • cachedBeforeCreate.subscription !== null;
  • cachedBeforeCreate.subscription !== null &&
  • !hasPendingColdRestore;
    ```

3. `useTerminalColdRestore.ts` handleStartShell onSuccess

実シェルが spawn されたタイミングで、cache と session-readiness waiters を ready にマークする。失敗時は reject する。

```diff
onSuccess: (result) => {
pendingInitialStateRef.current = result;
maybeApplyInitialState();
+

  • // Mark the cache + readiness waiters now that a real shell exists.

  • v1TerminalCache.startStream(paneId);

  • v1TerminalCache.setStreamReady(paneId);

  • markTerminalSessionReady(paneId);

    setIsRestoredMode(false);
    coldRestoreState.delete(paneId);
    ...
    },
    onError: (error) => {
    ...

  • rejectTerminalSessionReady(

  • paneId,

  • new Error(error.message || "Failed to start shell"),

  • );
    isStreamReadyRef.current = true;
    ```

FORK NOTE

これは upstream 由来のリグレッションなので、通常フォークの方針では修正せず upstream の fix を待つのが原則ですが、terminal が使えないのは致命的なため一時パッチとして適用します。

upstream が自前で fix を出したら、本 PR の修正を revert → upstream の実装に合わせる形で追従する想定。コミットメッセージと各修正箇所の `FORK NOTE` コメントに経緯を残してあります。

検証

  • ✅ `bun run typecheck` パス
  • ✅ `bunx biome check` パス

再現手順(修正前)

  1. 新規 v1 terminal ペインを作成 → 何か入力 → 動く
  2. 別のタブに switch
  3. Terminal タブに戻す
  4. 入力 → 反映されない 🔴

修正後の期待動作

1〜3 は同じ。4 で入力 → 正常に反映される

影響範囲

  • `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts`
  • `apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts`

合計 2 ファイル、+45 / -8 行。

関連

Summary by CodeRabbit

リリースノート

  • バグ修正
    • ターミナルセッションの再開時における初期化の順序を改善し、コールドリストア中のセッション管理を最適化しました
    • セッション再開エラー時のエラーメッセージ処理を強化し、より詳細な情報をユーザーに提供するようにしました

…ore cached

Root cause (from superset-sh#3348):

createOrAttach onSuccess eagerly called v1TerminalCache.startStream /
setStreamReady / markTerminalSessionReady before inspecting result.isColdRestore.
Cold-restore responses come back without a real backend session, yet the cache
was left in streamReady=true state. On the next mount (e.g. after a tab switch
unmount from PersistentTabRenderer — terminal panes are not persistent views),
useTerminalLifecycle's isReattach fast-path saw streamReady=true + live
subscription and skipped createOrAttach entirely. From that moment on every
keystroke was forwarded to electronTrpcClient.terminal.write with no backend
session, the main process threw, and writeRef (no onError callback) silently
swallowed the failure — the user just saw a terminal that displayed output
but refused to accept input.

Fix:

1. useTerminalLifecycle.ts: defer startStream/setStreamReady/
   markTerminalSessionReady until after the cold-restore branches return, so
   only real backend sessions flip the cache into streamReady=true.
2. useTerminalLifecycle.ts: when coldRestoreState.has(paneId) is true, force
   isReattach=false so remounts re-enter the full attach path instead of
   trusting stale streamReady. Belt-and-suspenders against the module-level
   coldRestoreState map surviving unmount.
3. useTerminalColdRestore.ts: in handleStartShell's onSuccess (which finally
   spawns a real shell for the restored pane), mark the cache/readiness
   waiters as ready. Reject readiness waiters on failure so preset paths do
   not hang forever.

FORK NOTE: this is an upstream regression in superset-sh#3348 that superset-sh#3389 only narrowed.
Revisit when upstream publishes its own fix.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

Warning

Rate limit exceeded

@MocA-Love has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 46 minutes and 6 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 46 minutes and 6 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44e58cb5-675a-405a-8750-0f3711d7b428

📥 Commits

Reviewing files that changed from the base of the PR and between e7bb403 and 541a2f2.

📒 Files selected for processing (3)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/v1-terminal-cache.ts
📝 Walkthrough

Walkthrough

ターミナルセッションの冷復元時の初期化フローが改善されました。useTerminalColdRestoreでは成功時にストリーム準備をマーク、失敗時にセッション準備を拒否します。useTerminalLifecycleでは冷復元状態確認を追加し、キャッシュ初期化を遅延させてバックエンド接続後のみ実行するようになりました。

Changes

Cohort / File(s) Summary
Terminal Cold Restore Hook
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts
セッション準備状態の管理とv1ターミナルキャッシュの初期化処理を追加。handleStartShell成功時にストリーム開始とセッション準備マーク、失敗時にセッション準備拒否を実装。
Terminal Lifecycle Hook
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts
冷復元待機状態のチェック条件を追加してリアタッチ高速パスをガード。キャッシュストリーム初期化の実行タイミングをcreateOrAttach直後から冷復元完了後に変更し、バックエンドセッション確立後のみ実行するように修正。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 コールドな復元も、温かく迎えて
ストリームは準備、セッション整えて
順序を正して、フローは美しく
バックエンド待ちて、やさしく繋ぐ
ターミナルの息吹、ついに生まれる ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed タイトルは変更の主要な目的(タブ切り替え時の cold restore キャッシュによるターミナル入力停止の修正)を明確に示しており、変更セットの中核を正確に要約している。
Description check ✅ Passed PR説明は症状、根本原因、修正内容、検証方法を詳細に記載しており、リポジトリのテンプレート構造に大体対応している。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/v1-terminal-cold-restore-cache-desync

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts (1)

657-659: 重複ロジックのヘルパー関数への抽出を検討してください。

ストリーム準備状態のマーキングシーケンス(startStreamsetStreamReadymarkTerminalSessionReady)がuseTerminalColdRestore.tsと重複しています。将来的な一貫性のために、v1-terminal-cacheモジュールにヘルパー関数を追加することを検討できます。

ただし、PRノートによると、これはアップストリームの修正が利用可能になるまでの一時的なフォークパッチとのことなので、現状のままでも問題ありません。

♻️ オプション:ヘルパー関数の提案
// v1-terminal-cache.ts に追加
export function markSessionReady(paneId: string): void {
  startStream(paneId);
  setStreamReady(paneId);
  markTerminalSessionReady(paneId);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts`
around lines 657 - 659, The sequence startStream(paneId) →
setStreamReady(paneId) → markTerminalSessionReady(paneId) is duplicated in
useTerminalLifecycle (functions: v1TerminalCache.startStream,
v1TerminalCache.setStreamReady, markTerminalSessionReady) and
useTerminalColdRestore; extract this sequence into a single helper exported from
the v1-terminal-cache module (e.g., export function markSessionReady(paneId:
string)) and replace the three-call sequence in both useTerminalLifecycle.ts and
useTerminalColdRestore.ts with a single v1TerminalCache.markSessionReady(paneId)
call to keep logic consistent and maintainable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts`:
- Around line 657-659: The sequence startStream(paneId) → setStreamReady(paneId)
→ markTerminalSessionReady(paneId) is duplicated in useTerminalLifecycle
(functions: v1TerminalCache.startStream, v1TerminalCache.setStreamReady,
markTerminalSessionReady) and useTerminalColdRestore; extract this sequence into
a single helper exported from the v1-terminal-cache module (e.g., export
function markSessionReady(paneId: string)) and replace the three-call sequence
in both useTerminalLifecycle.ts and useTerminalColdRestore.ts with a single
v1TerminalCache.markSessionReady(paneId) call to keep logic consistent and
maintainable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 605b6b80-7e1a-4b40-8e16-a89ac9d12925

📥 Commits

Reviewing files that changed from the base of the PR and between 2942450 and e7bb403.

📒 Files selected for processing (2)
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalColdRestore.ts
  • apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalLifecycle.ts

Address CodeRabbit review on #167: the three-call sequence
startStream + setStreamReady + markTerminalSessionReady was repeated in
useTerminalLifecycle.ts and useTerminalColdRestore.ts. Centralize it in
v1-terminal-cache.ts so the cold-restore and normal attach paths stay in
lockstep, and drop the now-unused markTerminalSessionReady imports from
the consumers.
@MocA-Love
Copy link
Copy Markdown
Owner Author

CodeRabbit レビュー対応

Nitpick 指摘の通り、startStreamsetStreamReadymarkTerminalSessionReady の 3 ステップを v1-terminal-cache.tsmarkSessionReady(paneId) ヘルパーに抽出しました (541a2f275)。

変更内容

  • v1-terminal-cache.ts: markSessionReady() を新規 export。session-readiness モジュールの markTerminalSessionReady をここで内部的に呼ぶ。
  • useTerminalLifecycle.ts: 3 行のシーケンスを v1TerminalCache.markSessionReady(paneId) の 1 行に置き換え。不要になった markTerminalSessionReady の import を削除。
  • useTerminalColdRestore.ts: 同上。rejectTerminalSessionReady のみ import に残す(onError 経路で使用)。

検証

  • bun run typecheck
  • bunx @biomejs/biome check

cold-restore と通常 attach の両方の経路が同じヘルパーを通るようになり、将来的な drift を防げます。

@MocA-Love MocA-Love merged commit 65f4157 into main Apr 15, 2026
6 checks passed
MocA-Love added a commit that referenced this pull request Apr 15, 2026
Codex review (PR#174): the previous unconditional markSessionReady()
call in handleRetryConnection.onSuccess broke the PR #167 invariant
that the cache must not flip to streamReady=true before a real backend
session exists. Cold-restore reconnect responses come back with
isColdRestore=true and no backend; setting the cache ready in that
state lets a subsequent tab-switch remount take the isReattach
fast-path and silently drop user keystrokes — exactly the bug PR #167
was meant to fix.

Restore the !result.isColdRestore gate. The cold-restore reconnect
path is still wired end-to-end because handleStartShell.onSuccess (in
main, after PR #167) calls markSessionReady once the replacement shell
spawns.
MocA-Love added a commit that referenced this pull request Apr 15, 2026
Codex review (PR#174): the previous unconditional markSessionReady()
call in handleRetryConnection.onSuccess broke the PR #167 invariant
that the cache must not flip to streamReady=true before a real backend
session exists. Cold-restore reconnect responses come back with
isColdRestore=true and no backend; setting the cache ready in that
state lets a subsequent tab-switch remount take the isReattach
fast-path and silently drop user keystrokes — exactly the bug PR #167
was meant to fix.

Restore the !result.isColdRestore gate. The cold-restore reconnect
path is still wired end-to-end because handleStartShell.onSuccess (in
main, after PR #167) calls markSessionReady once the replacement shell
spawns.
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.

2 participants