Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/web/src/lazurio/threadTabs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"
import { toastManager } from "../components/ui/toast";
import {
type RightPanelSurface,
selectActiveRightPanelSurface,
selectThreadRightPanelState,
type ThreadRightPanelState,
useRightPanelStore,
Expand All @@ -21,6 +22,7 @@ import {
newTabSurface,
parseThreadTabsAnswer,
placesOf,
receiveForeign,
receiveSnapshot,
reconcileThreadTabs,
resetThreadTabs,
Expand Down Expand Up @@ -138,6 +140,25 @@ describe("the strip from the server's list", () => {
});
});

it("selects the first tab where nothing is selected, so the panel never opens on Nová karta beside them", () => {
expect(
reconcile({ isOpen: false, activeSurfaceId: null, surfaces: [] }, [id(1), id(2)]),
).toEqual({ isOpen: false, activeSurfaceId: tab(1).id, surfaces: [tab(1), tab(2)] });
// A panel stored with tabs and no selection takes one even when the list brings nothing new.
const unselected = { isOpen: true, activeSurfaceId: null, surfaces: [tab(1)] };
expect(reconcile(unselected, [id(1)])).toEqual({ ...unselected, activeSurfaceId: tab(1).id });
// An empty strip stays as it is.
const empty = { isOpen: true, activeSurfaceId: null, surfaces: [] };
expect(reconcile(empty, [])).toBe(empty);
});

it("selects another Environment's tab the server announces where nothing is selected", () => {
setPanel({ isOpen: true, activeSurfaceId: null, surfaces: [] });
receiveSnapshot(threadA, snapshot([]));
receiveForeign(threadA, { tabId: "ab".repeat(16), view: foreign.view });
expect(panel()).toEqual({ isOpen: true, activeSurfaceId: foreign.id, surfaces: [foreign] });
});

it("drops this Environment's unlisted tabs and keeps pending ones, Nová karta and other Environments' tabs", () => {
const newTab = newTabSurface();
const state = {
Expand Down Expand Up @@ -250,6 +271,22 @@ describe("reveals", () => {
expect(panel()).toEqual({ isOpen: true, activeSurfaceId: tab(1).id, surfaces: [tab(1)] });
});

it("leave the agent's tab of a thread that comes into view selected, for the panel the person opens", () => {
// The agent opened the page while the thread was out of view (or before a reload): its
// reveal is in the first snapshot, a baseline, so the panel does not open by itself...
syncOf(threadA).inlinePanel = true;
receiveSnapshot(
threadA,
snapshot([{ tabId: id(1), agent: true, revealRequest: { id: "epoch-1:1" } }]),
);
expect(panel()).toEqual({ isOpen: false, activeSurfaceId: tab(1).id, surfaces: [tab(1)] });
// ...and when the person opens it, it shows the agent's page, not Nová karta.
useRightPanelStore.getState().toggleVisibility(threadA);
expect(
selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, threadA),
).toEqual(tab(1));
});

it("give way to a choice the person made since the snapshot came", () => {
syncOf(threadA).inlinePanel = true;
setPanel({ isOpen: true, activeSurfaceId: files.id, surfaces: [files] });
Expand Down
39 changes: 19 additions & 20 deletions apps/web/src/lazurio/threadTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ export function parseForeignTab(value: unknown): ForeignTab | null {
* `kept` (pending holds, tabs the device keeps itself) has. Every other Environment browser
* surface goes, lazurio.7's included (its own tab and its new tabs at the view's bare origin).
* Listed tabs without a surface are added at the end, in the list's order, without opening a
* closed panel. An active surface that went falls back as upstream's reconciliation does.
* closed panel. Where nothing is selected, or the selected surface went, the first Environment
* browser tab is selected, as upstream's reconciliation does for its browser: the panel never
* opens on the empty Nová karta beside tabs of the strip whose frames it would not load (a thread
* that comes into view after its agent opened a page, whose first snapshot reveals nothing).
*/
export function reconcileThreadTabs(
current: ThreadRightPanelState,
Expand All @@ -181,20 +184,18 @@ export function reconcileThreadTabs(
id: tabSurfaceId(tabId),
kind: "environment-browser",
}));
if (added.length === 0 && surfaces.length === current.surfaces.length) return current;
const next = [...surfaces, ...added];
const activeStillExists = next.some((surface) => surface.id === current.activeSurfaceId);
return {
...current,
surfaces: next,
activeSurfaceId: activeStillExists
? current.activeSurfaceId
: current.activeSurfaceId === null
? null
: (next.find((surface) => surface.kind === "environment-browser")?.id ??
next[0]?.id ??
null),
};
const activeSurfaceId = next.some((surface) => surface.id === current.activeSurfaceId)
? current.activeSurfaceId
: (next.find((surface) => surface.kind === "environment-browser")?.id ?? next[0]?.id ?? null);
if (
added.length === 0 &&
surfaces.length === current.surfaces.length &&
activeSurfaceId === current.activeSurfaceId
) {
return current;
}
return { ...current, surfaces: next, activeSurfaceId };
}

/** What the panel has seen of the server's reveal requests, per run of the server (`epoch`). */
Expand Down Expand Up @@ -428,7 +429,9 @@ const panelOf = (threadRef: ScopedThreadRef) =>
const hasSurface = (threadRef: ScopedThreadRef, surfaceId: string) =>
panelOf(threadRef).surfaces.some((surface) => surface.id === surfaceId);

/** Adds a surface, in front with the panel open, or behind the surface in view. */
/** Adds a surface, in front with the panel open, or behind the surface in view. Where nothing is
* selected it is the one selected, as in the strip after the server's list, and a closed panel
* stays closed. */
function withSurface(
current: ThreadRightPanelState,
surface: EnvironmentBrowserSurface,
Expand All @@ -439,11 +442,7 @@ function withSurface(
: [...current.surfaces, surface];
return front
? { ...current, isOpen: true, surfaces, activeSurfaceId: surface.id }
: {
...current,
surfaces,
activeSurfaceId: current.activeSurfaceId ?? (current.isOpen ? null : surface.id),
};
: { ...current, surfaces, activeSurfaceId: current.activeSurfaceId ?? surface.id };
}

/** Removes a surface the way upstream closes one: the neighbour comes to the front. */
Expand Down
6 changes: 5 additions & 1 deletion docs/operations/lazurio-fork-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,11 @@ T3 žádný prohlížeč nemá, a tak overlay dělá tohle:
servíruje, sleduje stránka seznam vlákna a každé zařízení srovná svůj pás s
tímtéž seznamem. Nové záložky přidá na konec v pořadí souboru a neotevře jimi
zavřený panel; kde už záložku ukazuje, zůstává volbou zařízení (záložky jde
přetahovat, i povrchy overlaye). Povrch je `environment-browser:<ID>` (id
přetahovat, i povrchy overlaye). Kde není vybraný žádný povrch, nebo vybraný
zmizel, vybere první záložku prohlížeče jako upstream srovnání svého
prohlížeče, takže se panel otevře na ní, a ne na prázdné Nové kartě vedle
záložek, jejichž rámy by nenačetl (vlákno přijde do zobrazení až po tom, co
agent stránku otevřel). Povrch je `environment-browser:<ID>` (id
velkými písmeny) bez adresy; rám ukazuje `<origin pohledu>/t/<ID>` s originem
ze snímku. Jen záložka jiného Environmentu má uloženou adresu (`view`). Čekající
podržení srovnání nezahodí, pozůstatky lazurio.7 (vlastní záložka vlákna, nové
Expand Down
Loading