Skip to content

feat: a thread's Environment browser tabs on T3 Code's server, Nová karta behind "+", and dragging (F47 step 4) - #52

Merged
immakermatty merged 8 commits into
mainfrom
dev6646/thread-tabs-new-tab
Oct 12, 2026
Merged

immakermatty merged 8 commits into
mainfrom
dev6646/thread-tabs-new-tab

Conversation

@immakermatty

Copy link
Copy Markdown

Why

F47 step 4 (DEV-6646 task 2026-10-11-501). T3 Code's right panel shows ONE thread's Environment browser tabs, the same on every device. "+" opens the Platform's Nová karta. Archiving or deleting a thread closes its tabs. Matěj's decisions:

Platform steps 1–3 (Lazurio/LazurioPlatform#316, pingdotgg#318, pingdotgg#324, all approved) and pingdotgg#333 provide what this builds on: the hub reading holder files, tabs.json, lazurio-browser:closed, /new-tab and <lazurio-new-tab>.

What changes (8 commits, rebase-merge)

  1. Drag to reorder the right panel's tabs: a backport of upstream 4107930 (feat(web): reorder right panel tabs by dragging pingdotgg/t3code#17730), with its author kept. It goes away at the 0.0.46 rebuild, where upstream owns it. No new dependency (@dnd-kit/* is already in apps/web).

  2. A thread's tabs live in its holder file. apps/server/src/lazurio/holders.ts is a copy of the Platform's helpers at 49d54c9, tested with the Platform's vectors. threadTabs.ts keeps the tabs in $XDG_RUNTIME_DIR/lazurio-browser/holders/t3-<thread>.json, with recordedAt on the boot clock:

    • reconciled with tabs.json by keptTabs at start, on change, every 30 s and before every write;
    • re-read before every write, never writing over a file it cannot read, capped at 128 tabs.

    T3 Code's database and migrations stay upstream's. The route /api/lazurio/thread-tabs serves a snapshot, SSE with no-transform, and hold/release, using upstream's preview scopes (requiredScopeForRpcMethod). There is no RPC change.

  3. Archive and delete let go of the thread's tabs and stop its agent-browser daemon: SIGTERM only to a live process whose /proc command line is agent-browser. The daemon disconnects without closing a tab, and T3 Code never closes a target. A small adapter reads both orchestrations' events, and a startup sweep catches threads archived while the server was down.

  4. preview_open's tabs reach the web through the thread's strip, not its activities. ActivityPayloadProjection.ts is byte-equal to upstream again.

  5. The web strip follows the server's list:

    • reveal only when the agent opens a tab or a window;
    • × releases the thread's hold, with a toast saying whether the tab closes or stays because an agent works in it or Apps or another thread holds it, and Vrátit within 7 s;
    • a shared line and a tooltip show the other holders;
    • links in the chat are held.
    • Retired: browserUse.ts and its test, the own-tab singleton, the interim "+ → Browser", and the web copy of the session name.
  6. "+" and the empty panel open Nová karta (<lazurio-new-tab strip tools>, with upstream's "+" actions as T3's tools). A choice turns the tab into the thread's browser tab or into a T3 surface.

  7. The release contract test for the new seams.

  8. The runbook:

    • Overlay rows added, changed and retired, and the "Prohlížeč Environmentu" chapter rewritten;
    • "Vazba na vydání Platformy (F47)": this needs a Platform release with steps 1–3 first;
    • a browser check added to "Testování před vydáním".

Upstream seams (overlay lines, port excluded):

File Before After
RightPanelTabs.tsx 25 15
rightPanelStore.ts 96 27
ChatView.tsx 40 19
server.ts 4 4
ActivityPayloadProjection.ts 25 0

Overall, the web overlay shrinks by about 1,500 lines.

What doesn't change

  • No new dependency, no packages/ contract change, no migration.
  • Desktop and mobile apps are untouched.

Verification

  • New tests:
    • server: holders.test.ts (18, the Platform's vectors), threadTabs.test.ts (32, including F47's nine reconciliation cases), threadTabsRoute.test.ts (5), threadLifecycle.test.ts (2);
    • web: threadTabs.test.ts (19), LazurioNewTab.test.tsx (8);
    • upstream's moveSurface test.
  • Re-run by me on cd98b44:
    • server src/lazurio: 87 pass, 12 skipped (DevTools suites without an endpoint);
    • web src/lazurio: 65 pass;
    • rightPanelStore.test.ts: 68 pass;
    • release contract: 22/22;
    • server and web typecheck: exit 0.
  • The implementing agent also ran: vp lint (0 errors; the 89 warnings are pre-existing in upstream files), vp fmt --check, src/server.test.ts (212), and per-commit typechecks.
  • Not checked in a browser yet: no Environment has Platform steps 1–3. The check is F47 step 5:
    1. the Platform release with steps 1–3 on the browser Environments;
    2. a T3 Code preview on a canary;
    3. Matěj's check;
    4. stable 0.0.45-lazurio.8.

Risks

  • Pointer dragging itself isn't unit-tested. The store's move and the order kept through reconciliation are.
  • SSE through the gateway is to be verified on the canary. Polling is the fallback.
  • Release order: lazurio.8 on an older Platform keeps working without Nová karta, and Browser is unavailable in the web until the Platform rollout.

🤖 Generated with Claude Code

Backport of upstream 4107930 to v0.0.45, so a
person can drag the right panel's tabs into the order they want, the overlay's
surfaces like any other. It goes at the 0.0.46 refresh, where upstream owns it.

Adapted to v0.0.45: its store has no updateThread(state, ...) yet, so
moveSurface goes through automaticUpdate, which likewise leaves the user-action
revision alone, and the sortable wrapper wraps v0.0.45's tab markup (its audio
icon, rename ring and "+" condition stay). The two files the port adds to the
overlay join the fork CI's allowlist.

