Say what "open in container" is doing, and land on the pane doing it
Build App (Preview) / compute-version (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m40s
Build App (Preview) / build-linux (pull_request) Successful in 5m33s
Build App (Preview) / build-windows (pull_request) Successful in 5m40s
Build App (Preview) / prune-previews (pull_request) Successful in 1s

Opening a page is a container probe, a browser launch, a page load and
often a viewer start — several seconds during which the only feedback
was the click itself. Worse from a terminal, where the result appears in
a pane the user is not looking at.

So: the backend emits progress on the existing `container-progress`
channel at each step, the Browser tab renders that line whenever it is
set — the progress belongs to the project, not to whoever pressed the
button, which is what lets a terminal-initiated open report anywhere at
all — and the terminal's "In container" now selects the project's
Browser tab before starting, so the line has somewhere to appear.

Selecting a sub-tab from outside needed a route: `ProjectHome` keeps it
in local state, so `openProjectHomeTab` parks a request in the store and
the pane consumes it once. Consumed once, so it cannot fight the user's
own clicking afterwards.

Preview releases now prune themselves to the newest KEEP_PREVIEWS (2),
in a job that runs only if all three platforms published — a
half-finished run must not evict a good older build. The cleanup
workflow's manual sweep stays as the backstop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-11 11:13:21 -07:00
co-authored by Claude Opus 5
parent 85ea3956e8
commit ab747ce53d
7 changed files with 128 additions and 3 deletions
@@ -377,6 +377,15 @@ export default function BrowserTab({ project, active }: Props) {
</span>
)}
<div className="flex-1" />
{progress && (
<span
className="flex items-center gap-1.5 text-xs text-[var(--text-secondary)] min-w-0"
aria-live="polite"
>
<StatusIndicator tone="busy" label="" />
<span className="truncate">{progress}</span>
</span>
)}
{live && poppedOut === true && (
<span className="flex items-center gap-1.5 text-xs text-[var(--text-secondary)]">
Keep on top
@@ -43,6 +43,16 @@ export default function ProjectHome({ projectId, active }: Props) {
const { projects, remove } = useProjects();
const project = projects.find((p) => p.id === projectId);
const [tab, setTab] = useState<ProjectHomeTabId>("overview");
// Somewhere else asked for this project on a particular sub-tab — currently
// "I opened a page in the container's browser, show me it". Consumed once, so
// it cannot fight the user's own clicking afterwards.
const pendingHomeTab = useAppState((s) => s.pendingHomeTab);
useEffect(() => {
if (pendingHomeTab?.projectId !== projectId) return;
setTab(pendingHomeTab.tab as ProjectHomeTabId);
useAppState.getState().clearPendingHomeTab();
}, [pendingHomeTab, projectId]);
const [confirmRemove, setConfirmRemove] = useState(false);
const [confirmReset, setConfirmReset] = useState(false);
const [showMigration, setShowMigration] = useState(false);
@@ -554,6 +554,9 @@ export default function TerminalView({ sessionId, active }: Props) {
return;
}
if (!projectId) return;
// Land on the pane that will show it, before the work starts: opening takes
// several seconds, and the progress line lives there.
useAppState.getState().openProjectHomeTab(projectId, "browser");
// A sign-in page is the one case where the *window* size matters least and
// the layout matters most, so it gets the ordinary desktop viewport.
// `true`: from a terminal there is no Browser pane on screen, so the page
+26
View File
@@ -71,6 +71,18 @@ interface AppState {
tabOrder: string[];
activeTabKey: string | null;
openProjectHome: (projectId: string) => void;
/**
* Open a project's home tab *on a particular sub-tab*.
*
* The sub-tab is local state inside `ProjectHome`, so this parks a request
* here for it to pick up: an action taken somewhere else entirely — opening a
* page in the container's browser from a terminal — has to be able to land
* the user on the pane that shows the result.
*/
openProjectHomeTab: (projectId: string, tab: string) => void;
/** Consumed once by `ProjectHome`, then cleared. */
pendingHomeTab: { projectId: string; tab: string } | null;
clearPendingHomeTab: () => void;
closeHomeTab: (projectId: string) => void;
setActiveTabKey: (key: string) => void;
cycleTab: (delta: number) => void;
@@ -235,6 +247,20 @@ export const useAppState = create<AppState>((set) => ({
...activation(key),
};
}),
openProjectHomeTab: (projectId, tab) =>
set((state) => {
const key = homeTabKey(projectId);
return {
selectedProjectId: projectId,
tabOrder: state.tabOrder.includes(key)
? state.tabOrder
: [...state.tabOrder, key],
pendingHomeTab: { projectId, tab },
...activation(key),
};
}),
pendingHomeTab: null,
clearPendingHomeTab: () => set({ pendingHomeTab: null }),
closeHomeTab: (projectId) =>
set((state) => {
const key = homeTabKey(projectId);