Skip to content

Commit e2de9a1

Browse files
feat(terminal): associate terminals with workspace tabs
- Added workspaceTabId to terminal request, port, and record contracts - Propagated workspaceTabId through server handler to runtime terminal creation - Updated WorkspacePanels to find terminal by active tab instead of using global first terminal - Changed auto-create logic to trigger per tab rather than when no terminals exist globally - Included workspaceTabId in listTerminals response for client-side matching - Updated known-gaps documentation to clarify ADR 0020 contract-baseline scope
1 parent 9713c4c commit e2de9a1

20 files changed

Lines changed: 323 additions & 126 deletions

‎packages/webui/docs/adr/0022-webui-workspace-panel-is-a-browser-tab-strip.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,9 @@ owns rather than a thing the Agent drives.
119119
surface could not be a tab's content; a launcher can. The `browser` kind is not one of the four:
120120
it is deleted, and the tab it used to be is replaced by the web tabs.
121121
- **The URL box belongs to the new tab page.** There is no persistent address bar. A web tab carries
122-
a toolbar instead: back, forward, reload, and the URL shown read-only. A person who wants another
123-
URL opens a new tab and types it there.
122+
a toolbar instead: back, forward, reload, home to return to the new tab page in the same tab, and
123+
the URL shown read-only. Back and forward are disabled when the page has no entry in that
124+
direction. A person who wants another URL opens a new tab and types it there.
124125
- **Every web tab goes through the same switch.** The person's launcher path reads the same surface
125126
switch as the Agent's path, and a tab on a surface whose switch is off does not open.
126127
- **A web tab is titled by the page and glyphed by default.** The title is the one the provider

‎packages/webui/docs/browser-use/01-capability-matrix.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ that closes it. It is a gap, not a denial.
443443
| The page a person opened lands in the tab that asked for it | **implemented** | the launcher dispatches `request-web-page` before the call, so a `watchBrowserWebTabs` snapshot that lands mid-call finds the tab that asked; `open-web-tab` binds the page to the named tab, and to a launcher that is waiting when the command names none. Either order, one page, one tab — and the tab is not activated, so a person who looked elsewhere is not moved. `packages/webui/src/client/features/workspace/projection/workspace-panel-state.ts` |
444444
| A refusal keeps the launcher, and says why | **implemented** | the runtime's own message is carried on the launcher's `request.error` and rendered under the URL box, so the box stays and the person can type something else. Nothing about the launcher's refusal is worded differently from the Agent's |
445445
| The URL box exists only on the new tab page | **implemented** | the launcher renders exactly one input, and a web tab renders none: the browser business's host shows the page's URL in the tab's toolbar as text, not as a field. `packages/webui/src/client/features/browser/ui/BrowserPanelHost.public.tsx` |
446-
| A web tab carries back, forward, reload and the URL | **implemented** | the toolbar is rendered by the browser business's **published host** — ADR 0022's component table already gave it that home ("binds a tab to a target, shows the picture and the toolbar") — and it is drawn over the one thing the strip hands it: the tab's target, and the url the runtime published with it. The strip owns the row of tabs; a workspace component drawing a foreign business's page chrome would invert the dependency rule. `BrowserPanelHost.public.tsx` |
446+
| A web tab carries back, forward, reload, home and the URL | **implemented** | the toolbar is rendered by the browser business's **published host** — ADR 0022's component table already gave it that home ("binds a tab to a target, shows the picture and the toolbar") — and it is drawn over the one thing the strip hands it: the tab's target, the url and navigation-history availability the runtime publishes with it. Back and forward are disabled when no entry exists in that direction; home replaces the current tab's content with its launcher. The strip owns the row of tabs; a workspace component drawing a foreign business's page chrome would invert the dependency rule. `BrowserPanelHost.public.tsx` |
447447
| The toolbar's three presses go through the existing command path, and name the page | **implemented** | one request per press on the browser family's own `sendBrowserCommand` operation — no second operation, no second route. `WebuiBrowserCommandRequest` gained an optional `target`, and the handler routes a named target to `WebuiBrowserPort.sendCommandToTarget`. The target is what makes the press mean the page in front of the person: an absent target still resolves the surface's selection (a run's, or the panel's own cell), which is a page the tab may not be showing. `packages/webui/src/server/operation/browser.ts`, `packages/webui/src/server/operation/handlers/browser.ts`, `packages/webui/src/runtime/browser/browser-service.ts` |
448448
| A toolbar press that cannot be honoured says so, and leaves the page alone | **implemented** | the refusal travels out with the runtime's own code and message: `browser_target_stale` for a reference recorded before a move, `browser_capability_denied` when the page's history has nothing to go to. The service checks the surface switch and the command gate first, so a refusal names the switch rather than the page. The client helper does not catch: `browser-web-tab-toolbar.test.tsx` |
449449
| Back, forward and reload move a real page | **implemented** | `packages/webui/test/integration/isolated-chromium-two-pages.test.ts`, "the web tab's toolbar, on a real page": three presses through the client's own helper, the operation's validator and handler, the service and a real isolated Chromium. Observed: `back` lands on the page it came from, `forward` on the one it left, `reload` re-reads the page it is on, the generation advances with each move so the pre-move reference is refused with `browser_target_stale` while the tab's current reference still works, and a `back` with nothing behind it is refused with `browser_capability_denied` with the page untouched |
@@ -602,4 +602,4 @@ the thing that changes — which is what #207 does.
602602
when it is refreshed, is not decided.
603603
- **The bridged surfaces have no tab model at all.** Everything measured here is the isolated
604604
Chromium. The extension bridge exposes one visible tab per browser, so a tab strip over it is a
605-
different question and was not asked.
605+
different question and was not asked.

