Files
IdeaSDK/frontend/src/features/web/WebAgentCell.tsx
Blomios 3a18556ffa fix(frontend): refit web agent cells after opening a new one (#61 web regression)
The web shell never adopted the desktop's #61 fix: WebAgentCell/WebWorkspace
didn't pass any refitSignal to TerminalView, so a cell opened after another
kept a stale xterm scaling — desktop "fixed" it via an incidental window
resize, but mobile has no such escape hatch.

- TerminalView: a refit landing on a transient 0x0 container now reschedules
  on the next few frames instead of giving up for good (bounded retries),
  closing the independent timing gap Architect identified. refitSignal stays
  the single explicit-refit mechanism; the terminal/PTY is never recreated,
  and resize is still pushed to the PTY only when rows/cols actually change.
- WebAgentCell: new optional refitSignal prop, forwarded to TerminalView
  as-is (no key change, no remount).
- WebWorkspace/LiveProjectPanel: new cellLayoutVersion counter, the web
  equivalent of desktop useLayout.layoutVersion, bumped on every cell
  open/close and forwarded as refitSignal to the visible WebAgentCell.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 21:21:52 +02:00

102 lines
3.9 KiB
TypeScript

/**
* Web agent cell — ticket #13, lot F4. Thin wiring (no new terminal logic) that
* reuses the existing, transport-neutral {@link TerminalView} to render a CLI
* agent over the WebSocket in web mode.
*
* The agent's CLI runs server-side (B6); the browser is display-only. This
* component just supplies `TerminalView` with the DI **agent gateway** as the
* opener/reattacher:
* - `open` → `agent.launchAgent(projectId, agentId, …)` (frame `agent.launch`,
* unified `terminal.attached` ack; the minted conversation id is carried on the
* returned handle),
* - `reattach` → `agent.reattach(sessionId, …)` (frame `terminal.attach`, no
* relaunch, bounded scrollback repainted) — so a browser reload/reconnect
* resumes the surviving server-side PTY.
*
* The session id is persisted in component state so a re-mount re-attaches rather
* than relaunching, matching the desktop cell's lifecycle. A structured agent is
* refused server-side (`UNSUPPORTED`) and surfaced by `TerminalView`'s own error
* banner — no crash. It touches only gateways via DI (no `@tauri-apps/api`).
*/
import { useCallback, useState } from "react";
import type {
OpenTerminalOptions,
ReattachResult,
TerminalHandle,
} from "@/ports";
import { useGateways } from "@/app/di";
import { TerminalView, type TerminalInputApi } from "@/features/terminals";
import { TerminalKeyBar } from "./TerminalKeyBar";
interface WebAgentCellProps {
/** Owning project id (resolved server-side by `agent.launch`). */
projectId: string;
/** Agent to launch/attach. */
agentId: string;
/** Working directory for the cell (typically the project root). */
cwd: string;
/** Layout leaf id, when the caller tracks one (drives the singleton guard). */
nodeId?: string;
/**
* Bumped by the caller after a web cell opens/closes (ticket #61 web
* regression) — the web equivalent of desktop `useLayout.layoutVersion`.
* Forwarded to {@link TerminalView} as-is; never changes this component's
* `key`, so the terminal/PTY is never recreated by it.
*/
refitSignal?: number;
}
export function WebAgentCell({ projectId, agentId, cwd, nodeId, refitSignal }: WebAgentCellProps) {
const { agent } = useGateways();
const [sessionId, setSessionId] = useState<string | null>(null);
// Input API of the mounted xterm (#69), used by the mobile key bar. Stays
// `null` in headless renders where xterm cannot mount — the bar then renders
// disabled rather than throwing.
const [inputApi, setInputApi] = useState<TerminalInputApi | null>(null);
const open = useCallback(
(options: OpenTerminalOptions, onData: (bytes: Uint8Array) => void): Promise<TerminalHandle> =>
agent.launchAgent(
projectId,
agentId,
nodeId ? { ...options, nodeId } : options,
onData,
),
[agent, projectId, agentId, nodeId],
);
const reattach = useCallback(
(sid: string, onData: (bytes: Uint8Array) => void): Promise<ReattachResult> =>
agent.reattach(sid, onData),
[agent],
);
return (
// #69 — the cell was a flat `h-64` (256px), which on a phone left the agent
// a letterbox roughly 20 lines tall. It now takes 60% of the *dynamic*
// viewport on phones (so the collapsing URL bar can't clip it) and keeps a
// fixed, desktop-like height from `sm` up. `min-h-0` on the terminal row
// lets it shrink inside the flex column instead of pushing the key bar off.
<div
data-testid="web-agent-cell"
className="flex h-[60dvh] min-h-64 w-full flex-col overflow-hidden rounded-md border border-border sm:h-80"
>
<div className="min-h-0 flex-1">
<TerminalView
cwd={cwd}
agentMode
open={open}
reattach={reattach}
sessionId={sessionId}
onSessionId={setSessionId}
onReady={setInputApi}
refitSignal={refitSignal}
/>
</div>
<TerminalKeyBar api={inputApi} />
</div>
);
}