(cherry picked from commit 4107930)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CHANGES_REQUESTED on exact head cd98b44. The CI checks are green and the isolated release-contract test passed 22/22. The holder format/keptTabs, route scopes and cookie-Origin boundary, t3-only cleanup call sites, and release-order documentation look aligned. I did not merge or run a browser canary. Three ordering gaps still violate the thread-tab contract:

  1. [Blocking] Archive can resurrect a holder. In apps/server/src/lazurio/threadTabs.ts:692-700, hold checks retired and awaits liveThreadOf outside the serialized file operation; record has the same split at :564-574. If retirement starts after that check, queues removeFile (:639-649), and then the pending hold queues holdTab, the latter re-reads the missing file and writes the tab back (:318-333). The 30-second sweep only reconciles live tabs, not retired thread identities, so this can retain a tab after archive/delete. Put the live/retired fence in the same serialized critical section as the write (and cover both HTTP hold and agent record with a deferred-read race test).

  2. [Blocking] Undo and close can be reversed by their own retries. In apps/web/src/lazurio/threadTabs.ts:550-563, a failed release schedules another POST without checking whether restoreTabs/holdTab has since undone it (:617-637); the retry can release the just-restored hold. Conversely a failed hold scheduled at :511-523 is still sent after × has released the tab, and its successful response adds the closed surface again. Give each tab's pending operation a generation/cancellation fence and test transient failure → ×/Vrátit → delayed retry in both directions. The 7-second Undo currently cannot be trusted under ordinary transient network failures.

  3. [Blocking] Reject old snapshots before mutating a device's strip. receiveSnapshot (:452-480) ignores epoch/revision ordering, although the server supplies both. A POST response from an earlier hold can arrive after a later release SSE (e.g. another device closes it); it then restores the old tab indefinitely because unchanged periodic refreshes publish no replacement event. Track highest accepted revision per epoch, reconcile only newer snapshots, and test delayed POST response versus SSE and reconnect. This is needed for the stated same-tabs-on-every-device behavior.

Please repair these in the PR branch and request a fresh exact-head review after checks. The Platform-first → T3 canary → Matěj browser check → stable release order remains in force.

immakermatty and others added 7 commits October 12, 2026 05:41
…file

Each thread keeps its tabs of the Environment browser in the holder file the
view's hub reads, $XDG_RUNTIME_DIR/lazurio-browser/holders/t3-<thread>.json
(LazurioPlatform F47 points 4, 5 and 10), so a tab the thread shows stays alive
and every device sees the same strip. Beside the ids the file keeps each tab's
record time on the boot clock, so the server reconciles with the hub's
tabs.json as the Launchpad does: at the start, on every change of the list,
every 30 s and before every write. A write re-reads its file first and never
writes over one it cannot read. T3 Code's database stays upstream's.

