Single, self-contained work order for the ADR-0057 implementation — written for an agent with no prior conversation context. Rolls up to epic #2409.
Read first: docs/adr/0057-console-ai-chat-one-conversation-docked.md (merged in #2411), plus upstream invariants cloud ADR-0063 (two agents, surface-bound) and cloud ADR-0025 (OSS = MCP only).
Objective
Collapse the console's fragmented AI-chat surfaces into one chat system, on the principle:
surface = view · conversation = model · product (ask / build) = binding axis.
This is NOT a merge of ask and build — cloud ADR-0063 already fixed the agent model (two products, bound by surface, no roster, no per-turn classifier; it reversed ADR-0040's unified classifier — do not reintroduce it). This is objectui console-layer consolidation only. No cloud backend change except the optional deps below.
⛔ Repo workflow (mandatory)
- Worktree-first. This repo is edited by multiple agents and a PreToolUse hook blocks edits outside a dedicated worktree. Create one:
git worktree add ../objectui-<task> -b <branch> main && cd ../objectui-<task> && pnpm install (see AGENTS.md / CLAUDE.md).
- One PR per phase, reversible, each with an ADR-0054 browser proof. No big-bang: P3 touches the most-used AI entry (the FAB) and the Studio layout.
Current state (starting points — line numbers drift, grep the symbols)
Three shells over one backend (POST /api/v1/ai/agents/:name/chat), forking the conversation:
packages/app-shell/src/layout/ConsoleFloatingChatbot.tsx — bottom-right FAB (ask); agent pick ~L915; wired to assistant/assistantBus.ts + useReconcileOnError.
packages/app-shell/src/console/ai/AiChatPage.tsx — full-page /ai/build + /ai/ask + Live Canvas (ADR-0037); resolve ~L610; exports ChatPane.
packages/app-shell/src/views/studio-design/StudioAiCopilot.tsx — left-docked build copilot (ADR-0080 aiSlot); forks the thread via scope studio:${packageId}:${agent} (~L56); reuses ChatPane.
packages/plugin-chatbot/src/renderer.tsx — SDUI type:'chatbot' embedded in runtime end-user apps.
Two state layers: app-shell (useChatConversation, useConversationList, useAiSurface, conversationLanguage, reconcileTurn, assistantBus) vs plugin-chatbot (useObjectChat, useAgents, usePendingActions, useHitlInChat, agentAliases).
~5 divergent surface→agent picks (each re-implements the ADR-0063 chain): StudioAiCopilot.tsx:44, ConsoleFloatingChatbot.tsx:915, ConsoleLayout.tsx:84 (unique !aiStudioEnabled && isBuildAgent → downgrade special case), AiChatPage.tsx:610, layout/agentPicker.ts:46.
Two gates: hooks/useAiSurface.ts (correct — access-filtered agent catalog, ADR-0068) vs discovery.services.ai / isAiEnabled (per-user 403 gap; useAiSurface's docstring explicitly warns against it).
Already shared (don't rebuild): ChatbotEnhanced render body, the /api/v1/ai/agents/:name/chat transport, and the assistantBus context bridge (editor context / open / review / canvas-invalidation signals).
Phases
P1 — one conversation context + one gate (no visible change) — LAND FIRST
- Introduce
ChatSessionProvider (or consolidate into @object-ui/plugin-chatbot) owning agent resolution, conversation binding, list, streaming, pending actions, reconcile. All three shells become thin views over it.
- Key conversations on
(user, app, product) — change StudioAiCopilot's studio:${packageId}:${agent} → app:${packageId}:${product} so the full-page focus view and the docked copilot share ONE thread.
- Delete
isAiEnabled / discovery.services.ai; use useAiSurface everywhere.
- Acceptance: same thread visible in full-page
/ai/build ↔ Studio copilot for one app; seat-less user sees no AI surface; no other visible change.
P2 — declarative surface→agent resolver
const SURFACE_DEFAULT = { 'studio-build': 'build', 'default': 'ask' } as const;
function resolveSurfaceAgent(surface, { catalog, appDefaultAgent, aiStudioEnabled }) {
const want = appDefaultAgent ?? SURFACE_DEFAULT[surface] ?? 'ask';
const eff = !aiStudioEnabled && isBuildAgent(want) ? 'ask' : want; // ConsoleLayout special case, folded in ONCE
return resolveAgentParam(eff, catalog) ?? resolveDefaultAgentName(catalog);
}
- Migrate every call site (StudioAiCopilot, ConsoleFloatingChatbot, ConsoleLayout, AiChatPage, agentPicker) to it; delete the scattered picks + the ConsoleLayout downgrade.
- Acceptance: unit table for ADR-0063 — Studio→build, console/app→ask, aiStudio-off downgrade,
app.defaultAgent bounded to ask/build (reject anything else). No roster/classifier representable.
P3 — docked rail + Studio right reflow; retire the FAB — ⚠ needs founder sign-off (dock side)
ChatDock: render the shared panel as a right, collapsible, resizable rail, default collapsed (keep the FAB's zero-idle-cost). FAB → launcher / ⌘I toggle.
/ai = the same panel maximized (build + Live Canvas), same thread; deep links open the dock maximized.
- Move the Studio copilot into the shared right dock → Studio reflows to
[left: nav/tree] [center: canvas+properties] [right: chat].
- Runtime end-user apps (SDUI
chatbot) keep the bubble — same state code, different chrome.
- Optional dep objectstack-ai/cloud#816 (render debug/canvas/resume by declared
capabilities, not isBuildAgent checks). Non-blocking.
- Acceptance: chat location identical across console pages; expand/collapse preserves the thread; Studio 3-pane reflow; runtime bubble unchanged.
P4 — ask→build explicit handoff — ⚠ needs founder sign-off (explicit vs seamless); dep cloud#817
- In
ask, a build-shaped request surfaces an explicit "Open in Builder →" carrying the conversation context via the generalized assistantBus (ADR-0063 decline-and-redirect — never a silent re-route into authoring).
- Dep objectstack-ai/cloud#817 upgrades a context-less handoff to context-carrying.
- Acceptance: ask thread opens the Builder with context; no authoring ever happens on the ask surface.
Invariants (do not violate)
Verification (end-to-end)
- Boot the cloud local stack (serves
/api/v1/ai/*): cd <cloud> && OBJECTUI_ROOT=<objectui> OS_MULTI_ORG_ENABLED=true CLOUD_PORT=4000 OBJECTOS_PORT=5050 bash scripts/dev-local/run-stack.sh --with-ui --seed --keep --workdir /tmp/os-stack. Needs cloud/scripts/dev-local/.env.local with AI_GATEWAY_API_KEY or the agent catalog is empty.
- Backend/API checks work headless with the seeded session:
curl GET /api/v1/ai/agents → build,ask.
- UI checks: the
better-auth.session_token cookie is httpOnly — can't be JS-injected, and the automation browser tab is a separate Chrome profile from the user's, so UI verification needs a login into the controlled tab (or a maintainer). Backend verification needs no browser.
- Each phase ships an ADR-0054 UI-testability proof.
Open founder decisions (block P3/P4 only)
- Dock side + Studio reflow (chat vs properties on the right).
- Handoff explicit "Open in Builder" vs a more seamless in-place switch.
- P1/P2 do not depend on these — start there.
Related
Epic #2409 · ADR docs/adr/0057-console-ai-chat-one-conversation-docked.md (#2411) · Cloud deps objectstack-ai/cloud#816 (P3, optional), objectstack-ai/cloud#817 (P4).
Single, self-contained work order for the ADR-0057 implementation — written for an agent with no prior conversation context. Rolls up to epic #2409.
Read first:
docs/adr/0057-console-ai-chat-one-conversation-docked.md(merged in #2411), plus upstream invariants cloud ADR-0063 (two agents, surface-bound) and cloud ADR-0025 (OSS = MCP only).Objective
Collapse the console's fragmented AI-chat surfaces into one chat system, on the principle:
This is NOT a merge of
askandbuild— cloud ADR-0063 already fixed the agent model (two products, bound by surface, no roster, no per-turn classifier; it reversed ADR-0040's unified classifier — do not reintroduce it). This is objectui console-layer consolidation only. No cloud backend change except the optional deps below.⛔ Repo workflow (mandatory)
git worktree add ../objectui-<task> -b <branch> main && cd ../objectui-<task> && pnpm install(see AGENTS.md / CLAUDE.md).Current state (starting points — line numbers drift, grep the symbols)
Three shells over one backend (
POST /api/v1/ai/agents/:name/chat), forking the conversation:packages/app-shell/src/layout/ConsoleFloatingChatbot.tsx— bottom-right FAB (ask); agent pick ~L915; wired toassistant/assistantBus.ts+useReconcileOnError.packages/app-shell/src/console/ai/AiChatPage.tsx— full-page/ai/build+/ai/ask+ Live Canvas (ADR-0037); resolve ~L610; exportsChatPane.packages/app-shell/src/views/studio-design/StudioAiCopilot.tsx— left-docked build copilot (ADR-0080aiSlot); forks the thread via scopestudio:${packageId}:${agent}(~L56); reusesChatPane.packages/plugin-chatbot/src/renderer.tsx— SDUItype:'chatbot'embedded in runtime end-user apps.Two state layers: app-shell (
useChatConversation,useConversationList,useAiSurface,conversationLanguage,reconcileTurn,assistantBus) vs plugin-chatbot (useObjectChat,useAgents,usePendingActions,useHitlInChat,agentAliases).~5 divergent surface→agent picks (each re-implements the ADR-0063 chain):
StudioAiCopilot.tsx:44,ConsoleFloatingChatbot.tsx:915,ConsoleLayout.tsx:84(unique!aiStudioEnabled && isBuildAgent → downgradespecial case),AiChatPage.tsx:610,layout/agentPicker.ts:46.Two gates:
hooks/useAiSurface.ts(correct — access-filtered agent catalog, ADR-0068) vsdiscovery.services.ai/isAiEnabled(per-user 403 gap;useAiSurface's docstring explicitly warns against it).Already shared (don't rebuild):
ChatbotEnhancedrender body, the/api/v1/ai/agents/:name/chattransport, and theassistantBuscontext bridge (editor context / open / review / canvas-invalidation signals).Phases
P1 — one conversation context + one gate (no visible change) — LAND FIRST
ChatSessionProvider(or consolidate into@object-ui/plugin-chatbot) owning agent resolution, conversation binding, list, streaming, pending actions, reconcile. All three shells become thin views over it.(user, app, product)— changeStudioAiCopilot'sstudio:${packageId}:${agent}→app:${packageId}:${product}so the full-page focus view and the docked copilot share ONE thread.isAiEnabled/discovery.services.ai; useuseAiSurfaceeverywhere./ai/build↔ Studio copilot for one app; seat-less user sees no AI surface; no other visible change.P2 — declarative surface→agent resolver
app.defaultAgentbounded to ask/build (reject anything else). No roster/classifier representable.P3 — docked rail + Studio right reflow; retire the FAB — ⚠ needs founder sign-off (dock side)
ChatDock: render the shared panel as a right, collapsible, resizable rail, default collapsed (keep the FAB's zero-idle-cost). FAB → launcher / ⌘I toggle./ai= the same panel maximized (build + Live Canvas), same thread; deep links open the dock maximized.[left: nav/tree] [center: canvas+properties] [right: chat].chatbot) keep the bubble — same state code, different chrome.capabilities, notisBuildAgentchecks). Non-blocking.P4 — ask→build explicit handoff — ⚠ needs founder sign-off (explicit vs seamless); dep cloud#817
ask, a build-shaped request surfaces an explicit "Open in Builder →" carrying the conversation context via the generalizedassistantBus(ADR-0063 decline-and-redirect — never a silent re-route into authoring).Invariants (do not violate)
ask/build), bound by surface, no roster, no per-turn classifier; a build-shaped request onaskis declined+redirected, never silently rerouted.Verification (end-to-end)
/api/v1/ai/*):cd <cloud> && OBJECTUI_ROOT=<objectui> OS_MULTI_ORG_ENABLED=true CLOUD_PORT=4000 OBJECTOS_PORT=5050 bash scripts/dev-local/run-stack.sh --with-ui --seed --keep --workdir /tmp/os-stack. Needscloud/scripts/dev-local/.env.localwithAI_GATEWAY_API_KEYor the agent catalog is empty.curl GET /api/v1/ai/agents→build,ask.better-auth.session_tokencookie is httpOnly — can't be JS-injected, and the automation browser tab is a separate Chrome profile from the user's, so UI verification needs a login into the controlled tab (or a maintainer). Backend verification needs no browser.Open founder decisions (block P3/P4 only)
Related
Epic #2409 · ADR
docs/adr/0057-console-ai-chat-one-conversation-docked.md(#2411) · Cloud deps objectstack-ai/cloud#816 (P3, optional), objectstack-ai/cloud#817 (P4).