‎packages/webui/docs/client/ownership.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ proposing to move this state to the server.
133133
writes panel state through `stateFor(sessionId)` and `dispatch(sessionId, …)`, so a tab opened in
134134
one session is not in another's. A tab is a stable slot whose content can be replaced in place, and
135135
the URL box exists only inside the new tab page — a web tab carries a toolbar (back, forward,
136-
reload, the URL read-only) instead. The run's selection and the tab the person is looking at are two
136+
reload, home to its launcher, and the URL read-only) instead. Back and forward are disabled when
137+
their corresponding history entry is absent. The run's selection and the tab the person is looking at are two
137138
cells with one writer each, with one bootstrap: a person who opens a web tab while the run has no
138139
selection gives the run that page, decided by the runtime at the moment the page appears
139140
(`WebuiBrowserTabOpener` — `runtime/browser/browser-service.ts` — `openWebTab`). A tab is a stable

‎packages/webui/src/client/features/browser/ui/BrowserPanel.public.tsx‎

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,11 @@ import { webuiBrowserPicturePoint } from "../application/browser-commands.js";
6868
const PANEL_COPY = {
6969
title: "浏览器",
7070
frameLabel: "浏览器画面",
71-
frameEmpty: "还没有画面。选择一个标签页,或等待浏览器推送第一帧。",
72-
frameNoTarget: "没有选中的标签页。",
73-
frameUnrenderable: "收到了一帧无法显示的画面数据,已忽略。",
7471
inputLabel: "浏览器输入区",
7572
unauthorized: "隔离浏览器未授权。请在设置的「浏览器」页中打开对应的浏览器开关。",
7673
absent: "当前运行时没有浏览器能力,浏览器面板不可用。",
7774
notReady: "浏览器能力尚未就绪(例如扩展未连接),面板暂时无法显示页面。",
7875
unknown: "当前运行时没有报告浏览器能力,面板按未授权处理。",
79-
changed: (generation: number) =>
80-
`标签页已更新到第 ${generation} 代,之前的输入不再有效。`,
81-
dropped: (count: number) => `已丢弃 ${count} 帧过期画面。`,
8276
} as const;
8377

8478
export interface BrowserPanelProps {
@@ -392,16 +386,6 @@ export function BrowserPanel({
392386
</p>
393387
) : null}
394388

395-
{state?.targetChange ? (
396-
<p className="webui-browser-notice" role="alert" data-testid="browser-panel-target-changed">
397-
{PANEL_COPY.changed(state.targetChange.target.generation)}
398-
</p>
399-
) : null}
400-
{state?.droppedFrameCount ? (
401-
<p className="webui-browser-notice" role="status" data-testid="browser-panel-dropped-frames">
402-
{PANEL_COPY.dropped(state.droppedFrameCount)}
403-
</p>
404-
) : null}
405389
{state?.error ? (
406390
<p
407391
className="webui-browser-notice is-error"
@@ -446,9 +430,7 @@ export function BrowserPanel({
446430
onKeyDown={onKeyDown}
447431
/>
448432
) : (
449-
<p className="webui-browser-empty" role="status" data-testid="browser-panel-frame-empty">
450-
{frame ? PANEL_COPY.frameUnrenderable : viewed ? PANEL_COPY.frameEmpty : PANEL_COPY.frameNoTarget}
451-
</p>
433+
<div data-testid="browser-panel-frame-empty" aria-hidden="true" />
452434
)}
453435
</div>
454436
</section>

0 commit comments

Comments
 (0)