Skip to content

Commit f4db5f3

Browse files
fix(app-shell): the console AI dock names the selected app and the page in view in its chat request (#30043) (#30047)
<!-- claude-projects-attribution: {"github_login":"os-zhuang"} --> _Requested by **Sam** · [project thread](https://claude.ai/code/project/chan_01GsBogozeZG5sdpAARrN7HP?thread=cmsg_01GsBogozeZG5sdpAARrN7HPQri8yJvYhNpGXfbb2mtPTE)_ Part of #30043. The card carries no `Claim:` naming this branch, so the closing-target guard would refuse a closing keyword here; the dispatching seat closes the card after this lands (or posts the claim and switches this line to a closing keyword). ## What changes The console's AI chat request (`POST /api/v1/ai/agents/:agent/chat`) now names the app the user has selected and the page in view, on every chat surface, the floating dock included. - `ui/packages/app-shell/src/console/ai/AiChatPage.tsx` (`ChatPane`): `context.activeApp` is no longer the constant `'AI'`. It is the selected app's package: the new `activeAppPackageId` prop (the console app the dock sits in), else `editPackageId` (the app picked for editing on `/ai/build?package=…`, the Studio copilot's package). With neither, the key is omitted. The `surfaceContext` prop takes the shared `ChatSurfaceContext` type, which adds cloud#1610's existing `selection` key to the client side. - `ui/packages/app-shell/src/layout/chatSurfaceContext.ts` (new): `consoleChatSurface(pathname, hostPaths)` reads the page in view off the URL into cloud#1610's shape. It matches against `APP_ROUTE_METADATA_READS`, the route table `AppContent` is held equal to, ranked the way `<Routes>` ranks it, so a static route (`search`, `reports`) or a host route never reads as an object. - object list, data, new: `{ artifact: { type: 'object', name } }`; a saved list view adds `selection: { kind: 'view', id }` - record page (and its edit form and named record pages): `{ artifact: { type: 'object', name }, selection: { kind: 'record', id } }` - dashboard, report, page: `{ artifact: { type, name } }`; the dashboard designer adds `mode: 'design'` - anything else (app landing, search, component pages, metadata admin, redirects): no `surface` - `ui/packages/app-shell/src/layout/ConsoleLayout.tsx`: computes the dock's surface from `location.pathname` each render and hands it, with the object's translated label for the "discussing" chip, to both dock presentations (desktop rail and phone sheet). `AppContent` passes its host route paths in. - `ChatDock.tsx` / `ChatDockConversation.tsx`: forward `surfaceContext` / `surfaceArtifactLabel`, and pass the console app's package as `activeAppPackageId` whichever agent the dock runs. - `useObjectChat.transportIdentity.test.tsx`: the fixture no longer models `activeApp: 'AI'`. - Changeset: `@object-ui/app-shell` patch. ## Why objectstack#30043 (the objectstack half of cloud#3564). Measured on staging by scripting the dock's own body on an object list page: `{activeApp:"AI", agentName:"build", autoPublishAiBuilds:true, packageId:"app.jpph"}`. The agent was told the user was in "AI" and was never told the page or record, so "who owns this record?" was answered with "please give me the record's name", and "how do I use this app?" was answered for another app. How the agent uses these fields is cloud#3564's half. The value is read per send: `useObjectChat` reads `body` through a ref at send time (pinned since objectui#4187), and the dock's pane is keyed on the conversation, not on the page, so navigating with the dock open moves the next turn's context without remounting the pane. ## Verification - `ChatDock.requestContext-30043.test.tsx` (new): real `ConsoleLayout`, real dock (rail and sheet), real lazy `ChatDockConversation`, real `ChatPane`, real `useObjectChat`; the pinned value is the JSON body of the POST captured off a `fetch` stub. Six cases: record page (activeApp + object + record id), object list page (the measured page), build agent bound to the app (activeApp = packageId), navigation with the dock open (list → record → search drops the key), no app selected (no `activeApp`, no `'AI'` anywhere), phone sheet. With `AiChatPage.tsx` and `ChatDockConversation.tsx` put back to `origin/main` on the committed tree, all 6 fail (`expected 'AI' to be 'app.crm'`, `expected undefined to deeply equal { artifact … }`, `expected { activeApp: 'AI', … } to not have property "activeApp"`); restored, all 6 pass. - `chatSurfaceContext-30043.test.ts` (new): 8 cases over the route mapping, including decoded segments, static and host routes that shadow `:objectName`, and a guard that every mapped path is a route the console declares. - Local, on `1aa3407a`: the two new files plus every `layout/__tests__/ChatDock*`, `layout/__tests__/ConsoleLayout*`, `console/ai/__tests__/*` and the Studio copilot tests: 65 files, 368 tests, all pass. `useObjectChat.transportIdentity.test.tsx`: 5/5. - `@object-ui/app-shell` `type-check`: NOT MEASURED locally (this container has not built the `@object-ui/*` declaration closure, so tsc reports unresolved modules across the whole package, untouched files included). `Type Check UI` in CI is the reading. Clause-②: no Metadata-protocol: none ## Acceptance notes - **Record name is not sent.** The card's Do 2 lists "object + record id + record name" and also says "use the existing cloud#1610 shape; do not add a second one". That shape (`ToolExecutionContext.surfaceContext` in the spec, and cloud's `sanitizeSurfaceContext`) has no slot for a record's display name: `selection` is `{ kind, id }`, and cloud drops any other key. A name also has no race-free source in the dock today: the record page publishes its title to `NavigationContext` unkeyed, and `RecordDetailView` keeps the previous record's title until the next record loads, so a name read there could pair record B's id with record A's name. This PR sends the object and the record id, which the agent's `get_record` resolves. Carrying the name needs a spec key (e.g. `selection.label`) plus an id-keyed title channel; that is the seat's call. - **activeApp is a package id**, as the card asks ("the same value as `packageId`"). The cloud agent route does not read `context.activeApp` today (only `appName`, `objectName`, `recordId`, `viewName`, `surface`, `packageId`), so this half changes what is sent; reading it is cloud#3564's half. - **Console apps without a `_packageId`** send no `activeApp` (pinned): nothing is selected by package, and no placeholder is sent. - The dock's "discussing" chip now appears on object, record, dashboard, report and page routes. On object pages it reads the object's translated label; on dashboards, reports and pages it reads `type · name` (the dock does not load those buckets just to label a chip). ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (`packages/spec` not touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (CI `Lint UI` is the reading.) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: n/a. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QUs3Brx8PodVWZfV1nTSCh --- _Generated by [Claude Code](https://claude.ai/code/session_01QUs3Brx8PodVWZfV1nTSCh)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>
1 parent f68b47c commit f4db5f3

10 files changed

Lines changed: 562 additions & 10 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): the console AI dock tells the agent which app the user has selected and which page or record is in view, instead of the constant `activeApp: 'AI'` and no page at all (#30043)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Every chat request the console sends to `POST /api/v1/ai/agents/:agent/chat` used to carry `context.activeApp: 'AI'`, whatever app the user was in, and the floating dock sent no `context.surface`. So the assistant asked "which app?" and "which record?" while the screen showed both.
11+
12+
Now:
13+
14+
- `context.activeApp` is the package of the app the user has selected: the console app the dock sits in (whether the dock runs `ask` or `build`), the app picked for editing on the full-page `/ai/build?package=…`, the package the Studio copilot designs. With no app selected the key is left out; nothing sends a placeholder.
15+
- The dock, on the desktop rail and the phone sheet alike, sends `context.surface` in cloud#1610's existing shape for the page in view: an object list (`artifact: { type: 'object', name }`, with `selection: { kind: 'view', id }` on a saved list view), a record page (the object plus `selection: { kind: 'record', id }`), a dashboard, report or page (`artifact: { type, name }`, `mode: 'design'` in the dashboard designer). A page that shows no one artifact (the app landing, search, a component page) sends no `surface`.
16+
- Both are read on every send, so navigating with the dock open moves the next turn's context.
17+
18+
The dock's "discussing" chip now names the object by its label on object pages, as the Studio copilot's chip already did for its artifacts.

‎ui/packages/app-shell/src/console/AppContent.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1534,6 +1534,7 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
15341534
objects={allObjects}
15351535
connectionState={connectionState}
15361536
userId={user?.id}
1537+
hostRoutePaths={hostRoutePaths}
15371538
>
15381539
<CommandPalette
15391540
apps={apps}

‎ui/packages/app-shell/src/console/ai/AiChatPage.tsx‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ import {
109109
import { AppHeader } from '../../layout/AppHeader.js';
110110
import { CONSOLE_TOP_BAR } from '../../layout/consoleTopBar.js';
111111
import { armChatDockExpanded, readDockReturnLocation } from '../../layout/chatDockState.js';
112+
import type { ChatSurfaceContext } from '../../layout/chatSurfaceContext.js';
112113
import {
113114
useAdvertiseShortcut,
114115
type AdvertisedShortcut,
@@ -1572,11 +1573,7 @@ interface ChatPaneProps {
15721573
* `context.surface` on every turn (the transport reads the body per send),
15731574
* and surfaced to the user as a chip above the composer.
15741575
*/
1575-
surfaceContext?: {
1576-
pillar?: string;
1577-
artifact?: { type: string; name: string };
1578-
mode?: string;
1579-
};
1576+
surfaceContext?: ChatSurfaceContext;
15801577
/**
15811578
* objectui#8219 — the display label of `surfaceContext.artifact`, when the
15821579
* host knows it (Studio: the Interfaces pillar's open leaf). Display-only: the
@@ -1597,6 +1594,13 @@ interface ChatPaneProps {
15971594
/** ADR-0070 "Edit with AI": the package the user opened to edit (from `?package=`),
15981595
* forwarded to the build agent as `context.packageId` to scope it to that app. */
15991596
editPackageId?: string;
1597+
/**
1598+
* objectstack#30043 — the app the user has selected, by its package: the
1599+
* console app the dock sits in, whichever agent it runs. Sent as
1600+
* `context.activeApp`; absent, `editPackageId` (the app picked for editing)
1601+
* is the selection, and with neither the key is omitted, never a placeholder.
1602+
*/
1603+
activeAppPackageId?: string;
16001604
/** A1.b bind-on-create: fired when this pane's build history binds to a package
16011605
* while the surface has no `?package=` — the page re-keys the conversation to
16021606
* `app:${pkg}:build` and writes `?package=` onto the URL. */
@@ -1636,6 +1640,7 @@ export function ChatPane({
16361640
apiBase,
16371641
conversationId,
16381642
editPackageId,
1643+
activeAppPackageId,
16391644
surfaceContext,
16401645
surfaceArtifactLabel,
16411646
onPackageBound,
@@ -1869,6 +1874,7 @@ export function ChatPane({
18691874
// pane mounts while the catalog is still loading, and a server without an
18701875
// AI service answers that catalog empty and `/models` with a 501.
18711876
const { models: aiModels, defaultModelId } = useAiModels({ apiBase, enabled: agents.length > 0 });
1877+
const selectedApp = activeAppPackageId || editPackageId || undefined;
18721878
const [selectedModelId, setSelectedModelId] = useState<string | undefined>(undefined);
18731879
const effectiveModelId = selectedModelId ?? defaultModelId;
18741880

@@ -1892,7 +1898,10 @@ export function ChatPane({
18921898
onError: handleChatError,
18931899
body: {
18941900
context: {
1895-
activeApp: 'AI',
1901+
// objectstack#30043 — the app the user has selected, never a constant:
1902+
// the agent was told "AI" while the user stood in their CRM. Omitted
1903+
// when nothing is selected.
1904+
...(selectedApp ? { activeApp: selectedApp } : {}),
18961905
agentName: activeAgent,
18971906
// Tell the agent the environment's publish posture so its narration
18981907
// matches reality (an auto-published build is live, not "to publish").
@@ -1902,6 +1911,7 @@ export function ChatPane({
19021911
...(editPackageId ? { packageId: editPackageId } : {}),
19031912
// cloud#1610 — what the user is currently discussing (advisory; the
19041913
// transport reads the body per send, so this stays fresh per turn).
1914+
// The console dock sends the page in view too (objectstack#30043).
19051915
...(surfaceContext ? { surface: surfaceContext } : {}),
19061916
},
19071917
},

‎ui/packages/app-shell/src/layout/ChatDock.tsx‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { useObjectTranslation } from '@object-ui/i18n';
3232
import { resolveApiBase } from '../console/ai/resolveApiBase.js';
3333
import { AiUsageIndicator } from './AiUsageIndicator.js';
3434
import { usePlanApprovalPending } from '../assistant/assistantBus.js';
35+
import type { ChatSurfaceContext } from './chatSurfaceContext.js';
3536
import {
3637
clampDockWidth,
3738
maximizedDockWidth,
@@ -248,6 +249,14 @@ export interface ChatDockPanelProps {
248249
* `children`.
249250
*/
250251
appPackageId?: string;
252+
/**
253+
* objectstack#30043 — the page in view, forwarded to the default body as
254+
* `context.surface` (see {@link ChatDockConversation}). Unused with
255+
* `children`.
256+
*/
257+
surfaceContext?: ChatSurfaceContext;
258+
/** objectui#8219 — the chip's display label for `surfaceContext.artifact`. Unused with `children`. */
259+
surfaceArtifactLabel?: string;
251260
/** Header title override; default "Assistant". */
252261
title?: string;
253262
/**
@@ -281,6 +290,8 @@ export function ChatDockPanel({
281290
apiBase: apiBaseProp,
282291
defaultAgent,
283292
appPackageId,
293+
surfaceContext,
294+
surfaceArtifactLabel,
284295
title,
285296
onMaximize,
286297
children,
@@ -367,6 +378,8 @@ export function ChatDockPanel({
367378
apiBase={apiBase}
368379
defaultAgent={defaultAgent}
369380
appPackageId={appPackageId}
381+
surfaceContext={surfaceContext}
382+
surfaceArtifactLabel={surfaceArtifactLabel}
370383
onCanvasOpenChange={handleCanvasOpenChange}
371384
onBoundPackageChange={setBoundPackageId}
372385
/>
@@ -388,6 +401,10 @@ export interface ChatDockMobileSheetProps {
388401
defaultAgent?: string;
389402
/** objectui#10926 — same contract as {@link ChatDockPanelProps.appPackageId}. */
390403
appPackageId?: string;
404+
/** objectstack#30043 — same contract as {@link ChatDockPanelProps.surfaceContext}. */
405+
surfaceContext?: ChatSurfaceContext;
406+
/** Same contract as {@link ChatDockPanelProps.surfaceArtifactLabel}. */
407+
surfaceArtifactLabel?: string;
391408
/** Header title override; default "Assistant". */
392409
title?: string;
393410
/**
@@ -427,6 +444,8 @@ export function ChatDockMobileSheet({
427444
apiBase: apiBaseProp,
428445
defaultAgent,
429446
appPackageId,
447+
surfaceContext,
448+
surfaceArtifactLabel,
430449
title,
431450
onMaximize,
432451
children,
@@ -491,6 +510,8 @@ export function ChatDockMobileSheet({
491510
apiBase={apiBase}
492511
defaultAgent={defaultAgent}
493512
appPackageId={appPackageId}
513+
surfaceContext={surfaceContext}
514+
surfaceArtifactLabel={surfaceArtifactLabel}
494515
onBoundPackageChange={setBoundPackageId}
495516
/>
496517
</React.Suspense>

‎ui/packages/app-shell/src/layout/ChatDockConversation.tsx‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { chatConversationScope, chatProductOfAgent } from '../hooks/chatScope.js
2222
import { resolveSurfaceAgent } from '../hooks/surfaceAgent.js';
2323
import { useCanAuthorMetadata } from '../hooks/useCanAuthorMetadata.js';
2424
import { isAiStudioEnabled } from '../runtime-config.js';
25+
import type { ChatSurfaceContext } from './chatSurfaceContext.js';
2526

2627
export interface ChatDockConversationProps {
2728
userId: string | undefined;
@@ -38,6 +39,15 @@ export interface ChatDockConversationProps {
3839
* authorable (see {@link ChatDockConversation}). Absent → app-less.
3940
*/
4041
appPackageId?: string;
42+
/**
43+
* objectstack#30043 — the page in view, as `context.surface` (cloud#1610's
44+
* shape; the layout reads it off the URL, see `consoleChatSurface`).
45+
* Re-read on every render and so on every send: navigating with the dock
46+
* open moves the next turn's surface. Absent → no `surface` key.
47+
*/
48+
surfaceContext?: ChatSurfaceContext;
49+
/** objectui#8219 — the display label of `surfaceContext.artifact`, for the chip only. */
50+
surfaceArtifactLabel?: string;
4151
/** ADR-0037/P3c — the Live Canvas open/close seam, forwarded to ChatPane. */
4252
onCanvasOpenChange?: (open: boolean) => void;
4353
/**
@@ -74,6 +84,8 @@ export function ChatDockConversation({
7484
apiBase,
7585
defaultAgent,
7686
appPackageId,
87+
surfaceContext,
88+
surfaceArtifactLabel,
7789
onCanvasOpenChange,
7890
onBoundPackageChange,
7991
}: ChatDockConversationProps) {
@@ -143,6 +155,11 @@ export function ChatDockConversation({
143155
apiBase={apiBase}
144156
conversationId={paneConversationId}
145157
editPackageId={editPackageId}
158+
// objectstack#30043 — the app the dock sits in is the selected app,
159+
// whichever agent runs and whether or not a build thread binds to it.
160+
activeAppPackageId={appPackageId}
161+
surfaceContext={surfaceContext}
162+
surfaceArtifactLabel={surfaceArtifactLabel}
146163
initialMessages={paneInitialMessages}
147164
pendingFirstMessageRef={pendingFirstMessageRef}
148165
onSent={() => {}}

‎ui/packages/app-shell/src/layout/ConsoleLayout.tsx‎

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88
* @module
99
*/
1010

11-
import React, { useCallback, useEffect, useRef, useState } from 'react';
11+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
1212
import { useLocation, useNavigate } from 'react-router-dom';
1313
import { AppShell } from '@object-ui/layout';
1414
import { useIsMobile } from '@object-ui/components';
15-
import { useObjectTranslation } from '@object-ui/i18n';
15+
import { useObjectLabel, useObjectTranslation } from '@object-ui/i18n';
1616

1717
// Lightweight FAB stub — the heavy chat chunk graph (plugin-chatbot,
1818
// shiki, streamdown, mermaid, @ai-sdk, ~20MB) only downloads on first
1919
// hover/click. See ConsoleChatbotFab.tsx.
2020
import { ConsoleChatbotFab } from './ConsoleChatbotFab.js';
2121
import { useChatDockState, ChatDockPanel, ChatDockMobileSheet } from './ChatDock.js';
22+
import { consoleChatSurface } from './chatSurfaceContext.js';
2223
import {
2324
matchChatDockShortcut,
2425
rememberDockReturnLocation,
@@ -64,6 +65,12 @@ interface ConsoleLayoutProps {
6465
* server-backed conversation history. Omit for unauthenticated/local-only.
6566
*/
6667
userId?: string;
68+
/**
69+
* objectstack#30043 — the paths of the host's extra app routes
70+
* (`AppContent`'s `extraRoutes`). A page one of them draws names no surface
71+
* to the dock, the way it shadows `:objectName` in `<Routes>`.
72+
*/
73+
hostRoutePaths?: readonly string[];
6774
}
6875

6976
/**
@@ -145,6 +152,7 @@ export function ConsoleLayout({
145152
objects,
146153
connectionState,
147154
userId,
155+
hostRoutePaths,
148156
}: ConsoleLayoutProps) {
149157
const appLabel = resolveKeyedI18nLabel(activeApp?.label) || activeAppName;
150158
// objectstack#28077 — once, before `AppShell`'s sidebar provider reads its
@@ -187,6 +195,24 @@ export function ConsoleLayout({
187195
// page draws its phone face at, so the two never disagree for a frame.
188196
const phone = usePhoneViewport();
189197
const recordPageOwnsTopBar = phone && isRecordPagePath(location.pathname);
198+
// objectstack#30043 — the page in view, handed to the dock as its
199+
// `context.surface` (cloud#1610's shape): the object of a list, the object
200+
// and record id of a record page, the dashboard, report or page. Read off the
201+
// URL each render, so the next send after a navigation carries the new page.
202+
const dockSurface = useMemo(
203+
() => consoleChatSurface(location.pathname, hostRoutePaths),
204+
[location.pathname, hostRoutePaths],
205+
);
206+
// The chip's label for an object surface: the object's own translated
207+
// label, the one its page shows. Display only; it never enters
208+
// `context.surface`. No definition in hand → the chip reads `type · name`.
209+
const { objectLabel } = useObjectLabel();
210+
const dockSurfaceLabel = useMemo(() => {
211+
const artifact = dockSurface?.artifact;
212+
if (artifact?.type !== 'object') return undefined;
213+
const def = (objects as ConsoleObject[] | undefined)?.find((o) => o?.name === artifact.name);
214+
return def ? objectLabel({ name: def.name, label: def.label ?? '' }) || undefined : undefined;
215+
}, [dockSurface, objects, objectLabel]);
190216
// "/ai = the dock maximized": record where we maximized FROM at click time,
191217
// so the page's collapse-to-dock returns exactly here (history-back could
192218
// land on a prior /ai URL after in-page conversation switches).
@@ -272,6 +298,8 @@ export function ConsoleLayout({
272298
// objectui#10926 — the app a build thread binds to (authorable
273299
// packages only; the dock decides).
274300
appPackageId={activeApp?._packageId}
301+
surfaceContext={dockSurface}
302+
surfaceArtifactLabel={dockSurfaceLabel}
275303
// ADR-0057 P3c — "/ai = the dock maximized": the maximize button
276304
// opens the full-page surface on THE SAME THREAD this rail shows —
277305
// `/ai` for an app-less thread (canonicalized to the default agent,
@@ -352,6 +380,8 @@ export function ConsoleLayout({
352380
userId={userId}
353381
defaultAgent={activeApp?.defaultAgent}
354382
appPackageId={activeApp?._packageId}
383+
surfaceContext={dockSurface}
384+
surfaceArtifactLabel={dockSurfaceLabel}
355385
// Bridge to full-page /ai (history + share live there on mobile). The
356386
// sheet defers the navigation until it has cleanly closed.
357387
onMaximize={openDockFullPage}

0 commit comments

Comments
 (0)