Skip to content

Match chat sessions to sites by site id instead of path - #4131

Merged
bcotrim merged 4 commits into
trunkfrom
match-chat-sessions-by-site-id
Jul 14, 2026
Merged

Match chat sessions to sites by site id instead of path#4131
bcotrim merged 4 commits into
trunkfrom
match-chat-sessions-by-site-id

Conversation

@bcotrim

@bcotrim bcotrim commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Related issues

  • Related to STU-1985 (one slice of the sidebar-redesign split — matching semantics only)

How AI was used in this PR

Claude implemented the change end-to-end from a scoped brief: traced the placement/matching flow, made the code changes, and extended the unit tests. I reviewed the diff via a guided code tour and steered the follow-ups (shared matching helper; recording the site id in session events).

Proposed Changes

Chat sessions are bound to local sites via placements in ~/.studio/app.json, which have always stored a validated siteId — but the actual matching (sidebar grouping, empty-draft reuse, session header/preview owner lookup, and the main process' owner-server resolution) compared filesystem paths. Since site paths are name-derived and get reused, deleting a site and creating a new one at the same folder made the new site silently inherit the dead site's chat history.

This PR switches matching to the site id, consistent with everything else per-site in app.json (e.g. siteMetadata). All matching now routes through one shared, dependency-free helper (packages/common/ai/sessions/owner-site.ts) used by the renderer, the router, and the main process:

  • A recreated site at the same path starts clean; chats from the deleted site show under Unassigned instead of leaking into the new site.
  • "New chat" no longer reuses an empty draft that belonged to a different (deleted) site at the same path.
  • Session header, dashboard preview, initial-route redirect, and environment switching resolve the owner site by id too, so a stale path can never rebind a session to the wrong site.
  • A session whose owner site no longer exists is orphaned visibly (Unassigned / plain site-name header), never adopted by another site and never dropped.
  • ownerSitePath/ownerSiteName are kept for display, and path matching remains as a defensive fallback for placements written before siteId existed (placements are read from user-editable JSON without schema validation), so no existing chats are dropped.

Execution target (session JSONL): the same path-reuse hazard existed inside the session event log — studio.site_selected events identified the agent's working site by path only, so prompting an old session whose folder now belongs to a recreated site would act on the new site's directory. Now every writer records an optional siteId on site_selected events, and turn dispatch resolves the working site by id from the CLI registry (refreshing a moved site's current name/path) with path fallback for events written before the field existed. Additive schema change — no migration: old events simply lack the field and keep resolving by path, exactly as today.

Testing Instructions

Unit tests cover the matching semantics and event recording:

  • npm test -- packages/common/ai/sessions/tests apps/ui/src/components/site-list apps/cli/commands/ai/tests apps/cli/ai/tests/tools.test.ts

Manual (agentic UI, apps/ui):

  1. Create a site and start a chat in it — it groups under that site in the sidebar.
  2. Delete the site, then create a new one with the same name (same folder path).
  3. The new site should show "No active chats"; the old chat should appear under Unassigned — previously it leaked into the new site.
  4. Open the old chat from Unassigned — the header shows the stored site name as plain text (no site dropdown), and prompting it does not target the new site.
  5. Draft reuse: open "New chat" for a site twice without prompting — the empty draft is reused. Recreate a site at the same path — the new site gets a fresh draft, not the old site's.
  6. Legacy fallback: remove siteId from one entry in aiSessionPlacements in ~/.studio/app.json — that chat should still group under its site via the path.
  7. Event recording: start a new chat for a site and check its JSONL under ~/.studio/sessions/ — the studio.site_selected entry should include siteId. Rename the site's folder (site settings), prompt again — the turn should resolve the site's new path via the id.

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

🤖 Generated with Claude Code

@bcotrim bcotrim self-assigned this Jul 8, 2026
@bcotrim
bcotrim marked this pull request as ready for review July 10, 2026 11:14
@bcotrim
bcotrim requested review from a team, katinthehatsite and youknowriad July 10, 2026 11:14
@youknowriad

Copy link
Copy Markdown
Contributor

In the other PRs, we're deleting the AI sessions when we delete the site, which means the issue being fixed here won't matter anymore. That said, I agree with the change here conceptually, it's better to match the id but my understanding is that this requires a change in the JSONL of the sessions right? In which case, what happens to old sessions?

@wpmobilebot

wpmobilebot commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing 2e911bd vs trunk

app-size

Metric trunk 2e911bd Diff Change
App Size (Mac) 1419.47 MB 1419.48 MB +0.01 MB ⚪ 0.0%

site-editor

Metric trunk 2e911bd Diff Change
load 1112 ms 1098 ms 14 ms ⚪ 0.0%

site-startup

Metric trunk 2e911bd Diff Change
siteCreation 6490 ms 6511 ms +21 ms ⚪ 0.0%
siteStartup 2397 ms 2389 ms 8 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@bcotrim

bcotrim commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

In the other PRs, we're deleting the AI sessions when we delete the site, which means the issue being fixed here won't matter anymore.

This change will still prevent existing unassigned sessions to be assigned to wrong sites. But you are correct, the main root cause of the issue will be gone with that PR

That said, I agree with the change here conceptually, it's better to match the id but my understanding is that this requires a change in the JSONL of the sessions right? In which case, what happens to old sessions?

In the app we have the information in app.json, which already has the siteId. In the JSONL we don't, so old sessions fall back to matching by path. I made the change to start recording the site id in the JSONL going forward. I considered the migration but I don't think it's worth it. For the existing sessions the migration would do exactly what the fallback does (no guarantees the path is the correct site) and new sessions will always have the site id. Let me know if you still prefer to run it so we can remove the fallback.

Thanks for the review!

…by-site-id

# Conflicts:
#	apps/cli/commands/ai/index.ts
@youknowriad

Copy link
Copy Markdown
Contributor

In the app we have the information in app.json,

I don't understand this, the source of truth of all sessions is the JSONL no? so how do you find the "id" of a session if it's not stored in the JSONL (in the app I mean)?

@bcotrim

bcotrim commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

The JSONL is the source of truth for the conversation (the transcript + which site the agent acts on), but the session↔site lives in app.json, keyed by the session's own id.

Concrete example — one chat on a site called "Restaurant" produces two entries:
JSONL:

{"type":"session","version":3,"id":"03239858-e5b1-4060-8123-80fc64316146","timestamp":"2026-07-10T11:10:35.481Z","cwd":"~/Studio"}
{"type":"custom","customType":"studio.site_selected","data":{"siteName":"Restaurant","sitePath":"/Users/bcotrim/Studio/restaurant"}}

~/.studio/app.json:

"aiSessionPlacements": {
  "03239858-e5b1-4060-8123-80fc64316146": {
    "kind": "site",
    "siteId": "2ebcac65-b983-4e35-86e9-665041ec4a21",
    "sitePath": "/Users/bcotrim/Studio/restaurant",
    "siteName": "Restaurant"
  }
}
@youknowriad

Copy link
Copy Markdown
Contributor

the session↔site lives in app.json, keyed by the session's own id.

AFAIK, this is only for the legacy app no, not the agentic one? I see this app.json config as throwable personally.

@bcotrim

bcotrim commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

AFAIK, this is only for the legacy app no, not the agentic one? I see this app.json config as throwable personally.

It does use it, unless I'm missing something:
https://github.com/Automattic/studio/blob/trunk/apps/ui/src/data/core/connectors/ipc/index.ts#L540 -> https://github.com/Automattic/studio/blob/trunk/apps/studio/src/ipc-handlers.ts#L246

If you edit app.json and remove the aiSessionPlacements block all chats will be unassigned in the UI.

What would you suggest as an alternative? Now is the best time to revisit the solution and change it, I'm happy to do that in the context of this PR or as a follow-up

@youknowriad

Copy link
Copy Markdown
Contributor

What would you suggest as an alternative? Now is the best time to revisit the solution and change it, I'm happy to do that in the context of this PR or as a follow-up

When I initially implemented the agentic UI, it was not like that for sure. I think that app.json config was added later. The idea was to guess the assignment from the sessions JSONL and organize them in the sidebar.

I don't mind the app.json addition, but I'm interested in two things:

  • Why it's in app.json and not something in cli.json, sessions are not app specific.
  • Why did we introduce this config in the first place, what were we trying to solve, that the previous behavior didn't actually solve.
@bcotrim

bcotrim commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

I did a bit of digging, it seems the placements were introduced by #3476
Your point makes sense, the ai placements data should live in shared.json to be accessible by app and CLI. I'll make that change in a follow-up, if you agree.
We can also explore moving away from those configs and into JSONL completely but that's a bigger refactor
What do you think?

@youknowriad

Copy link
Copy Markdown
Contributor

Your point makes sense, the ai placements data should live in shared.json to be accessible by app and CLI. I'll make that change in a follow-up, if you agree.

No, I think we should remove, shared, app and cli.json files in favor of a single config.json file. Today, the CLI make use of all the app.json and shared.json config files in the studio ui command, so that separation doesn't make sense anymore. WDYT

@youknowriad

Copy link
Copy Markdown
Contributor

Interesting that I made that change without realizing haha. And yeah we can explore moving away separately.

@bcotrim
bcotrim merged commit f2bc60e into trunk Jul 14, 2026
11 checks passed
@bcotrim
bcotrim deleted the match-chat-sessions-by-site-id branch July 14, 2026 08:45
@bcotrim

bcotrim commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

No, I think we should remove, shared, app and cli.json files in favor of a single config.json file. Today, the CLI make use of all the app.json and shared.json config files in the studio ui command, so that separation doesn't make sense anymore. WDYT

I added STU-2046 so we can move the discussion there.

bcotrim added a commit that referenced this pull request Jul 16, 2026
## Related issues

- Related to STU-1985
- Final extraction from #3998 (built on the already-landed #4131, #4133,
and #4134)

## How AI was used in this PR

Claude (Fable 5) extracted and adapted this change from the #3998
reference branch under close direction, preserving Shaun Andrews'
original work via co-authored commits, and re-verified the result
(typecheck, unit tests, light/dark visual pass in the browser UI). Code
was human-reviewed throughout.

## Proposed Changes

This is the capstone of the site-centric sidebar work: the sidebar
becomes a flat list of sites, with no per-site chat sublists.

- Clicking a site opens its most recent (non-archived) chat, or starts a
new chat when the site has none. Chat history is no longer browsed from
the sidebar — that moved below the composer in #4133.
- Each site row carries a single activity indicator that aggregates all
of the site's sessions, with priority: live-site sync > needs an answer
> working > new message. This keeps long site lists calm while still
surfacing what needs attention.
- Row quick actions (site actions menu + start/stop status button)
appear on hover/focus; drag-to-reorder from #4134 keeps working on the
flat rows.
- Status colors move to shared `--studio-color-status-*` tokens: the
wpds palette has no vivid status colors
(`--wpds-color-fg-content-success` resolves to near-black at dot size),
which also fixes the session header's running dot rendering almost
black.
- Chrome polish: full-bleed 44px site icon with overlaid status badge in
the site dropdown, flattened site icons, a visible hover tint on the
sidebar create button, the floating sidebar toggle moved to the
bottom-left, and aligned settings-header padding.

Two product decisions to be aware of (intentional for now):

- **Unread "new message" badges are in-memory only** — they reset (seed
as all-seen) on every launch.
- **The current chat is indicated via `aria-current` + active-row
styling**, not the literal "Current chat" label from the mockups.

## Testing Instructions

- Start Studio with `npm start` and enable the `Agentic UI` feature
flag.
- Confirm the sidebar shows a flat list of sites (no chat sublists):
clicking a site with chats opens its latest chat; a site without chats
opens the new-chat view.
- Hover a row: the site actions menu and start/stop button appear; right
side shows a green dot for running sites, a play affordance for stopped
ones.
- Send a prompt on one site, switch to another: the first row shows a
working spinner, then a "New message" badge when the turn completes;
answering a pending question clears the "Needs an answer" indicator.
- Drag a site row to reorder; the order persists across restarts.
- Delete a site you're currently viewing and confirm you land back on
the home view.
- Check the sidebar and site dropdown in **both light and dark mode**.

## Pre-merge Checklist

- [x] Have you checked for TypeScript, React or other console errors?

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Shaun Andrews <191598+shaunandrews@users.noreply.github.com>
bcotrim added a commit that referenced this pull request Jul 21, 2026
…#4130)

## Related issues

- Related to STU-1985 — extracted from #3998 as part of splitting it
into smaller PRs.

## How AI was used in this PR

The core change was cherry-picked from #3998 (authored there with AI
assistance). Claude Code ported it onto trunk, resolved merge conflicts
(including the moves to id-based session↔site matching from #4131 and
the signed-out gates from #4183), addressed review feedback, and wrote
the unit tests for the redirect logic. All code was reviewed and
verified locally (lint, typecheck, apps/ui test suite).

## Proposed Changes

Studio's agentic UI always lands on the first site (in raw fetch order)
at launch, regardless of where the user was working. This PR makes the
app reopen on the site the user last worked with:

- The dashboard remembers the last visited site in `localStorage`.
- On launch, the `/` index route opens that site's newest active chat
(or a new chat if it has none) — matching exactly what clicking the site
in the site-centric sidebar (#4191) does.
- If the remembered site no longer exists (deleted) or nothing is stored
yet, it falls back to the **top site in sidebar order**, respecting the
manual ordering from #4134 instead of raw fetch order.
- The sidebar auto-scrolls to keep the restored site's row visible when
it sits below the fold (thanks @sejas for catching this!).

An earlier revision restored the exact last-visited *session*. We
simplified to site-level restore to stay consistent with the
site-centric sidebar model: sites are the primary navigation unit, and a
site always opens its newest active chat.

No launch-time cost: the index route already fetched sites and sessions
before redirecting; this adds only a synchronous `localStorage` read.

## Testing Instructions

1. Launch Studio with the agentic UI and open a chat on a site other
than the top one in the sidebar.
2. Quit and relaunch — you should land on that same site (its newest
active chat).
3. With enough sites to make the sidebar scroll, work on a site near the
bottom, relaunch — the sidebar should scroll so the selected site's row
is visible.
4. Archive the chat you were on, relaunch — you should land on that
site's newest remaining active chat, or a new chat if none are left.
5. Delete the site you were on, relaunch — you should land on your
sidebar's top site, with no errors.
6. Drag a different site to the top of the sidebar, clear the app's
localStorage, relaunch — the fallback should follow your sidebar order.

## Pre-merge Checklist

- [x] Have you checked for TypeScript, React or other console errors?

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Shaun Andrews <shaun@automattic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants