feat(frontend): agent playground sessions, files, and inspector - #5306
Conversation
Ports the parked session FE (@agenta/entities/session Fern+zod wrappers, the records-replay adapter transcriptToMessages, loadSession, + regression test) onto fe-chore/request-optimizations-onbig (= big-agents + FE/sdk/runner improvements). AgentChatPanel was fully rewritten on that base (frame-split; the per-session useChat moved to AgentConversation.tsx), so the records-hydration hook is re-wired there as a follow-up, not carried from the old AgentChatPanel.
…tion Re-attaches the records-replay hydration to req-opt's frame-split structure (the per-session useChat moved from AgentChatPanel to AgentConversation). Cache-first: on open with an empty local message cache, loadSessionMessages (queryRecords → v6 messages) seeds the conversation; own-run sessions with a warm cache skip it. Stamps seenIdsRef + restoredIdsRef so restored history renders settled, and arms the bottom pin.
…d/utils ElicitationWidget imports partitionElicitationDraft from the utils barrel, but the elicitation-m1-followups change added it to elicitation.ts + the widget without adding it to index.ts — breaking the next-dev build. Present on big-agents too (base bug); this unblocks the local build.
…ig-agents-add-sessions
…ig panel Tool rows in the agent template's Tools section now surface resolution problems without opening each drawer: a catalog action that 404s (renamed/ removed) or a missing connection shows a red Unresolved tag; a connection with is_valid=false shows an amber Reconnect tag. Collapsed provider groups roll up their worst child status, and the section header tooltip reflects it. The probe reuses the drawer's action-detail query family (shared cache, 5-min staleTime) with a low-priority fetch so playground load is unaffected; 404s skip retries and transient errors never mark a row. GatewayToolsBridge gains connectionsErrored so a failed connections fetch reads as unknown rather than "all connections removed".
…y clients - add session liveness primitive (deriveStreamNest / deriveSessionLifecycle / refineLifecycleWithSandbox) with 11 unit tests - add killSession (DELETE /sessions/streams) to the session API - thread lowPriority through querySessionRecords / fetchSessionStream / querySessionStreams; add per-resource sessions/mounts clients (normal + low-priority) - zod boundary: interaction status as string, add interaction created_at
…session - records replay: split assistant turns on `done`; hydrate empty-cache sessions from records with a transcript skeleton; guard brand-new sessions from the empty query - SWR revalidate-on-open: refetch records low-priority, adopt only if server strictly ahead - liveness badge in the tab dot (local run-state, else backend liveness) from ONE shared project-wide is_alive query; low-priority hydration + liveness - End session (kill) in the history menu, shown only when the session is alive
…ctions status - StreamsTab uses deriveStreamNest (deletes local nest.ts) + shows resumable/reattachable - InteractionsTab reads status via the validated package query (string, not object.code)
Records what's done/pending/blocked for the agent-chat FE integration, the backend asks (durable archive/delete cascade, session naming), and FOLLOWUP(sessions,*) code markers.
…ivation - add queryMountFiles / readMountFile (mounts file ops) with zod schemas - deriveMountRows: fold the flat whole-tree listing into a one-level folder/file view, plus mountBreadcrumbs (7 unit tests)
Upgrade the Mounts tab from a flat metadata list to a navigable folder/file tree with text preview (drill-in client-side via deriveMountRows). Shows a clear notice when the mount object store isn't configured. Doc updated (browser done, read-write follow-up, store-config gotcha).
Replace the fixed-width TurnInspector with one adaptive right panel beside the chat: - nested antd Splitter, persisted width, min/max + a chat-width floor so the panel can't squeeze the transcript; collapses to 0 when closed (chat never remounts) - refined Turn|Session segmented switch; Turn = the migrated turn inspector (live messages), Session = session-scoped content (Mounts/State/Interactions, reusing the durable endpoints) - rightPanelAtom replaces turnInspectorAtom; session view is valid in chat mode too
Group the left config panel: draftable config sections (model/instructions/tools/mcp/ skills/advanced) then an 'Operations' divider for sections that never enter a revision — Triggers (moved out) and a new Mounts stub (placeholder for agent-level mounts, which don't exist yet; a conversation's live files are in the chat session panel).
Triggers and Mounts render as regular sections; the labeled divider didn't match any existing header style.
…on header Replace the removed uppercase divider with an 'Operations' heading that matches the config panel header's typography (13px semibold), so the two regions read as peers.
Configuration, Triggers, and Mounts are now three peer regions with identical header styling (the panel header's 13px semibold). Triggers and Mounts leave the accordion: their headers carry the count/add-trigger and coming-soon affordances, bodies render directly (Subscriptions/Schedules sub-sections come from TriggerManagementSection).
…-style bars Full-bleed 48px tinted bars with the bottom hairline — a visual copy of the panel's sticky Configuration header — instead of inline text headings.
…r Triggers The Triggers region bar drops its + (count only); its body now renders two ConfigAccordionSection sections — Subscriptions and Schedules — with the same icon/count/header-+ /collapse treatment as the template config sections. Each section's + and empty-state link open its own create drawer (subscription picker / schedule drawer), default-bound to the agent.
Slide the panel in/out (220ms width transition on the splitter panes) instead of snapping. The transition is enabled only around open-state flips so divider dragging stays free of rubber-banding, and the panel content is retained through the close so it slides out instead of blanking.
The transition class was gated behind an effect-driven flag, so it landed a commit after the width change and never animated. The panes now carry the transition whenever the divider isn't being dragged (onResizeStart/End flip it synchronously), and the close-hold is derived during render so content is retained in the same commit that starts the collapse.
…uration Move them out of AgentTemplateControl (where they sat inside the Configuration section's body, under its sticky header) into a new AgentOperationsSections rendered at the panel level. Each section wraps its own sticky Configuration-style header bar, so the panel is three vertically stacked sections and scrolling swaps the pinned header per region.
AgentConfigSkeleton drops its Triggers row (no longer a config section); new AgentOperationsSkeleton renders the real Triggers/Mounts header bars over pulsing bodies below the Configuration section while hydration is pending or agent-ness is unknown — the panel holds its final three-section shape from first paint, and no trigger queries fire for maybe-prompt apps.
Rename the Mounts region to Storage and give it config-style inner sections like Triggers has: App drive (the agent's durable folder, #5215 — coming soon) and Session drive (per-conversation working folder, browsable from the session panel). 'Drive' over 'mount' — mounts name the mechanism, not the thing. Skeleton updated to match.
The Session drive section now browses the ACTIVE conversation's files: the app layer slots a SessionDriveContent body (chat scope + active tab -> the shared mount file browser) into AgentOperationsSections' new sessionDrive prop, keeping entity-ui decoupled from chat state. The section is controlled-open so the browser and its queries mount only when expanded (zero cold-load requests); no open conversation shows an explanatory empty state, and MountsTab's empty copy drops the mount jargon.
|
@coderabbitai review |
✅ Action performedReview finished.
|
The folder tree row selected and toggled on the same click, so collapsing a folder also selected it and flipped the right pane to the folder view. Split the caret (toggle only) from the row body (select only); the row now reveals children on select but never collapses.
There was a problem hiding this comment.
Actionable comments posted: 5
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6e332904-4236-425f-bf35-6179d27b401f
📒 Files selected for processing (24)
web/oss/.gitignoreweb/oss/package.jsonweb/oss/scripts/copy-pdf-worker.mjsweb/oss/src/components/AgentChatSlice/assets/markdown.tsxweb/oss/src/components/Drives/DriveDrawer.tsxweb/oss/src/components/Drives/DriveExplorer.tsxweb/oss/src/components/Drives/DriveFileRow.tsxweb/oss/src/components/Drives/FileThumb.tsxweb/oss/src/components/Drives/FilesDrawer.tsxweb/oss/src/components/Drives/FilesDrawerBody.tsxweb/oss/src/components/Drives/OriginTag.tsxweb/oss/src/components/Drives/StorageSection.tsxweb/oss/src/components/Drives/agentDrive.tsweb/oss/src/components/Drives/driveIcons.tsxweb/oss/src/components/Drives/driveKinds.tsweb/oss/src/components/Drives/driveTree.tsweb/oss/src/components/Drives/fileMeta.tsxweb/oss/src/components/Drives/pdfThumb.tsweb/oss/src/components/Drives/renderers.tsxweb/oss/src/components/Drives/useSessionDrive.tsweb/packages/agenta-entities/src/session/core/fileActivity.tsweb/packages/agenta-entities/src/session/core/mountBrowser.tsweb/packages/agenta-entities/src/session/core/pathUtils.tsweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/SkillFormView.tsx
🚧 Files skipped from review as they are similar to previous changes (17)
- web/oss/src/components/Drives/FilesDrawerBody.tsx
- web/oss/src/components/Drives/driveIcons.tsx
- web/oss/src/components/Drives/StorageSection.tsx
- web/oss/src/components/Drives/OriginTag.tsx
- web/oss/src/components/Drives/DriveFileRow.tsx
- web/oss/src/components/AgentChatSlice/assets/markdown.tsx
- web/oss/src/components/Drives/pdfThumb.ts
- web/packages/agenta-entities/src/session/core/pathUtils.ts
- web/packages/agenta-entities/src/session/core/mountBrowser.ts
- web/oss/src/components/Drives/useSessionDrive.ts
- web/oss/src/components/Drives/fileMeta.tsx
- web/oss/src/components/Drives/FileThumb.tsx
- web/oss/src/components/Drives/driveKinds.ts
- web/packages/agenta-entities/src/session/core/fileActivity.ts
- web/oss/src/components/Drives/agentDrive.ts
- web/oss/src/components/Drives/driveTree.ts
- web/oss/src/components/Drives/FilesDrawer.tsx
| const resolveRel = (dir: string, rel: string): string => { | ||
| const out: string[] = [] | ||
| for (const seg of (dir ? dir.split("/") : []).concat(rel.split("/"))) { | ||
| if (seg === "" || seg === ".") continue | ||
| if (seg === "..") out.pop() | ||
| else out.push(seg) | ||
| } | ||
| return out.join("/") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Resolve root-relative URLs from the mount root.
resolveRel("docs", "/assets/logo.png") currently returns docs/assets/logo.png. Leading-slash links and assets should discard dir.
Proposed fix
const resolveRel = (dir: string, rel: string): string => {
const out: string[] = []
- for (const seg of (dir ? dir.split("/") : []).concat(rel.split("/"))) {
+ const base = rel.startsWith("/") ? [] : dir ? dir.split("/") : []
+ const target = rel.replace(/^\/+/, "")
+ for (const seg of base.concat(target.split("/"))) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const resolveRel = (dir: string, rel: string): string => { | |
| const out: string[] = [] | |
| for (const seg of (dir ? dir.split("/") : []).concat(rel.split("/"))) { | |
| if (seg === "" || seg === ".") continue | |
| if (seg === "..") out.pop() | |
| else out.push(seg) | |
| } | |
| return out.join("/") | |
| const resolveRel = (dir: string, rel: string): string => { | |
| const out: string[] = [] | |
| const base = rel.startsWith("/") ? [] : dir ? dir.split("/") : [] | |
| const target = rel.replace(/^\/+/, "") | |
| for (const seg of base.concat(target.split("/"))) { | |
| if (seg === "" || seg === ".") continue | |
| if (seg === "..") out.pop() | |
| else out.push(seg) | |
| } | |
| return out.join("/") | |
| } |
| async function fetchMountDataUri( | ||
| mountId: string, | ||
| projectId: string, | ||
| path: string, | ||
| ): Promise<string | null> { | ||
| const blob = await fetchMountFileBlob({mountId, projectId, path}) | ||
| if (!blob || blob.size > INLINE_ASSET_CAP) return null | ||
| return new Promise((resolve) => { | ||
| const reader = new FileReader() | ||
| reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null) | ||
| reader.onerror = () => resolve(null) | ||
| reader.readAsDataURL(blob) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Bound and throttle HTML asset expansion before downloading.
Promise.all launches one request per stylesheet/image, while INLINE_ASSET_CAP is checked only after the complete blob downloads. A small HTML file can trigger thousands of concurrent multi-megabyte requests. Deduplicate URLs, limit asset count/concurrency, and enforce the byte limit before or during transfer.
Also applies to: 338-359
| await Promise.all( | ||
| Array.from(doc.querySelectorAll<HTMLLinkElement>('link[rel~="stylesheet"][href]')).map( | ||
| async (link) => { | ||
| const href = link.getAttribute("href") ?? "" | ||
| if (!href || isExternalUrl(href)) return | ||
| const css = await fetchMountText(mountId, projectId, resolveRel(dir, href)) | ||
| if (css == null) return | ||
| const style = doc.createElement("style") | ||
| style.textContent = css | ||
| link.replaceWith(style) | ||
| }, | ||
| ), | ||
| ) | ||
|
|
||
| await Promise.all( | ||
| Array.from(doc.querySelectorAll<HTMLImageElement>("img[src]")).map(async (img) => { | ||
| const src = img.getAttribute("src") ?? "" | ||
| if (!src || isExternalUrl(src) || src.startsWith("data:")) return | ||
| const uri = await fetchMountDataUri(mountId, projectId, resolveRel(dir, src)) | ||
| if (uri) img.setAttribute("src", uri) | ||
| }), | ||
| ) | ||
|
|
||
| // Strip every agent-authored script vector so allow-scripts only runs our interceptor. | ||
| doc.querySelectorAll("script").forEach((s) => s.remove()) | ||
| doc.querySelectorAll("iframe[srcdoc]").forEach((f) => f.removeAttribute("srcdoc")) | ||
| doc.querySelectorAll("*").forEach((el) => { | ||
| for (const attr of Array.from(el.attributes)) { | ||
| if (/^on/i.test(attr.name)) el.removeAttribute(attr.name) | ||
| else if (/^\s*javascript:/i.test(attr.value)) el.setAttribute(attr.name, "#") | ||
| } | ||
| }) | ||
| doc.querySelectorAll("a[href]").forEach((a) => { | ||
| if (isExternalUrl(a.getAttribute("href") ?? "")) { | ||
| a.setAttribute("target", "_blank") | ||
| a.setAttribute("rel", "noopener noreferrer") | ||
| } | ||
| }) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Block remote subresources in untrusted HTML previews.
External stylesheets, images, embeds, and CSS URLs remain active. Opening an agent-authored file can therefore contact attacker-controlled hosts automatically. Inject a restrictive CSP allowing only inlined data: assets/styles and the trusted interceptor script, or strip remote subresources.
Also applies to: 469-472
| } catch { | ||
| return html |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Fail closed when HTML assembly throws.
The catch returns the original agent-authored HTML, which is then rendered with allow-scripts. Any assembly failure therefore bypasses all script and handler stripping. Return an inert escaped document or surface a preview error instead.
Also applies to: 469-472
…keyboard nav Replace the harsh full-saturation accent bar/ring (which hooked the rounded corner and read badly in dark mode) with a simple left-edge accent border, square on list rows. Applies to the session/drawer selected rows and the drive files' just-changed state. Add a shared themed focus-visible ring across every drive surface (rows, tree, folder tiles) in place of the browser default outline, and blur the drawer-open triggers so an ESC-close no longer leaves a stuck ring. Add keyboard navigation: arrow keys for the flat lists (config Files, chat rail), the WAI-ARIA tree pattern for the explorer (up/down/left/right/expand/collapse), and 2D arrows for the tile grids. The tree focuses its selected row on open so arrows work immediately. Reuse the origin tag's tooltip copy on the All/Agent/ Session filter tabs.
…n sections Both ConfigAccordionSection siblings fell back to key="empty" when their list was empty, so opening the playground with no triggers logged a React duplicate- key warning. Scope the fallbacks per section (no-subscriptions / no-schedules); the has-* keys were already unique and the 0<->1 remount behaviour is preserved.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dcbeef67-7e0f-42b2-a1cc-68276399967f
📒 Files selected for processing (10)
web/oss/src/components/Drives/ContextRail.tsxweb/oss/src/components/Drives/DriveExplorer.tsxweb/oss/src/components/Drives/DriveFileRow.tsxweb/oss/src/components/Drives/FilesWindow.tsxweb/oss/src/components/Drives/OriginTag.tsxweb/oss/src/components/Drives/StorageFilesHeader.tsxweb/oss/src/components/Drives/StorageSection.tsxweb/oss/src/components/Drives/driveKeyboard.tsweb/oss/src/styles/globals.cssweb/packages/agenta-entity-ui/src/DrillInView/SchemaControls/TriggerManagementSection.tsx
🚧 Files skipped from review as they are similar to previous changes (7)
- web/oss/src/components/Drives/OriginTag.tsx
- web/oss/src/components/Drives/StorageFilesHeader.tsx
- web/oss/src/components/Drives/StorageSection.tsx
- web/oss/src/components/Drives/ContextRail.tsx
- web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/TriggerManagementSection.tsx
- web/oss/src/components/Drives/DriveExplorer.tsx
- web/oss/src/components/Drives/FilesWindow.tsx
| export const gridArrowKeyDown = (e: KeyboardEvent<HTMLElement>) => { | ||
| if (!["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return | ||
| const items = Array.from(e.currentTarget.querySelectorAll<HTMLButtonElement>("button")) | ||
| if (!items.length) return | ||
| e.preventDefault() | ||
| const idx = items.findIndex((r) => r === document.activeElement) | ||
| const cur = idx < 0 ? 0 : idx | ||
| const top0 = items[0].offsetTop | ||
| const cols = Math.max(1, items.filter((it) => it.offsetTop === top0).length) | ||
| const focusAt = (i: number) => items[Math.min(Math.max(i, 0), items.length - 1)]?.focus() | ||
| switch (e.key) { | ||
| case "Home": | ||
| return focusAt(0) | ||
| case "End": | ||
| return focusAt(items.length - 1) | ||
| case "ArrowLeft": | ||
| return focusAt(cur - 1) | ||
| case "ArrowRight": | ||
| return focusAt(cur + 1) | ||
| case "ArrowUp": | ||
| return focusAt(idx < 0 ? 0 : cur - cols) | ||
| case "ArrowDown": | ||
| return focusAt(idx < 0 ? 0 : cur + cols) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Fix idx < 0 skipping behavior and robustify column counting.
There are two issues with the grid navigation logic:
- If no item is currently focused (
idx < 0), pressingArrowRightorArrowDownusescur = 0and adds1orcols, meaning it skips the first item entirely and focuses item1orcolsinstead. - In
FilesWindow.tsx, the grid items are wrapped inmotion.divcomponents.Framer Motioncomponents often establish a containing block via transforms, meaningoffsetTopfor all buttons inside them will evaluate to0. This would causecolsto equalitems.length, breaking up/down navigation.
Handle idx < 0 upfront and use getBoundingClientRect().top for a robust, DOM-agnostic column calculation.
🛠️ Proposed fix
export const gridArrowKeyDown = (e: KeyboardEvent<HTMLElement>) => {
if (!["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return
const items = Array.from(e.currentTarget.querySelectorAll<HTMLButtonElement>("button"))
if (!items.length) return
e.preventDefault()
const idx = items.findIndex((r) => r === document.activeElement)
- const cur = idx < 0 ? 0 : idx
- const top0 = items[0].offsetTop
- const cols = Math.max(1, items.filter((it) => it.offsetTop === top0).length)
const focusAt = (i: number) => items[Math.min(Math.max(i, 0), items.length - 1)]?.focus()
+
+ if (idx < 0) {
+ return e.key === "End" ? focusAt(items.length - 1) : focusAt(0)
+ }
+
+ // Using getBoundingClientRect() avoids offsetParent issues with animated wrappers (e.g., motion.div).
+ const top0 = items[0].getBoundingClientRect().top
+ const cols = Math.max(1, items.filter((it) => Math.abs(it.getBoundingClientRect().top - top0) < 5).length)
+
switch (e.key) {
case "Home":
return focusAt(0)
case "End":
return focusAt(items.length - 1)
case "ArrowLeft":
- return focusAt(cur - 1)
+ return focusAt(idx - 1)
case "ArrowRight":
- return focusAt(cur + 1)
+ return focusAt(idx + 1)
case "ArrowUp":
- return focusAt(idx < 0 ? 0 : cur - cols)
+ return focusAt(idx - cols)
case "ArrowDown":
- return focusAt(idx < 0 ? 0 : cur + cols)
+ return focusAt(idx + cols)
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const gridArrowKeyDown = (e: KeyboardEvent<HTMLElement>) => { | |
| if (!["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return | |
| const items = Array.from(e.currentTarget.querySelectorAll<HTMLButtonElement>("button")) | |
| if (!items.length) return | |
| e.preventDefault() | |
| const idx = items.findIndex((r) => r === document.activeElement) | |
| const cur = idx < 0 ? 0 : idx | |
| const top0 = items[0].offsetTop | |
| const cols = Math.max(1, items.filter((it) => it.offsetTop === top0).length) | |
| const focusAt = (i: number) => items[Math.min(Math.max(i, 0), items.length - 1)]?.focus() | |
| switch (e.key) { | |
| case "Home": | |
| return focusAt(0) | |
| case "End": | |
| return focusAt(items.length - 1) | |
| case "ArrowLeft": | |
| return focusAt(cur - 1) | |
| case "ArrowRight": | |
| return focusAt(cur + 1) | |
| case "ArrowUp": | |
| return focusAt(idx < 0 ? 0 : cur - cols) | |
| case "ArrowDown": | |
| return focusAt(idx < 0 ? 0 : cur + cols) | |
| } | |
| } | |
| export const gridArrowKeyDown = (e: KeyboardEvent<HTMLElement>) => { | |
| if (!["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) return | |
| const items = Array.from(e.currentTarget.querySelectorAll<HTMLButtonElement>("button")) | |
| if (!items.length) return | |
| e.preventDefault() | |
| const idx = items.findIndex((r) => r === document.activeElement) | |
| const focusAt = (i: number) => items[Math.min(Math.max(i, 0), items.length - 1)]?.focus() | |
| if (idx < 0) { | |
| return e.key === "End" ? focusAt(items.length - 1) : focusAt(0) | |
| } | |
| // Using getBoundingClientRect() avoids offsetParent issues with animated wrappers (e.g., motion.div). | |
| const top0 = items[0].getBoundingClientRect().top | |
| const cols = Math.max(1, items.filter((it) => Math.abs(it.getBoundingClientRect().top - top0) < 5).length) | |
| switch (e.key) { | |
| case "Home": | |
| return focusAt(0) | |
| case "End": | |
| return focusAt(items.length - 1) | |
| case "ArrowLeft": | |
| return focusAt(idx - 1) | |
| case "ArrowRight": | |
| return focusAt(idx + 1) | |
| case "ArrowUp": | |
| return focusAt(idx - cols) | |
| case "ArrowDown": | |
| return focusAt(idx + cols) | |
| } | |
| } |
# Conflicts: # web/oss/package.json # web/pnpm-lock.yaml
[chore] Revert ts dep bumps from #5337
…s-add-sessions # Conflicts: # web/oss/package.json # web/pnpm-lock.yaml
Context
This branch builds the agent playground's session and files experience on top of big-agents. It adds client-side multi-session chat, a durable Files/drive surface with rich previews for what the agent reads and writes, a records-backed Inspector, and the supporting backend/runner pieces those surfaces depend on (file recency, durable-replay trace ids, resume-after-kill). Base is
big-agents(now merged intomain); the branch has been re-aligned several times.Scope note: sessions are client-side (backend is not yet a source of truth)
The backend does not yet own sessions — there is no server session registry, list endpoint, title, or app link (foundational gaps tracked separately). So "sessions" here are a client-side construct: the session list lives in the browser (localStorage), and each session hydrates its durable content (the record log + mounts) by id. Cross-device / persistent session listing, server-side titles, and a session↔app link are out of scope for this branch and gated on that backend work. What ships is the playground UX over local sessions plus durable per-session content — not server-owned sessions.
What this adds
Sessions (client-side). Multi-session chat in the playground: a tab bar in build mode, a rail plus history popover in chat mode. Sessions are held in localStorage and hydrate durable content (records / mounts) per session id. New sessions autofocus the composer; never-run empty "husk" sessions are pruned so the local list stays clean; per-session drive/quick-look state is keyed by session id, so a backgrounded tab never clobbers the active one. Per the scope note, the list and titles are local, not server-owned.
Drive / Files. One Files surface across the config panel, the chat context rail, and a full two-pane drawer. It aggregates the session cwd mount and the agent's durable per-artifact mount (folded under
agent-files/), tags each file Agent vs Session (with an explanatory tooltip), orders newest-first via the object store's mtime, and hides runner-internal artifacts (agents/…,.agenta-*); dot-prefixed files are dimmed in the drawer and dropped from the compact config list. The preview experience adds:sandbox="allow-scripts"runs only a small link interceptor).Unified Inspector. One records-backed panel replaces the old Turn and Session panels, with Timeline, Context, and Runtime lenses. While the panel is open you can click any assistant turn to focus it (gated on the panel being open, not on a turn already being selected). The Streams and State tabs live-revalidate during a run.
Backend / runner.
MountFile.mtimeis threaded from the object store's LastModified, so "newest first" works no matter how a file was created (bash script, Write tool, upload). Previously recency came only from Write/Edit tool records, so bash-created files fell back to alphabetical.trace_idon the terminaldoneevent, so a reloaded transcript links a replayed turn back to its trace.session_streams(excluding soft-deleted rows) lets a resumed session mint a fresh stream row after a kill instead of colliding with the dead one.Tests / notes
tscandpnpm lint-fixare clean across the touched packages (no new type errors vs the big-agents baseline). CodeQL and the web production build pass.sandbox="allow-scripts"after stripping the agent's<script>/ inline handlers /javascript:URLs / nestedsrcdoc, so only our link interceptor runs and the frame stays origin-isolated from Agenta. Residual risk if a script vector is missed; it still cannot reach Agenta's origin, DOM, or cookies.doneevent).NEXT_PUBLIC_AGENT_CHAT_STOP_KILLS_SESSION(Stop also kills the session) is behind a flag and still in progress; the resume-after-kill migration lands here to support it.SessionInspector/StreamsTab.tsxhas a pre-existingSessionStream.statustype error.What to QA
+, switch tabs, then reopen a closed one via the history clock. There is no cross-device list — sessions live in this browser. A new session focuses the composer; abandoning an empty one leaves no residue in the local history.agents/sessions/…,.agenta-*) must NOT appear, and dot-prefixed files (e.g..claude/…) are hidden from the config list.