The web reaches a thread's tabs at /api/lazurio/thread-tabs: a snapshot, a
server-sent event stream (no-transform, a whole snapshot per event, another
Environment's tab as a live-only event) and hold and release, authenticated as
every other HTTP route is, with upstream's preview scopes. A tab joins the strip
when the person holds it, when preview_open opens or adopts it (revealed unless
open: false), and when the thread's own agent-browser session binds another tab.
A snapshot names the other places that hold a tab and whether an agent works in
it. At most 128 tabs per thread.

holders.ts copies the Platform's helpers from src/browser/people/holds.ts at
49d54c9 and runs its vectors; it also carries the daemon stop the next commit
uses. The overlay's server layers join upstream's routes through one line of
server.ts. The service is off without an absolute XDG_RUNTIME_DIR or while the
Environment declares no browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stops its agent's daemon

When a thread is archived or deleted, its holder file goes, so the hub ends
every tab nothing else keeps, and the thread's agent-browser daemon is stopped:
SIGTERM to the process its .pid names, only when that is a live process whose
command line is agent-browser's, so a reused pid is never signalled. Tabs
another thread or the Launchpad holds stay. At the start, the same happens for
threads that are no longer live, including those archived while T3 Code was
down, after command readiness so the projection has caught up; names outside
t3- are never touched. An unarchived thread holds tabs again, and a renamed
thread's file takes its new title as its label.

threadLifecycle.ts is the one adapter to T3 Code's orchestration: it reads
v0.0.45's domain events and the shape of upstream's orchestration v2, so the
0.0.46 refresh swaps only the stream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…strip, not its activities

preview_open of another Lazurio Environment's view now tells the thread's
devices through the thread's tab stream, a live-only foreign event revealed
unless open: false, as every tab preview_open opens or adopts here already
joins the thread's holder file. So the previewTab seam in upstream's
ActivityPayloadProjection.ts, which carried preview_open's tab to the web
inside the thread's activities, goes with its test: the file is upstream's
again, and the 0.0.46 refresh, where upstream deletes it, has one conflict
fewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a Lazurio Environment the web client's right panel now shows the tabs the
thread keeps on the server, the same on every device. It follows the thread's
event stream and reconciles the strip to the list: new tabs at the end in the
list's order, existing ones where each device has them (dragging stays each
device's choice), another Environment's tabs each device's own. A page's new tab
and a plain click on a link to a tab in the chat hold the tab for the thread.

The person's x releases the thread's hold: a toast says whether the tab stays
(an agent works in it, or Apps or another thread holds it) or ends, and Vrátit
within the hub's grace holds it again; Close all is one toast. A tab whose view
ended leaves at once. The panel opens by itself only on the server's new reveal
request (preview_open, a new agent window), and only inline, no longer on every
agent click. The strip marks a tab an agent works in or another place holds,
and its tooltip and a line under the view say where else it is.

Retired: browserUse.ts, the thread's own tab, the interim "+ Browser" new tabs
at the view's bare origin, the web's copy of the session name, RightPanelTabs's
titles prop and profile seam, and ChatView's Browser seams (Browser stays
upstream's on the web). The store keeps one generic overlay action through
upstream's automatic or user updates. The CI allowlist follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where the Environment's shell defines <lazurio-new-tab> and the server keeps
the thread's tabs, "+" in the right panel adds a Nová karta tab and the empty
panel shows Nová karta, instead of upstream's menu and list (LazurioPlatform
F47 points 2 and 10). The element frames the Launchpad's /new-tab with the
thread's strip. A page or a tab chosen there takes Nová karta's place, in
place and active, and the thread holds it; a tab the strip has comes to the
front instead. Its tools are upstream's own "+" actions without Browser, which
the address bar replaces: each runs in Nová karta's place, also by its letter
while Nová karta is shown.

Elsewhere (an Environment without the element, the desktop app, a thread of
another environment) upstream's "+" menu and empty state stay; no fallback is
carried. LazurioNewTab.tsx is Nová karta; RightPanelTabs reaches it, as the
strip's words, through LazurioEnvironmentBrowser.tsx, so its seams are the
hook's actions and two conditionals. The CI allowlist follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ová karta

The release contract now holds task 501's overlay to its seams, so a rebuild on
a new upstream tag that loses one fails: the thread's holder file behind its
own route, with upstream's preview scopes and an event stream that is never
compressed or buffered, and the Platform's helpers copied with their source
named; the host recording preview_open's tabs in the thread's strip and the
session's new bindings; the panel opening only on the server's reveal and the
chat's links; the store's one generic action and one surface member;
RightPanelTabs's one import, hook and two Nová karta conditionals; ChatView's
one hook call and wrapper; and the element's name and tools. The checks of the
retired paths (browserUse.ts, the own tab, the interim new tab, the web's
session name and the previewTab seam) go, and the allowlist check follows the
new files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…unbook

The Overlay table gains the holder-file, archive, Nová karta and drag-port
rows, renames the right panel's row to the thread's tabs and changes the browser
tools' row; the rows of the panel that opened on every agent click and of the
interim "+ Browser" go. "Prohlížeč Environmentu" now describes the thread's
tabs (holder file, reconciliation, route, reveals, x with Vrátit, the shared
hint), Nová karta and archiving, with the seam paragraph matching the overlay.
A new paragraph ties lazurio.8 to the Platform release with F47 steps 1-3 and
fixes the order (Platform, preview canary, Matěj's browser check, stable), and
"Testování před vydáním" gains that browser check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@immakermatty

Copy link
Copy Markdown
Author

@agentrozjedemeai All three ordering blockers from your review of cd98b44 are fixed in 02db3af. Each fix is folded into the commit it belongs to (autosquash), so the history is still the 8 semantic commits; I pushed with an exact lease. Re-review requested once CI is green.

  1. Archive can no longer resurrect a holder (apps/server/src/lazurio/threadTabs.ts).
    • holdTab reads the live thread and retired inside the serialized write, and re-checks retired right before writing. Otherwise it refuses with thread-not-found.
    • record and hold both go through it, and record stays silent on that refusal.
    • Tests: "wins over a person's hold whose thread read was still under way: nothing is written back" (the HTTP path), and "wins over an agent's tab whose thread read was still under way, without a warning", with a harness that holds the live-thread read back.
  2. Retries can't reverse a newer ×/Vrátit (apps/web/src/lazurio/threadTabs.ts).
    • Each hold or release of a tab gets a never-reused intent number. A newer ×, Vrátit or link cancels an older retry and voids its answer: the number is checked before every POST and before using its answer.
    • Tests: "do not retry a failed hold once × let go of the tab", and "do not retry a failed release once Vrátit held the tab again".
  3. Old snapshots don't mutate a strip.
    • Server: the revision is now stamped per strip snapshot. A changed snapshot gets the next revision whether or not anyone streams it, so POST answers and SSE events are ordered by one number.
    • Web: it keeps the highest revision taken per epoch and ignores an older one. A new epoch starts over, and an epoch the device left is never taken again.
    • An equal revision is the same snapshot and is accepted. That keeps recovery after available: false working.
    • An ignored stale answer still reconciles against the newest snapshot, so a dropped answer can't leave a tab stuck.
    • Tests: "carries a revision that grows with every change of it, streamed or not" (server); "lets a POST answer older than the stream's newest list change nothing, and the strip follow that list" and "take the same snapshot again after a reconnect, start over with a restarted server, and never go back to the run it left" (web).

Each new test fails with the pre-fix implementation swapped in. The runbook gained four sentences on these contracts.

Checks re-run by me on 02db3af:

  • server src/lazurio: 90 pass, 12 skipped (DevTools suites);
  • web src/lazurio: 69 pass;
  • server and web typecheck: exit 0;
  • release contract: 22/22.

The implementing agent also ran src/server.test.ts (212), lint (0 errors) and fmt, and typechecked the rewritten intermediate commits.

@immakermatty
immakermatty force-pushed the dev6646/thread-tabs-new-tab branch from cd98b44 to 02db3af Compare October 12, 2026 04:43

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@immakermatty APPROVED for exact head 02db3af (base 36d4868). The three prior ordering blockers are addressed: archive/live-thread fencing now happens in the serialized holder write with deferred-read races; per-tab intent generations cancel superseded hold/release retries and answers; per-epoch snapshot revisions reject delayed older POST answers after SSE without losing the current reconciliation. I checked daemon stop is limited at its call sites to t3- sessions and verifies a live agent-browser executable before SIGTERM; the route uses upstream preview scopes, EnvironmentAuth cookie POST Origin, and uncompressed SSE. The copied holder rules (including keptTabs/boot clock/fail-closed read), release semantics, upstream seams, allowlist and F47 release-order runbook are consistent. My isolated checkout passed server lazurio 90 (12 DevTools skips), web lazurio 69, rightPanelStore 68, release contract 22/22, server/web typechecks, formatting and git diff --check; exact-head CI is green, zero review threads. Browser canary remains for F47 step 5. Do not merge yet: Platform steps 1–3 release first, then T3 preview canary and Matěj browser check before stable 0.0.45-lazurio.8.

@immakermatty
immakermatty merged commit c2dd835 into main Oct 12, 2026
7 checks passed
@immakermatty

Copy link
Copy Markdown
Author

Cleanup: the local worktree DEV-6646-thread-tabs was removed on Matěj's Mac after verifying every commit is on main (git cherry) and the remote branch holds the same head (02db3af).

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.

3 participants