feat(frontend): the agent page chrome and the session tab rail move into the packages - #5879
feat(frontend): the agent page chrome and the session tab rail move into the packages#5879ardaerzin wants to merge 8 commits into
Conversation
|
@coderabbitai review |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR extracts shared playground, session, chat, drive, and layout components into reusable packages. OSS chat, playground, and sessions screens now consume these components and shared state. ChangesShared agent playground UI
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
632dab3 to
7125323
Compare
f332816 to
7d387ac
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (9)
web/packages/agenta-playground-ui/src/components/AgentBuildPanel.tsx (1)
40-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReduce the component comments to one short line.
These comments describe general component behavior. They do not document a bug, race, or ordering constraint.
web/packages/agenta-playground-ui/src/components/AgentBuildPanel.tsx#L40-L47: Replace the multi-line component rationale with a short component description.web/packages/agenta-playground-ui/src/components/AgentPageHeader/AgentPageHeader.tsx#L23-L38: Replace the multi-line responsive and CSS rationale with a short component description.web/packages/agenta-playground-ui/src/components/AgentPageHeader/AgentRevisionStatus.tsx#L93-L100: Replace the multi-line shared-UI rationale with a short component description.As per coding guidelines, keep in-code comments to at most one short line unless they explain a genuinely surprising constraint.
Source: Coding guidelines
web/oss/src/components/Playground/Components/PlaygroundHeader/index.tsx (3)
638-652: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winMemoize the
leadingandactionsslot JSX.Both slots are rebuilt on every
PlaygroundHeaderrender.PlaygroundHeadersubscribes to many atoms, includingdisplayAgentNameon each keystroke of a rename. Every one of those renders produces new element trees forAgentPageHeader, which defeats anyReact.memoon the shared header and on the nestedEntityPickersubtree.Wrap both in
useMemowith their real dependencies.♻️ Proposed memoization for the `leading` slot
- const leading = currentWorkflow?.flags?.is_custom ? ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="ghost" size="icon" aria-label="Workflow options"> - <DotsThree size={16} /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start" className="w-[180px]"> - <DropdownMenuItem onSelect={openModal}> - <PencilSimple size={16} /> - Configure workflow - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - ) : undefined + const leading = useMemo( + () => + currentWorkflow?.flags?.is_custom ? ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" size="icon" aria-label="Workflow options"> + <DotsThree size={16} /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="start" className="w-[180px]"> + <DropdownMenuItem onSelect={openModal}> + <PencilSimple size={16} /> + Configure workflow + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) : undefined, + [currentWorkflow?.flags?.is_custom, openModal], + )Apply the same treatment to the
actionstree, or extract it into a small memoized child component so the evaluator picker does not re-render during a rename.As per coding guidelines: "Memoize inline arrays containing objects or JSX when passing them as props to avoid unnecessary rerenders."
Also applies to: 678-841
Source: Coding guidelines
673-677: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRename the
AgentRevisionSelectorprop to match the value passed.Line 675 passes
rootEntityId, which is a revision id, into a prop namedvariantId. InsideAgentRevisionSelectorthat same value is forwarded asrevisionIdtoAgentRevisionStatus. The mismatch invites a future caller to pass a real variant id.Rename the prop to
revisionIdinweb/oss/src/components/Playground/Components/AgentRevisionSelector/index.tsxand update this call site.
206-206: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winRemove the unused
BaseContainerPropsextension.There are no additional
PlaygroundHeadercall sites, butPlaygroundHeaderPropsstill extendsHTMLProps<T>and accepts attributes that the component does not consume after destructuring onlyclassName. Restrict the prop type toclassNameand keepkeyavailable via React’s standard props instead.web/packages/agenta-sessions-ui/src/SessionTab.tsx (1)
66-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSpread
restafter the component's own handlers, or compose them.
{...rest}comes first, sorole,aria-selected,tabIndex, and every handler below it win. A host that passesonClick,onKeyDown,onFocus,onBlur,onMouseEnter, oronMouseLeaveloses it without any warning. The file header states that rest props land on the root, so this is a surprising limit on the public contract.Keep the ARIA and
tabIndexdefaults fixed, and compose the pointer/keyboard handlers with the incoming ones.♻️ Proposed composition of host handlers
-export const SessionTab = ({ - active, - label, - statusDot, - renderActions, - onSelect, - className, - ...rest -}: SessionTabProps) => { +export const SessionTab = ({ + active, + label, + statusDot, + renderActions, + onSelect, + className, + onClick: onClickProp, + onKeyDown: onKeyDownProp, + onMouseEnter: onMouseEnterProp, + onMouseLeave: onMouseLeaveProp, + onFocus: onFocusProp, + onBlur: onBlurProp, + ...rest +}: SessionTabProps) => {<div {...rest} role="tab" aria-selected={active} tabIndex={0} - onClick={onSelect} - onKeyDown={onKeyDown} - onMouseEnter={onEnter} - onMouseLeave={onLeave} - onFocus={onEnter} - onBlur={onBlurChip} + onClick={(e) => { + onClickProp?.(e) + onSelect() + }} + onKeyDown={(e) => { + onKeyDownProp?.(e) + onKeyDown(e) + }} + onMouseEnter={(e) => { + onMouseEnterProp?.(e) + onEnter() + }} + onMouseLeave={(e) => { + onMouseLeaveProp?.(e) + onLeave(e) + }} + onFocus={(e) => { + onFocusProp?.(e) + onEnter() + }} + onBlur={(e) => { + onBlurProp?.(e) + onBlurChip(e) + }}web/packages/agenta-sessions-ui/src/SessionTabRail.tsx (1)
198-219: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueHoist the per-row
menuForandonMenuSelectclosures out of the map.Both props are new function identities for every row on every render.
RailTabcallsmenuFor?.(vm)during render throughSessionRowContextMenu, so the menu entries rebuild on each render of each chip. The rail re-renders on every session-list update.Build one stable
menuFor/onMenuSelectpair withuseCallbackthat takes the row id and resolves the index fromorderedIds.web/packages/agenta-sessions/src/state/tabOrder.ts (1)
27-34: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winA hand-arranged session order is lost outside the visible window.
setSessionTabOrderAtomreplaces the stored id list for a scope, and the only caller supplies just the ids the rail currently renders.useSessionCardListcaps that set withlimit, so ids arranged earlier vanish from storage;applySessionTabOrderthen treats those sessions as unseen and moves them to the front on the next visit.
web/packages/agenta-sessions/src/state/tabOrder.ts#L27-L34: mergeidswith the existing entry instead of overwriting it. Keep the incoming order for ids present inids, append the previously stored ids that are absent, and cap the list length so the record cannot grow without bound.web/packages/agenta-sessions-ui/src/SessionTabRail.tsx#L171-L176: keep sending the full visible order, and update the inline comment to state that the atom merges rather than replaces.♻️ Proposed merge in the setter
+/** Keeps a scope's stored order from growing without bound as sessions come and go. */ +const MAX_ORDERED_IDS = 200 + export const setSessionTabOrderAtom = atom( null, (get, set, {scope, ids}: {scope: string; ids: string[]}) => { const projectId = get(projectIdAtom) if (!projectId) return - set(orderByScopeAtom, {...get(orderByScopeAtom), [scopeKey(projectId, scope)]: ids}) + const byScope = get(orderByScopeAtom) + const key = scopeKey(projectId, scope) + const visible = new Set(ids) + // The caller only sees a capped window; ids outside it keep their arrangement. + const merged = [...ids, ...(byScope[key] ?? []).filter((id) => !visible.has(id))] + set(orderByScopeAtom, {...byScope, [key]: merged.slice(0, MAX_ORDERED_IDS)}) }, )web/oss/src/components/AgentChatSlice/state/sessions.ts (1)
255-263: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winDedupe the incoming ids and use a
Setfor themissinglookup.
ids.filter((id) => openSet.has(id))keeps every occurrence of a repeated id. If a producer ever emits a duplicated id, the persisted open-ids order stores that duplicate, which yields duplicate React keys in the tab strip and survives reloads throughatomWithStorage.next.includes(id)also makes themissingscan O(n·m).A
Setfixes both in the same expression.♻️ Proposed refactor
export const reorderSessionsAtomFamily = atomFamily((key: string) => atom(null, (get, set, ids: string[]) => { const open = currentOpenIds(get, key) const openSet = new Set(open) - const next = ids.filter((id) => openSet.has(id)) - const missing = open.filter((id) => !next.includes(id)) + const seen = new Set<string>() + const next = ids.filter((id) => openSet.has(id) && !seen.has(id) && seen.add(id)) + const missing = open.filter((id) => !seen.has(id)) set(openIdsByAppAtom, {...get(openIdsByAppAtom), [key]: [...next, ...missing]}) }), )web/oss/src/components/AgentChatSlice/components/SessionTagBar.tsx (1)
297-307: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winStabilize the
reorderobject and prefertabIdsforremeasureKey.
reorder={{ids: tabIds, onReorder: reorderSessions}}builds a new object on every render. Both members are already stable, soSessionTabStripreceives a changed prop identity for no reason, and any effect it keys onreorderre-runs each render.
remeasureKey={sessions}passes the whole session array. Its identity changes whenever an upstream status or title update rebuilds the list, which remeasures more often than membership changes require.tabIdsalready tracks membership.♻️ Proposed refactor
const tabIds = useMemo(() => sessions.map((session) => session.id), [sessions]) + const reorder = useMemo( + () => ({ids: tabIds, onReorder: reorderSessions}), + [tabIds, reorderSessions], + )extra={extra} - remeasureKey={sessions} - reorder={{ids: tabIds, onReorder: reorderSessions}} + remeasureKey={tabIds} + reorder={reorder}Confirm that
SessionTabStriponly needs membership changes for remeasurement before you switchremeasureKey.As per coding guidelines: "Memoize inline arrays containing objects or JSX when passing them as props to avoid unnecessary rerenders" and "Components should remain focused and decoupled, pass IDs or keys instead of entire data structures".
Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 3ab31028-498e-43f2-ad7b-86f6a3fcf17b
📒 Files selected for processing (47)
web/oss/src/components/AgentChatSlice/AgentConversation.tsxweb/oss/src/components/AgentChatSlice/assets/markdown.tsxweb/oss/src/components/AgentChatSlice/components/Inspector/lenses/RuntimeLens.tsxweb/oss/src/components/AgentChatSlice/components/SessionTagBar.tsxweb/oss/src/components/AgentChatSlice/components/ToolActivity.tsxweb/oss/src/components/AgentChatSlice/hooks/useOnboardingChat.tsweb/oss/src/components/AgentChatSlice/hooks/useSessionActions.tsxweb/oss/src/components/AgentChatSlice/state/panelLayout.tsweb/oss/src/components/AgentChatSlice/state/sessions.tsweb/oss/src/components/Playground/Components/AgentRevisionSelector/index.tsxweb/oss/src/components/Playground/Components/Modals/CommitVariantChangesModal/assets/CommitVariantChangesButton/index.tsxweb/oss/src/components/Playground/Components/Modals/CommitVariantChangesModal/assets/types.d.tsweb/oss/src/components/Playground/Components/PlaygroundHeader/index.tsxweb/oss/src/components/Playground/Components/PlaygroundVariantConfig/assets/PlaygroundVariantConfigHeader.tsxweb/oss/src/components/Playground/Components/PlaygroundVariantConfig/index.tsxweb/oss/src/components/pages/sessions/SessionsPage.tsxweb/packages/agenta-chat/src/state/index.tsweb/packages/agenta-chat/src/state/panelLayout.tsweb/packages/agenta-chat/src/state/sessionEphemera.tsweb/packages/agenta-playground-ui/package.jsonweb/packages/agenta-playground-ui/src/components/AgentBuildPanel.tsxweb/packages/agenta-playground-ui/src/components/AgentConfigHeader.tsxweb/packages/agenta-playground-ui/src/components/AgentPageHeader/AgentPageHeader.tsxweb/packages/agenta-playground-ui/src/components/AgentPageHeader/AgentRevisionStatus.tsxweb/packages/agenta-playground-ui/src/components/AgentPageHeader/index.tsweb/packages/agenta-playground-ui/src/components/CommitVariantChanges/CommitVariantChangesButton.tsxweb/packages/agenta-playground-ui/src/components/CommitVariantChanges/CommitVariantChangesModal.tsxweb/packages/agenta-playground-ui/src/components/CommitVariantChanges/index.tsweb/packages/agenta-playground-ui/src/components/CommitVariantChanges/types.tsweb/packages/agenta-playground-ui/src/components/PlaygroundModeSwitch.tsxweb/packages/agenta-sessions-ui/package.jsonweb/packages/agenta-sessions-ui/src/SessionCardList.tsxweb/packages/agenta-sessions-ui/src/SessionFiltersBar.tsxweb/packages/agenta-sessions-ui/src/SessionFiltersPanel.tsxweb/packages/agenta-sessions-ui/src/SessionListPanel.tsxweb/packages/agenta-sessions-ui/src/SessionTab.tsxweb/packages/agenta-sessions-ui/src/SessionTabDragItem.tsxweb/packages/agenta-sessions-ui/src/SessionTabRail.tsxweb/packages/agenta-sessions-ui/src/SessionTabStrip.tsxweb/packages/agenta-sessions-ui/src/controls/SessionFilterControls.tsxweb/packages/agenta-sessions-ui/src/index.tsweb/packages/agenta-sessions-ui/src/useSessionActions.tsxweb/packages/agenta-sessions/src/state/index.tsweb/packages/agenta-sessions/src/state/tabOrder.tsweb/packages/agenta-sessions/src/state/waitingByAgent.tsweb/packages/agenta-ui/src/components/presentational/layout/FilterRailLayout.tsxweb/packages/agenta-ui/src/components/presentational/layout/index.tsx
💤 Files with no reviewable changes (1)
- web/oss/src/components/Playground/Components/Modals/CommitVariantChangesModal/assets/types.d.ts
|
|
||
| {TEMPLATE_STRIP_MODE ? ( | ||
| <CopiedToast | ||
| open={onboardingChat.copiedToastOpen} | ||
| text={STRIP_COPY.copiedToast} | ||
| onDone={() => onboardingChat.setCopiedToastOpen(false)} | ||
| /> | ||
| ) : null} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Resolve the onboarding hook used by AgentConversation and inspect its returned members.
set -euo pipefail
fd -t f 'AgentConversation.tsx' | xargs -r rg -n 'useOnboardingChat|copiedToastOpen|setCopiedToastOpen'
fd -t f 'useOnboardingChat.ts' | while IFS= read -r f; do
echo "== $f =="
rg -n 'copiedToastOpen|setCopiedToastOpen|return \{' -A 30 "$f"
done
# Any other producer of the copied-toast state?
rg -n 'copiedToastOpen' --glob '*.ts' --glob '*.tsx'Repository: Agenta-AI/agenta
Length of output: 992
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate CopiedToast definitions/usages and the surrounding AgentConversation copy-toast block.
fd -t f 'CopiedToast.*' .
printf '== CopiedToast usages ==\n'
rg -n 'CopiedToast|copiedToastOpen|setCopiedToastOpen' --glob '*.ts' --glob '*.tsx' .
printf '== AgentConversation relevant lines ==\n'
while IFS= read -r f; do
printf '--- %s ---\n' "$f"
wc -l "$f"
sed -n '50,90p' "$f"
sed -n '220,265p' "$f"
sed -n '730,760p' "$f"
done < <(fd -t f 'AgentConversation.tsx' .)
printf '== CopiedToast implementation candidates ==\n'
while IFS= read -r f; do
printf '--- %s ---\n' "$f"
wc -l "$f"
sed -n '1,260p' "$f"
done < <(fd -t f 'CopiedToast.*' .)Repository: Agenta-AI/agenta
Length of output: 1013
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== locate CopiedToast files ==\n'
fd -t f 'CopiedToast\.(tsx|ts)?' .
printf '== CopiedToast implementation candidates ==\n'
while IFS= read -r f; do
printf '--- %s ---\n' "$f"
wc -l "$f"
sed -n '1,260p' "$f"
done < <(fd -t f 'CopiedToast\.(tsx|ts)?' .)
printf '== AgentConversation relevant lines ==\n'
f=$(fd -t f 'AgentConversation.tsx' . | head -n 1)
printf '--- %s ---\n' "$f"
sed -n '1,100p' "$f"
sed -n '220,265p' "$f"
sed -n '730,760p' "$f"
printf '== useOnboardingChat relevant lines ==\n'
f=$(fd -t f 'useOnboardingChat.ts' . | head -n 1)
printf '--- %s ---\n' "$f"
sed -n '200,270p' "$f"Repository: Agenta-AI/agenta
Length of output: 282
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== locate CopiedToast files and imports exactly ==\n'
fd -a 'CopiedToast*' . | sed 's#^\./##'
rg -n 'CopiedToast|copiedToastOpen|setCopiedToastOpen' --glob '*.ts' --glob '*.tsx' . | sed -n '1,120p'
printf '\n== useOnboardingChat top of return and matching files ==\n'
use_file=$(fd 'useOnboardingChat.ts' . | head -n 1)
printf 'file=%s\n' "$use_file"
sed -n '200,270p' "$use_file"
printf '\n== AgentConversation relevant lines ==\n'
conv_file=$(fd 'AgentConversation.tsx' . | head -n 1)
printf 'file=%s\n' "$conv_file"
sed -n '1,100p' "$conv_file" | sed -n '35,70p'
sed -n '220,265p' "$conv_file"
sed -n '730,760p' "$conv_file"
printf '\n== StripHome toast block for comparison ==\n'
strip_file=$(fd 'StripHome.tsx' . | head -n 1)
printf 'file=%s\n' "$strip_file"
sed -n '280,310p' "$strip_file"Repository: Agenta-AI/agenta
Length of output: 9893
🏁 Script executed:
#!/bin/bash
set -euо pipefail
printf '== files matching CopiedToast, excluding current checkout maybe? ==\n'
find web/oss -iname '*copiedtoast*' -print 2>/dev/null | sed -n '1,80p'
printf '\n== exact import path resolution in oss package (TS path aliases config) ==\n'
find web -maxdepth 4 -path '*config*' \( -name '*.json' -o -name '*.js' -o -name '*.mjs' \) -print 2>/dev/null | xargs -r -n 1 sh -c 'echo "--- $0 ---"; rg -n "paths|`@oss`|TemplateStrip|CopiedToast|baseUrl" "$0" || true'
printf '\n== repo-wide CopiedToast string ==\n'
rg -n 'export .*CopiedToast|const CopiedToast|function CopiedToast|CopiedToast|copiedToastOpen|setCopiedToastOpen|openedToast|setOpenedToast|toastOpen|setToastOpen' web/oss/src --glob '*.ts' --glob '*.tsx' --glob '*.json' | sed -n '1,240p'Repository: Agenta-AI/agenta
Length of output: 4807
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== web/oss tsconfig paths relevant section ==\n'
sed -n '1,80p' web/oss/tsconfig.json
printf '\n== TemplateStrip component directory layout ==\n'
find web/oss/src/components/TemplateStrip -maxdepth 3 -type f | sort | sed -n '1,120p'
printf '\n== CopiedToast imports in AgentConversation/StripHome packages ==\n'
rg -n "from \"`@/oss/components/TemplateStrip/components/CopiedToast`\"|from \"`@ama`|from \"\\.\\.\\/components|from \"\\.\\/components" web/oss/src/components/AgentChatSlice/AgentConversation.tsx web/oss/src/components/pages/agent-home/StripHome.tsx
printf '\n== broader component export/search patterns in web/oss/src/components ==\n'
find web/oss/src/components -maxdepth 3 -type d -name '*TemplateStrip*' -print | sort
rg -n "export (default|const|function).*Copied|CopiedToast" web/oss/src/components --glob '*.ts' --glob '*.tsx' | sed -n '1,200p'Repository: Agenta-AI/agenta
Length of output: 4595
Address the missing CopiedToast definition and hook state before rendering the toast.
AgentConversation imports CopiedToast from @/oss/components/TemplateStrip/components/CopiedToast, but no matching component/folder exists under that path; CopiedToast is imported by StripHome as well. Use an exported toast component shared by both pages. useOnboardingChat also does not return copiedToastOpen or setCopiedToastOpen, so this render also needs either added hook state or local toast state.
| title={isAgentWorkflow ? undefined : "Playground"} | ||
| name={ | ||
| isAgentWorkflow ? ( | ||
| renameWorkflowId ? ( | ||
| <AgentNameInline | ||
| workflowId={renameWorkflowId} | ||
| name={agentName} | ||
| onRenamed={setDisplayAgentName} | ||
| /> | ||
| ) : ( | ||
| agentName || "Agent" | ||
| ) | ||
| ) : undefined | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
The header title and name flash the non-agent state while the root revision loads.
isAgentWorkflow is false until the agent signal resolves. During that window title renders "Playground" and name renders undefined. When the signal resolves, the title disappears and the agent name appears.
The code directly below already solves this problem for the action stack. showEvalActions at lines 277-280 waits for a definitive signal and checks !rootEntityQuery.isPending. The title and name slots do not use that guard, so the header text still flashes on an agent reload.
Gate the non-agent title on the same confirmed-prompt signal.
🐛 Proposed fix using the existing confirmed-prompt signal
+ // Same neutral-until-confirmed rule as `showEvalActions`: never render the prompt
+ // title while the agent signal is still pending, or an agent reload flashes "Playground".
+ const showPlaygroundTitle =
+ !isAgentWorkflow &&
+ (earlyAgentState === "non-agent" ||
+ (hasRootNode && !nodeIsAgent && !rootEntityQuery.isPending))
+
return (
<>
<AgentPageHeader
className={className}
leading={leading}
- title={isAgentWorkflow ? undefined : "Playground"}
+ title={showPlaygroundTitle ? "Playground" : undefined}📝 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.
| title={isAgentWorkflow ? undefined : "Playground"} | |
| name={ | |
| isAgentWorkflow ? ( | |
| renameWorkflowId ? ( | |
| <AgentNameInline | |
| workflowId={renameWorkflowId} | |
| name={agentName} | |
| onRenamed={setDisplayAgentName} | |
| /> | |
| ) : ( | |
| agentName || "Agent" | |
| ) | |
| ) : undefined | |
| } | |
| const showPlaygroundTitle = | |
| !isAgentWorkflow && | |
| (earlyAgentState === "non-agent" || | |
| (hasRootNode && !nodeIsAgent && !rootEntityQuery.isPending)) | |
| return ( | |
| <> | |
| <AgentPageHeader | |
| className={className} | |
| leading={leading} | |
| title={showPlaygroundTitle ? "Playground" : undefined} | |
| name={ | |
| isAgentWorkflow ? ( | |
| renameWorkflowId ? ( | |
| <AgentNameInline | |
| workflowId={renameWorkflowId} | |
| name={agentName} | |
| onRenamed={setDisplayAgentName} | |
| /> | |
| ) : ( | |
| agentName || "Agent" | |
| ) | |
| ) : undefined | |
| } |
| if (showAgentHeader && !embedded) { | ||
| return ( | ||
| <AgentConfigHeader | ||
| revisionId={variantId} | ||
| className={className} | ||
| deploy={ | ||
| isEvaluatorEntity ? null : ( | ||
| <DeployVariantButton | ||
| revisionId={variantId} | ||
| label="Deploy" | ||
| type="default" | ||
| size="small" | ||
| /> | ||
| ) | ||
| } | ||
| menu={<PlaygroundVariantHeaderMenu variantId={variantId} />} | ||
| /> |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Restore the OSS commit host adapter for the agent header.
This branch does not pass appId, onAfterCommit, or onCommitted to AgentConfigHeader. The shared header then renders a commit button without the OSS cache refresh and onboarding callbacks.
Pass the selected appId. Also pass callbacks that clear and refresh the registry and evaluator stores and record playground_committed_change. Otherwise agent commits leave OSS list data stale.
| * rail takes its place. This is the single source of truth for the playground's Build/Chat mode — | ||
| * the header switch writes it, and the layout, the config pane and the chat panel all read it. | ||
| */ | ||
| export const chatPanelMaximizedAtom = atom(false) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== file existence and relevant contents =="
fd -a 'panelLayout\.ts$' . | sed 's#^\./##'
file="$(fd 'panelLayout\.ts$' web | head -n 1)"
if [ -n "${file:-}" ]; then
echo "-- $file"
cat -n "$file"
fi
echo "\n== nearby usages of chatPanelMaximizedAtom =="
rg -n "chatPanelMaximizedAtom|chat-panel-maximized|Chat\Wmode|Build\Wmode|maximized" web/packages/agenta-chat web -g 'src/*.{ts,tsx}' -g '!node_modules' || true
echo "\n== package jotai and JSDOM/storage setup =="
fd -p 'package\.json$' web | xargs -r grep -l "jotai" || true
fd -p 'package\.json$' web | xargs -r grep -l "jsdom\|node" || true
fd -e 'ts' -e 'tsx' web | xargs -r grep -l "jotai/utils\|storageKey\|Storage\|setGlobalStorageKey\|JotaiContext" || trueRepository: Agenta-AI/agenta
Length of output: 2455
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Web root package/package-manager context =="
cat -n web/package.json | sed -n '1,220p'
echo "\n== relevant web package.json fields =="
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('web/package.json','utf8'));
console.log(JSON.stringify({
type: pkg.type,
dependencies: pkg.dependencies && Object.fromEntries(Object.entries(pkg.dependencies).filter(([k]) => k.startsWith('`@testing-library`') || k.startsWith('jotai') || k === 'jsdom' || k === 'node')),
devDependencies: pkg.devDependencies && Object.fromEntries(Object.entries(pkg.devDependencies).filter(([k]) => k.startsWith('`@testing-library`') || k.startsWith('jotai') || k === 'jsdom' || k === 'node')),
}, null, 2));
JS
echo "\n== search for JSDOM/browser/globals and Jotai storage setup =="
rg -n "jsdom|jsdom-global|JSDOM|DOMParser|setGlobalStorageKey|Storage\(|jotai/utils\|storageKey|enabledCheck|AtomStoreProvider|JotaiContext" web -g '!node_modules' || true
echo "\n== read exact target file =="
cat -n web/packages/agenta-chat/src/state/panelLayout.ts
echo "\n== agenta-chat package files =="
find web/packages/agenta-chat -maxdepth 3 -type f | sort | sed -n '1,160p'Repository: Agenta-AI/agenta
Length of output: 17371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== session state that already persists =="
cat -n web/packages/agenta-chat/src/state/sessionMessages.ts | sed -n '1,110p'
echo "\n== agenta-chat package exports/imports =="
cat -n web/packages/agenta-chat/src/state/index.ts | sed -n '1,160p'
cat -n web/packages/agenta-chat/package.json | sed -n '1,160p'
echo "\n== tabLocalStorage definition/usage =="
rg -n -C 4 "function tabLocalStorage|const tabLocalStorage|tabLocalStorage\(\\{" web | sed -n '1,220p' || true
echo "\n== vitest env config =="
cat -n web/packages/agenta-chat/vitest.config.ts | sed -n '1,140p'
echo "\n== focused imports of atomWithStorage =="
rg -n 'from "jotai/utils"|from '"'"'jotai/utils'"'"'|atomWithStorage' agenta-chat web/state web/packages -g 'src/**/*.{ts,tsx}' -g '!node_modules' | sed -n '1,240p' || trueRepository: Agenta-AI/agenta
Length of output: 11698
Persist the selected chat layout mode.
This atom controls the Build/Chat layout and resets after a refresh because it is in-memory. Store it with atomWithStorage using an agenta:-prefixed key, and keep the tab-scoped storage pattern used by nearby persisted atoms.
Proposed fix
-import {atom} from "jotai"
+import {atomWithStorage, createJSONStorage} from "jotai/utils"
+
+const tabLocalStorage = () => {
+ const storage = createJSONStorage<boolean>()
+ delete storage.subscribe
+ return storage
+}
+
+export const chatPanelMaximizedAtom = atomWithStorage<boolean>(
+ "agenta:agent-chat:panel-maximized",
+ false,
+ tabLocalStorage(),
+ {getOnInit: true} as const,
+)📝 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 chatPanelMaximizedAtom = atom(false) | |
| import {atomWithStorage, createJSONStorage} from "jotai/utils" | |
| const tabLocalStorage = () => { | |
| const storage = createJSONStorage<boolean>() | |
| delete storage.subscribe | |
| return storage | |
| } | |
| export const chatPanelMaximizedAtom = atomWithStorage<boolean>( | |
| "agenta:agent-chat:panel-maximized", | |
| false, | |
| tabLocalStorage(), | |
| {getOnInit: true} as const, | |
| ) |
Source: Coding guidelines
| <section | ||
| className={`h-[48px] flex items-center justify-between overflow-hidden ${ | ||
| embedded ? "grow" : "sticky top-0 z-[10] w-full" | ||
| } border-b border-colorBorderSecondary py-2 px-4 bg-[var(--ag-c-FFFFFF)] bg-[image:linear-gradient(var(--ag-colorFillTertiary),var(--ag-colorFillTertiary))] ${ |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Replace the legacy raw color token.
--ag-c-FFFFFF is a forbidden raw color token. It also fixes the base layer to white in dark appearance. Use a semantic Tailwind color utility or a supported var(--ag-color*) value.
As per coding guidelines, “Consume theme colors through Ant Design semantic tokens, Tailwind color utilities, or supported var(--ag-color*) variables; do not use raw hex colors or --ag-c-* literals.”
Source: Coding guidelines
| <nav | ||
| className={`flex gap-2 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ${ | ||
| className ?? "" | ||
| }`} | ||
| > |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use role="group" with a label instead of <nav>.
This row is a filter control, not site navigation. <nav> registers a navigation landmark in the assistive-technology landmark list, and it has no accessible name here. SessionStatusListControl in this same file already presents the identical status choice with role="group" and aria-label="Filter sessions by status". Match it.
🛠️ Proposed change
- <nav
+ <div
+ role="group"
+ aria-label="Filter sessions by status"
className={`flex gap-2 overflow-x-auto [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden ${
className ?? ""
}`}
>Close the element with </div> at line 176.
| <button | ||
| type="button" | ||
| aria-label="Filters" | ||
| className="box-border flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-lg border-0 bg-colorFillQuaternary px-2.5 text-sm text-colorTextSecondary" | ||
| > | ||
| <FunnelIcon size={16} weight={activeCount ? "fill" : "regular"} /> | ||
| Filters | ||
| {activeCount ? ( | ||
| <span className="rounded bg-colorFillSecondary px-1.5 py-0.5 text-[11px] leading-none text-colorText"> | ||
| {activeCount} | ||
| </span> | ||
| ) : null} | ||
| </button> |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add visible hover and keyboard-focus states.
The Filters and Clear buttons define base and disabled styles, but they do not define hover or focus-visible styles. Add semantic interaction states for both buttons.
As per coding guidelines, "Implement light and dark appearance and interaction states for every added or changed UI element, and verify both themes."
Also applies to: 117-128
Source: Coding guidelines
| onClick={() => { | ||
| setAgentId(null) | ||
| setMode(false) | ||
| setIncludeArchived(false) | ||
| }} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Do not clear a hidden agent filter.
When showAgent is false, activeCount does not include agentId. A user who clears Mode or Include then also loses an agent filter that the sheet does not show.
Only call setAgentId(null) when showAgent is true.
Proposed fix
onClick={() => {
- setAgentId(null)
+ if (showAgent) setAgentId(null)
setMode(false)
setIncludeArchived(false)
}}📝 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.
| onClick={() => { | |
| setAgentId(null) | |
| setMode(false) | |
| setIncludeArchived(false) | |
| }} | |
| onClick={() => { | |
| if (showAgent) setAgentId(null) | |
| setMode(false) | |
| setIncludeArchived(false) | |
| }} |
| const pressTimer = useRef<ReturnType<typeof setTimeout> | null>(null) | ||
|
|
||
| const clearPress = useCallback(() => { | ||
| if (pressTimer.current) clearTimeout(pressTimer.current) | ||
| pressTimer.current = null | ||
| }, []) | ||
|
|
||
| const onPointerDown = useCallback( | ||
| (event: React.PointerEvent<HTMLDivElement>) => { | ||
| clearPress() | ||
| // A press on the chip's own controls is that control's — dragging out of a rename input | ||
| // would steal the text selection, and out of the close button its click. | ||
| if ( | ||
| (event.target as HTMLElement | null)?.closest( | ||
| "input, textarea, button, [contenteditable='true']", | ||
| ) | ||
| ) { | ||
| return | ||
| } | ||
| if (event.pointerType !== "touch") { | ||
| controls.start(event) | ||
| return | ||
| } | ||
| if (!touchDrag) return | ||
| pressTimer.current = setTimeout(() => controls.start(event), longPressMs) | ||
| }, | ||
| [clearPress, controls, longPressMs, touchDrag], | ||
| ) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Clear pressTimer on unmount.
clearPress runs on pointer up, cancel, leave, and drag end. It does not run on unmount. If the chip unmounts while a touch is still held, no pointer event arrives, the timer fires afterwards, and controls.start(event) runs against a detached item. The rail re-renders from a live session query, so an unmount during a press is possible.
Add an unmount cleanup.
🛡️ Proposed cleanup on unmount
-import {useCallback, useRef, useState, type ReactNode, type Ref} from "react"
+import {useCallback, useEffect, useRef, useState, type ReactNode, type Ref} from "react" const clearPress = useCallback(() => {
if (pressTimer.current) clearTimeout(pressTimer.current)
pressTimer.current = null
}, [])
+
+ useEffect(() => clearPress, [clearPress])📝 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 pressTimer = useRef<ReturnType<typeof setTimeout> | null>(null) | |
| const clearPress = useCallback(() => { | |
| if (pressTimer.current) clearTimeout(pressTimer.current) | |
| pressTimer.current = null | |
| }, []) | |
| const onPointerDown = useCallback( | |
| (event: React.PointerEvent<HTMLDivElement>) => { | |
| clearPress() | |
| // A press on the chip's own controls is that control's — dragging out of a rename input | |
| // would steal the text selection, and out of the close button its click. | |
| if ( | |
| (event.target as HTMLElement | null)?.closest( | |
| "input, textarea, button, [contenteditable='true']", | |
| ) | |
| ) { | |
| return | |
| } | |
| if (event.pointerType !== "touch") { | |
| controls.start(event) | |
| return | |
| } | |
| if (!touchDrag) return | |
| pressTimer.current = setTimeout(() => controls.start(event), longPressMs) | |
| }, | |
| [clearPress, controls, longPressMs, touchDrag], | |
| ) | |
| const pressTimer = useRef<ReturnType<typeof setTimeout> | null>(null) | |
| const clearPress = useCallback(() => { | |
| if (pressTimer.current) clearTimeout(pressTimer.current) | |
| pressTimer.current = null | |
| }, []) | |
| useEffect(() => clearPress, [clearPress]) | |
| const onPointerDown = useCallback( | |
| (event: React.PointerEvent<HTMLDivElement>) => { | |
| clearPress() | |
| // A press on the chip's own controls is that control's — dragging out of a rename input | |
| // would steal the text selection, and out of the close button its click. | |
| if ( | |
| (event.target as HTMLElement | null)?.closest( | |
| "input, textarea, button, [contenteditable='true']", | |
| ) | |
| ) { | |
| return | |
| } | |
| if (event.pointerType !== "touch") { | |
| controls.start(event) | |
| return | |
| } | |
| if (!touchDrag) return | |
| pressTimer.current = setTimeout(() => controls.start(event), longPressMs) | |
| }, | |
| [clearPress, controls, longPressMs, touchDrag], | |
| ) |
| return useMemo(() => { | ||
| const counts = new Map<string, number>() | ||
| for (const row of rowsFromPages(waitingQuery.data?.pages)) { | ||
| const appId = sessionOpenTarget(row)?.appId | ||
| if (appId) counts.set(appId, (counts.get(appId) ?? 0) + 1) | ||
| } | ||
| return counts | ||
| }, [waitingQuery.data?.pages]) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Find file and package manifests"
git ls-files | rg '(^|/)running|(^|/)waitingByAgent.ts$|package.json$|pnpm-lock.yaml$' | head -200
echo
echo "Inspect target file"
if [ -f web/packages/agenta-sessions/src/state/waitingByAgent.ts ]; then
cat -n web/packages/agenta-sessions/src/state/waitingByAgent.ts
fi
echo
echo "Search useSessionList and queries"
rg -n "useSessionList|placeholderData|keepPreviousData|isPlaceholderData|waitingIds|waitingQuery" web/packages -SRepository: Agenta-AI/agenta
Length of output: 8681
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Target useSessionList"
cat -n web/packages/agenta-sessions/src/state/useSessionList.ts
echo
echo "package version"
node - <<'JS'
const fs = require('fs')
const pkg = JSON.parse(fs.readFileSync('web/packages/agenta-sessions/package.json', 'utf8'))
console.log(pkg.dependencies?.['`@tanstack/react-query`'] ?? pkg.devDependencies?.['`@tanstack/react-query`'] ?? 'not declared in package')
JS
echo
echo "lock version snippets"
rg -n '"`@tanstack/react-query`"|"`@tanstack/react-query`@' web/pnpm-lock.yaml | head -100Repository: Agenta-AI/agenta
Length of output: 7807
🌐 Web query:
TanStack Query infinite query placeholderData isPlaceholderData keepPreviousData documentation
💡 Result:
In TanStack Query (v5 and later), the keepPreviousData option has been removed in favor of using placeholderData [1]. To achieve the "keep previous data" functionality—where cached data from a previous query remains visible while a new query is fetching—you should now use the placeholderData option combined with the keepPreviousData identity function provided by TanStack Query [2][1]. For useInfiniteQuery, the placeholderData option works similarly to useQuery [3][4]. When the query key changes (for example, when a filter is applied to an infinite list), providing placeholderData: keepPreviousData allows the UI to continue displaying the old infinite data while the new data is being fetched [3][2]. Key points regarding this implementation: - Import keepPreviousData: You must import keepPreviousData from @tanstack/react-query [1]. - Usage: Set placeholderData: keepPreviousData in your useInfiniteQuery hook options [3][4]. - Data Structure: When using placeholderData with useInfiniteQuery, the placeholder data must match the required InfiniteData structure (an object containing pages and pageParams) [5][6]. - isPlaceholderData: When data is being served from the cache via placeholderData, the isPlaceholderData boolean will be true [2][4]. This is useful for indicating to the user that the displayed data is "stale" or from a previous state while a new request is in progress [3][2]. - Migration: If you are migrating from older versions of TanStack Query, the isPreviousData flag has also been replaced by isPlaceholderData [1]. Using placeholderData: keepPreviousData is the recommended, declarative way to maintain a seamless user experience during state transitions in infinite queries [3][1].
Citations:
- 1: https://tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5
- 2: https://tanstack.com/query/v5/docs/framework/react/guides/placeholder-query-data
- 3: https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries
- 4: https://tanstack.com/query/v5/docs/framework/react/guides/paginated-queries
- 5: https://tanstack.com/query/latest/docs/framework/react/guides/infinite-queries
- 6: https://tanstack.com/query/v5/docs/framework/react/guides/infinite-queries
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "All waitingByAgent uses and counts"
rg -n "useWaitingByAgent|waitingByAgent|waitingSessionIds|waitingQuery" web -S
echo
echo "package files mentioning tanstack"
rg -n '"`@tanstack/react-query`"|"\`@tanstack/react-query`"' web/package.json web/packages/agenta-sessions/package.json web/pnpm-lock.yaml || true
echo
echo "Inspect sessionListQueryOptions and queryFn if accessible"
git ls-files | rg 'session.*\.ts$|session.*\.tsx$' | rg '`@agenta/entities`'Repository: Agenta-AI/agenta
Length of output: 3759
Ignore placeholder rows when calculating waiting counts.
useSessionList keeps prior pages behind placeholderData: keepPreviousData. When waitingIds becomes empty and the query is disabled, waitingQuery.data can still contain old waiting rows, so useWaitingByAgent returns stale counts. If waitingQuery.isPlaceholderData is true, return an empty counts map.
Proposed fix
return useMemo(() => {
const counts = new Map<string, number>()
+ if (waitingQuery.isPlaceholderData) return counts
+
for (const row of rowsFromPages(waitingQuery.data?.pages)) {
const appId = sessionOpenTarget(row)?.appId
if (appId) counts.set(appId, (counts.get(appId) ?? 0) + 1)
}
return counts
-}, [waitingQuery.data?.pages])
+}, [waitingQuery.data?.pages, waitingQuery.isPlaceholderData])📝 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.
| return useMemo(() => { | |
| const counts = new Map<string, number>() | |
| for (const row of rowsFromPages(waitingQuery.data?.pages)) { | |
| const appId = sessionOpenTarget(row)?.appId | |
| if (appId) counts.set(appId, (counts.get(appId) ?? 0) + 1) | |
| } | |
| return counts | |
| }, [waitingQuery.data?.pages]) | |
| return useMemo(() => { | |
| const counts = new Map<string, number>() | |
| if (waitingQuery.isPlaceholderData) return counts | |
| for (const row of rowsFromPages(waitingQuery.data?.pages)) { | |
| const appId = sessionOpenTarget(row)?.appId | |
| if (appId) counts.set(appId, (counts.get(appId) ?? 0) + 1) | |
| } | |
| return counts | |
| }, [waitingQuery.data?.pages, waitingQuery.isPlaceholderData]) |
…ove into @agenta/playground-ui
… into @agenta/sessions-ui
…-c-* literals The config bar's opaque base was `--ag-c-FFFFFF`, a codemod shim that only stops being white in dark mode because it happens to alias the container role; name that role (`bg-colorBgContainer`) outright, so the translucent fill still layers over a real surface in both themes. The agent glyph's `--ag-c-13C2C2` becomes `text-cyan-6`, the generated antd scale token the desktop's other agent badges already use.
Two ways past it: the custom-child branch ignored `disabled` entirely, and the
standard branch spread `{...props}` AFTER it, so a caller's `disabled` won. Either
let an unchanged persisted variant open the commit modal. One effective value now,
applied after the spread and re-checked inside the open handler, so it holds
whichever branch renders — a caller may tighten the guard, never loosen it.
The modal's `handleSubmit` also closed over a stale `onAfterCommit`: a host that
re-renders the prop while the other deps hold still kept the old closure, so its
cache refresh never ran.
…dies with its chip `useSessionList` keeps previous data as a placeholder, so when nothing is waiting the query goes disabled and the last waiting page stood forever — the rail kept counting sessions that had already been answered. Count only rows the current id set produced. `SessionTabDragItem` cleared its long-press timer on pointer up/cancel/leave and drag end, none of which fire if the chip unmounts mid-press — and the strip re-renders off a live session query, so that is reachable. The timer then called `controls.start` for an item no longer in the Reorder group.
With the agent picker hidden, `activeCount` already excluded `agentId` — but Clear still reset it, so clearing Mode or Include silently widened an agent-scoped list to every agent. The button's enabled state and its effect now describe the same set. The status chip strip was a `<nav>`, which registers an unnamed navigation landmark for something that is not navigation; it becomes the same named `role="group"` its stacked twin already uses. Filters and Clear gain hover and focus-visible states — they had base and disabled only, so neither answered the keyboard.
A refresh is not a request to rearrange the window, but the layout mode was a plain atom, so anyone working in Chat landed back in Build on every reload. It persists on the same storage the package's other stores use — extracted out of sessionMessages rather than copied — so a write in one browser tab does not rearrange another. The value stays global, exactly as it was in memory; the mode belongs to the playground surface, not to a session. No `getOnInit`: this page server-renders, and reading localStorage during atom init would break hydration.
…its for a verdict Moving the agent config bar into the package took its commit button with it, and the OSS host stopped passing `appId`/`onAfterCommit`/`onCommitted` — so an agent commit skipped the registry and evaluator cache refresh and the onboarding event, leaving both lists stale. The adapter those props carry is now a hook, so the button wrapper and the agent header share one definition instead of the header re-deriving it. The page header read `isAgentWorkflow`, which is false while agent-ness is merely UNKNOWN, so the title flashed "Playground" on every agent reload before swapping to the agent identity. It waits for the same confirmed signal the eval action stack already waits for.
7125323 to
a4bfc3d
Compare
7d387ac to
f7f967f
Compare
Two commits. First the agent page header, build panel and commit modal move into
@agenta/playground-ui. Then the session tab rail, list panel and filters bar move into@agenta/sessions-ui.Together they are what
/m's tabbed session workspace is built from a few lanes up.Not run in a browser — static gates only (
pnpm lint-fix24/24,tsc --noEmitcleanfor
@agenta/shared,ui,entities,entity-ui,settings-ui,oss,ee,mobile).Stacked on
pkg/navigation-shell; review only this lane's diff.