Skip to content

Commit 5275d1f

Browse files
feat(app-shell): search on the Studio landing, the palette lists packages, objects and flows (objectui#11863, Q2) (#12044)
Fixes #11863 Clause-②: yes ## What this does Q2 of objectui#11863, search on the Studio landing, as ruled in retriage `6054672824` ("Q2 → A, with C as the fallback if no read lists flows across packages"). A read that lists flows across packages exists, so this builds A with flows. Q1 (PR #12004) and Q3 (PR #11996) have landed; Q2 is the card's last ruled item. - `StudioLanding` (`apps/console/src/components/StudioRoute.tsx`) mounts `CommandPaletteProvider` and `CommandPalette scope="studio"`. Under the provider, the `studio` header draws its Search trigger through objectui#11912's existing rule, and `Ctrl+K` / `⌘K` opens the palette. `AppHeader`'s rule is unchanged; only its comment now names the `/studio` mount. - `CommandPalette` gets a declared second scope. `scope="studio"` takes no other prop: there is no active app, so the scope has no app navigation group, no record search, no app switching, and no full-search command (its link is `/apps/APP/search`). It lists three Studio groups instead: - **Packages**: `fetchPackages`, the list the landing and its switcher read (kernel packages already left out). Each opens `/studio/PKG/data`, as a landing card does. - **Objects**: `useMetadata().objects`, each under the package it declares (`_packageId`), opened at `/studio/PKG/data?surface=object:NAME`. An object whose package is not in the list, or that has no package, has no Studio page and is left out. - **Flows**: the metadata cache's `flow` list, opened at `/studio/PKG/automations?surface=flow:NAME`. A flow that belongs to no package opens in the package-less scope, `/studio/~org/automations?surface=flow:NAME` (`studioOrgScopePath`, objectui#11553). - Entries match on label or machine name through `matchesPaletteQuery`, the rule the palette's other entries use (objectui#11812). An object or packaged flow shows its package's name on the right. Nothing is read until the palette opens. The theme commands and the recently viewed records stay. - Two pack rows in all ten packs: `console.commandPalette.packages` and `console.commandPalette.flows`. The objects group reuses `console.commandPalette.objects`. - Docs: `packages/app-shell/README.md` (the palette section) and `content/docs/guide/console.md` (a "Search in Studio" row). Changeset `.changeset/11863-studio-search.md`. The pending Q1 changeset `.changeset/11863-studio-header.md` gains one append-only dated note (patch round 1, see Acceptance notes). ## Contract face (Clause-② yes, widening) - `@object-ui/app-shell`, `CommandPalette` props: a second form, `scope="studio"`, which takes no other prop. The in-app form (`apps`, `activeApp`, `objects`, `onAppChange`, optional `dataSource`) is unchanged and still required; it leaves `scope` out. The props type is not exported by name. No export is added or removed. The scope is declared, not inferred from a missing `activeApp`. A type pin in the new test holds both directions: `CommandPalette scope="studio" activeApp=...` and a bare `CommandPalette` are compile errors. The pins are two `@ts-expect-error` lines that the package's `type-check` (`tsc -p tsconfig.test.json`) compiles. - `@object-ui/i18n`: the exported `en` pack and `TranslationKeys` gain `console.commandPalette.packages` and `console.commandPalette.flows`. Nothing is removed. - Changeset: `@object-ui/app-shell` minor, `@object-ui/i18n` patch, `@object-ui/console` patch. That follows the Q3 changeset's precedent for added pack rows. No major. ## The PM hypotheses, measured - **H1, held** on `049012b`. `CommandPaletteProvider` was mounted only in `ConsoleLayout`. `AppHeader` asks `useCommandPaletteProviderMounted()`. In the reverse leg below, base `StudioRoute.tsx` gives "Unable to find an element by: [data-testid="action:command-palette:open"]" on `/studio`. So this card mounts the palette itself. - **H2, held.** The clean shape mounts in `StudioLanding` only, not above Home, Organizations or AI. In a real browser, `/home` still draws 0 desktop and 0 compact triggers on this branch. Nothing was widened or narrowed silently. - **H3, held** on `049012b`. A scratch probe (deleted, never committed) put the base `CommandPalette` (blob `77b85897`) at `/studio` with `apps` empty and `activeApp` null. The query `search` rendered the full-search item, and picking it went to `/apps/undefined/search`: 1 passed. This branch drops that command and every app-scoped group in the `studio` scope, by declaration. - **H4: a cross-package flow read exists, so A, not C.** The metadata cache's `getItemsByType('flow')` is the unscoped `GET /api/v1/meta/flow`. It is the same unscoped list that `loadPackageLessSurfaces` narrows, and that function's doc says "The unscoped list also carries every PACKAGED item". On the server, `getMetaItems({ type })` with no package returns every package's items. The URL shapes come from `StudioDesignSurface`'s routing on `main`: `PILLAR_FOR_SURFACE_TYPE` (object to `data`, flow to `automations`), the `?surface=TYPE:NAME` deep link (`DESIGNER_SURFACE_PARAM`, `formatSurfaceParam`) and `studioOrgScopePath`. These were checked against a mocked API in the browser. I did not check them against a live objectstack server, because the sibling checkout is not installed or built in this container. - **H5, fits.** Eager closure under the lock: base `049012b` is 3,238,404 B ("3162.5 KB gzipped across 289 of 2474 chunks (budget: 3204.6 KB, headroom: 42.1 KB)"). Branch `dbca37f` is 3,239,095 B ("3163.2 KB gzipped across 289 of 2474 chunks (budget: 3204.6 KB, headroom: 41.4 KB)"). The delta is +691 B, under the 3,281,467 ceiling, which was not touched. The bytes land in the `index` chunk (+770 B). `packages-io` was already in the eager `PackagesPage` chunk on base, so `fetchPackages` adds no module. `i18n-locale-en` grows 45,231 to 45,241 B, under its own ceiling. `b6a003e` changes one test docstring after `dbca37f`, with no source change. - **H6.** `console.commandPalette.objects` fits the objects heading and is reused. No row in `packages/i18n` fits "Packages" or "Flows" as a group heading: `common.package` and `packagedAutomation.colFlow` are singular, and `console.studio.title` is the product name. The designer table (`metadata-admin/i18n.ts`) has `engine.packages.title`, but the palette reads only `packages/i18n` through `useObjectTranslation`. So the PR adds two rows in all ten packs. ## Verification (head `b6a003e`) - **Union**, every test file that imports or names `CommandPalette`, `CommandPaletteProvider`, `AppHeader`, `StudioRoute` or `commandPalette` (`git grep -l`, 59 files), under `os-verify-lock`: "Test Files 59 passed (59)", "Tests 515 passed (515)". - `packages/i18n/`: "Test Files 82 passed (82)", "Tests 1316 passed | 13 skipped (1329)". - `scripts/__tests__/`: "Test Files 179 passed | 2 skipped (181)", "Tests 5452 passed | 2 skipped (5454)". - **Type-check**: `pnpm --filter @object-ui/i18n --filter @object-ui/app-shell --filter @object-ui/console run type-check`, all three script names echoed, exit 0. This ran after `turbo run build --filter='@object-ui/console^...'`. - **New tests:** `CommandPalette.studioScope-11863.test.tsx` has 10 cases: groups and exclusions, each target URL, matching, no app-scoped entry plus an app-scope CONTROL, nothing read while closed plus a CONTROL. `StudioRoute.searchPalette-11863.test.tsx` has 4 cases through the real `studioRoutes`: both triggers, click, `Ctrl+K`, and picking a package opens its builder. The 11912 pin's CONTROL now includes `studio` under a provider. - **Reverse verification**, from committed `dbca37f`. `CommandPalette.tsx` and `StudioRoute.tsx` were swapped to their base blobs (`77b85897`, `9e2098ca`; anchors `useStudioPaletteEntries` 1 to 0 and `scope="studio"` 1 to 0). The run gave "Tests 13 failed | 8 passed (21)". Red: the 9 studio-scope cases (the base palette throws "Cannot read properties of undefined (reading 'filter')" with no app props) and the 4 route cases (no trigger). Green: the app-scope CONTROL and the 7 cases of the 11912 pin. The trap restored both files from `HEAD`: blobs `05643164`, `b314334d` equal `HEAD`, and `git diff HEAD` is empty. - **Gates**, exit 0 each: `check:i18n-keys` ("Every in-scope call-site key resolves against the en pack (3313 keys)"), `check:i18n-drift`, `check:i18n-dead-keys` (report-only; neither new key listed), `check:eager-locale-catalogues`, `check:new-line-citations` ("0 new citation(s)"), `check:control-bytes`, `check:vi-mock-specifiers`, `check:vi-mock-inherit`, `check:vi-mock-override-shape` and `check:test-path-roots`. Also exit 0: `check:changeset-claims` (report-only; `11806-login-server-unreachable.md` names `packages/app-shell/README.md` for its sign-up table, which this PR leaves alone), `check:pending-changeset-literals`, `check:doc-fences`, `check:icon-record-names`, `check:shell-escape-residue`, `check:unreferenced-sources`, `check:readme-exports`, `check:doc-snippets`, `check-changeset-presence` ("17 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)") and `check-changeset-no-major`. `check-governed-queue-guard --test` over the 20 paths says NOT GOVERNED. - **eslint**: `--no-inline-config --format json` over the 17 changed ts/tsx files gives 0 errors. Each of the 5 edited files has the same warning count as its base blob through `--stdin`: 2, 13, 22, 11 and 13. The two new test files have 0. The population is the config's `**/*.{ts,tsx}` and the file count comes from the JSON output. The config is not type-aware (no `parserOptions.project` / `projectService`), so this diff cannot move any untouched file's verdict. The repo-wide `pnpm lint` is CI's. - **Patch round 1, head `12f3f09`.** One commit, changeset text only: `.changeset/11863-studio-header.md`, 2 lines added and 0 removed. At that head, exit 0 each: `check-changeset-presence` ("17 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)"), `check-changeset-no-major`, `check:changeset-claims` (report-only, the same 11806 reading as above), `check:pending-changeset-literals`, `check:control-bytes` and `check:new-line-citations` ("0 new citation(s)"). The union, type-check, eslint, bundle and browser readings in this section are from `b6a003e` and `dbca37f` and were not re-run: no source or test file changed. The two bot comments on this PR were read: the changeset-claim re-read (6077690455) names `.changeset/11806-login-server-unreachable.md` for `packages/app-shell/README.md`, and its paragraph is about the sign-up table, which this PR leaves alone, so it still holds; the CI Console Performance Budget on `b6a003e` (6077731629) reads PASS, 3163.4 KB against 3204.6 KB. ### Real browser (Chromium `/opt/pw-browsers/chromium`, 1440x900, `vite preview` of the `dbca37f` build on a private port, API mocked with `page.route`; `pageErrors` empty) | Where | What was read | |---|---| | `/studio` | desktop trigger 1, compact 1; click gives `/studio?palette=1` | | palette, empty query | headings Packages, Objects, Flows, Preferences; Acme CRM, People; Account (Acme CRM), Employee (People); Onboard Account (Acme CRM), Nightly Cleanup; three theme commands. Kernel package, `sys_user` and the kernel flow absent | | query `search` | 0 items: no full-search command, nothing under `/apps/` | | query `employee`, picked | `/studio/com.acme.hr/data?surface=object%3Ahr_employee` | | `Ctrl+K` on `/studio`, package-less flow picked | `/studio?palette=1`, then `/studio/~org/automations?surface=flow%3Aorg_cleanup` | | CONTROL `/apps/crm_app` | triggers 1 and 1; headings Objects, Preferences, Actions; `search` gives the full-search command | | `/home` (untouched frame) | triggers 0 and 0 | ## Acceptance notes - The pending `.changeset/11863-studio-header.md` (Q1) ends its first paragraph with "No command palette is mounted on `/studio`, so the header shows no search trigger there." This PR makes that sentence false. Patch round 1 leaves the sentence as written and appends one dated note after its paragraph, in the objectui#6152 round-12 shape: from objectui#11863 Q2 (this PR) the landing mounts the command palette in its `studio` scope, so the header draws the search trigger and `⌘K` opens it. This PR's own changeset also states the reversal in its first paragraph. - Bounds, by design and stated in the code: objects and flows are the published items from the metadata cache, so an item that exists only as a draft is not listed. If the package read fails, the packaged entries are left out, and the landing under the palette shows that read's error. - The pillar builder (`/studio/PKG/TAB`) mounts no palette. It is `StudioDesignSurface`'s full-screen frame, which this claim does not hold. Session: `https://claude.ai/code/session_01CijGnfWLxTLUFcJkY2ouUw` --- _Generated by [Claude Code](https://claude.ai/code/session_01CijGnfWLxTLUFcJkY2ouUw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d328698 commit 5275d1f

21 files changed

Lines changed: 844 additions & 29 deletions

‎.changeset/11863-studio-header.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
The Studio landing (`/studio`) gets the console's own header (objectui#11863). Its old header held one control, the product wordmark linking back to Home. It now mounts `AppHeader` with `variant="studio"`: the brand logo, then a fixed "Studio" crumb, then the header's right-hand cluster as on Home and the Workspaces page (the inbox, help, and the account menu with profile, theme, language and sign-out). The brand links to the declared landing (`useHomePath()`), as the wordmark did, so the landing and the Studio builder's Home button still name one home. No command palette is mounted on `/studio`, so the header shows no search trigger there.
88

9+
**Note added 2026-10-09 (objectui#11863 Q2, PR objectui#12044):** the last sentence above no longer holds. From objectui#11863 Q2 the landing mounts the command palette in its `studio` scope, so the header draws the search trigger and `⌘K` opens the palette; see `.changeset/11863-studio-search.md`.
10+
911
**Clause-②: yes (widening; and one narrowing in `@object-ui/i18n`).** `AppHeader`'s `variant` prop accepts a fourth member, `'studio'`, beside `'app'`, `'home'` and `'orgs'`. It draws the brand and a fixed crumb, as `'orgs'` does. The union is not exported by name (the package entry exports `AppHeader` only), so a host meets it as the prop's type: a host that switches exhaustively over that type must handle `'studio'`. A call that passes no `variant` resolves it as before (`'app'` with an `appName`, `'home'` without). No export is added or removed.
1012

1113
`@object-ui/i18n`: each of the ten packs gains `console.studio.title` ("Studio", a product name every pack writes as is), the crumb's label, and drops `console.studio.backToHome`, the retired wordmark's tooltip, which nothing reads any more.

‎.changeset/11863-studio-search.md‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@object-ui/app-shell': minor
3+
'@object-ui/i18n': patch
4+
'@object-ui/console': patch
5+
---
6+
7+
The Studio landing (`/studio`) gets search (objectui#11863). It mounts the command palette, so its header shows the "Search ⌘K" trigger and `Ctrl+K` / `⌘K` opens the palette. Until now only the frame inside an app mounted one, so the landing's header drew no trigger (objectui#11912's rule). The entry for the landing's new header says it shows no search trigger; with this change it does.
8+
9+
On `/studio` no app is active, so the palette leaves out everything that belongs to an app: the app's objects, dashboards, pages and reports, record search, app switching, and the "Open Full Search Page" command, whose link starts with `/apps/APP` (mounted without an app it would have gone to `/apps/undefined/search`). It lists the Studio instead, in three groups:
10+
11+
- **Packages**: the packages the Studio landing lists (kernel packages left out). Each opens its Data pillar, `/studio/PKG/data`.
12+
- **Objects**: the objects of those packages, each opened in its package's Data pillar on that object (`/studio/PKG/data?surface=object:NAME`).
13+
- **Flows**: the flows of those packages, each opened in its package's Automations pillar on that flow (`/studio/PKG/automations?surface=flow:NAME`), and the flows that belong to no package, opened in the package-less scope (`/studio/~org/automations?surface=flow:NAME`).
14+
15+
An entry matches the query by its label or its machine name, as the palette's other entries do; an object or a packaged flow shows its package's name beside it. Nothing is read until the palette opens. Objects and flows are the published ones from the metadata cache, so an item that is still only a draft is not listed. The theme commands and the recently viewed records stay.
16+
17+
**Clause-②: yes (widening).** `CommandPalette`'s props gain a second form, `scope="studio"`, which takes no other prop. The props a host passes inside an app (`apps`, `activeApp`, `objects`, `onAppChange`, `dataSource`) are unchanged and still required there; that form leaves `scope` out. The props type is not exported by name (the package entry exports `CommandPalette` only), so a host meets it as the component's props. No export is added or removed. `@object-ui/i18n` gains two keys in all ten packs, `console.commandPalette.packages` and `console.commandPalette.flows`, the headings of two of the new groups; the objects group reuses `console.commandPalette.objects`.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* objectui#11863 (Q2) — the `/studio` landing mounts the command palette, in
5+
* its `studio` scope, so the header's search trigger appears there and opens a
6+
* palette that lists the Studio.
7+
*
8+
* Before it, only `ConsoleLayout` (inside an app) mounted a
9+
* `CommandPaletteProvider`, and `AppHeader` draws its "Search ⌘K" trigger only
10+
* under one (objectui#11912), so the landing's `studio` header had no search at
11+
* all and `Ctrl+K` opened nothing. Pinned through the REAL `studioRoutes` tree,
12+
* the real `AppHeader`, `CommandPaletteProvider` and `CommandPalette`, and the
13+
* real `fetchPackages` over a stubbed `fetch`:
14+
*
15+
* - the header draws both triggers (desktop and compact) on `/studio`;
16+
* - the desktop trigger and `Ctrl+K` each open the palette (`?palette=1`);
17+
* - the palette lists the Studio's packages, and picking one opens its pillar
18+
* builder through the real route tree.
19+
*
20+
* `t` answers in keys, as in `StudioRoute.landingI18n.test.tsx`, so a heading
21+
* is read off the key it asks for.
22+
*/
23+
24+
import '@testing-library/jest-dom/vitest';
25+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
26+
import { render, screen, fireEvent } from '@testing-library/react';
27+
import React from 'react';
28+
import { MemoryRouter, Routes, Route, useLocation } from 'react-router-dom';
29+
30+
/** Auth facts. This file only ever drives the holder path. */
31+
const auth = { isAuthenticated: true, isLoading: false, user: { id: 'u1' } as unknown };
32+
33+
function jsonResponse(body: unknown, status = 200): Response {
34+
return {
35+
ok: status >= 200 && status < 300,
36+
status,
37+
headers: { get: () => null },
38+
json: async () => body,
39+
} as unknown as Response;
40+
}
41+
42+
vi.mock('@object-ui/auth', async (importOriginal) => ({
43+
...(await importOriginal<Record<string, unknown>>()),
44+
useAuth: () => auth,
45+
createAuthenticatedFetch: () => async () =>
46+
jsonResponse({
47+
authenticated: true,
48+
userId: 'u1',
49+
systemPermissions: ['studio.access'],
50+
objects: {},
51+
fields: {},
52+
}),
53+
}));
54+
55+
// `AuthGuard` reaches `useAuth` through the auth package's OWN module graph,
56+
// not through its entry point — the same seam `StudioRoute.test.tsx` documents.
57+
vi.mock('../../../../packages/auth/src/useAuth', async (importOriginal) => ({
58+
...(await importOriginal<Record<string, unknown>>()),
59+
useAuth: () => auth,
60+
}));
61+
62+
// The same barrel seam as `StudioRoute.landingI18n.test.tsx`: `chrome/` is
63+
// pulled whole, so `CommandPalette` is the real one.
64+
vi.mock('@object-ui/app-shell', async (importOriginal) => ({
65+
...(await importOriginal<Record<string, unknown>>()),
66+
...(await vi.importActual<Record<string, unknown>>(
67+
'../../../../packages/app-shell/src/chrome/index'
68+
)),
69+
ConnectedShell: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
70+
RequireOrganization: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
71+
LoadingFallback: () => <div data-testid="loading" />,
72+
LoadingScreen: () => <div data-testid="error-screen" />,
73+
getProductName: () => 'ObjectOS',
74+
BuilderLanding: () => <div data-testid="studio-front-door">pick a package</div>,
75+
StudioDesignSurface: () => <div data-testid="studio-pillar-builder" />,
76+
}));
77+
78+
vi.mock('@object-ui/i18n', async (importOriginal) => ({
79+
...(await importOriginal<Record<string, unknown>>()),
80+
useObjectTranslation: () => ({
81+
t: (key: string) => `«${key}»`,
82+
}),
83+
}));
84+
85+
import { studioRoutes } from './StudioRoute';
86+
87+
const DESKTOP = 'action:command-palette:open';
88+
const MOBILE = 'action:command-palette:open-mobile';
89+
const OVERLAY = 'overlay:command-palette';
90+
91+
function LocationProbe() {
92+
const location = useLocation();
93+
return <div data-testid="location" data-path={`${location.pathname}${location.search}`} />;
94+
}
95+
96+
function renderStudio(at = '/studio') {
97+
return render(
98+
<MemoryRouter initialEntries={[at]}>
99+
<Routes>
100+
{studioRoutes}
101+
<Route path="/home" element={<div data-testid="home-launcher">home</div>} />
102+
<Route path="/login" element={<div data-testid="login-page">login</div>} />
103+
</Routes>
104+
<LocationProbe />
105+
</MemoryRouter>,
106+
);
107+
}
108+
109+
function locationNow(): string {
110+
return screen.getByTestId('location').getAttribute('data-path') ?? '';
111+
}
112+
113+
beforeEach(() => {
114+
vi.clearAllMocks();
115+
// The package list answers two packages; every other read (the header's
116+
// feeds, the AI agent catalogue) answers empty, so no request leaves the
117+
// process.
118+
vi.stubGlobal(
119+
'fetch',
120+
vi.fn(async (url: string) =>
121+
String(url).endsWith('/api/v1/packages')
122+
? jsonResponse({
123+
data: [
124+
{ manifest: { id: 'com.acme.crm', name: 'Acme CRM' }, writable: true },
125+
{ manifest: { id: 'com.acme.hr', name: 'People' }, writable: true },
126+
],
127+
})
128+
: jsonResponse({ data: [] }),
129+
),
130+
);
131+
});
132+
133+
afterEach(() => {
134+
vi.unstubAllGlobals();
135+
});
136+
137+
describe('the /studio landing mounts the command palette (objectui#11863, Q2)', () => {
138+
it('draws the header search trigger, desktop and compact', async () => {
139+
renderStudio();
140+
await screen.findByTestId('studio-front-door');
141+
142+
expect(screen.getByTestId(DESKTOP)).toBeInTheDocument();
143+
expect(screen.getByTestId(MOBILE)).toBeInTheDocument();
144+
// Closed until asked.
145+
expect(screen.queryByTestId(OVERLAY)).not.toBeInTheDocument();
146+
});
147+
148+
it('opens the palette from the trigger, and it lists the Studio packages', async () => {
149+
renderStudio();
150+
await screen.findByTestId('studio-front-door');
151+
152+
fireEvent.click(screen.getByTestId(DESKTOP));
153+
expect(await screen.findByTestId(OVERLAY)).toBeInTheDocument();
154+
expect(locationNow()).toBe('/studio?palette=1');
155+
156+
expect(await screen.findByText('«console.commandPalette.packages»')).toBeInTheDocument();
157+
expect(screen.getByText('Acme CRM')).toBeInTheDocument();
158+
expect(screen.getByText('People')).toBeInTheDocument();
159+
});
160+
161+
it('opens the palette on Ctrl+K', async () => {
162+
renderStudio();
163+
await screen.findByTestId('studio-front-door');
164+
165+
fireEvent.keyDown(document, { key: 'k', ctrlKey: true });
166+
expect(await screen.findByTestId(OVERLAY)).toBeInTheDocument();
167+
expect(locationNow()).toBe('/studio?palette=1');
168+
});
169+
170+
it("picking a package opens that package's pillar builder", async () => {
171+
renderStudio('/studio?palette=1');
172+
fireEvent.click(await screen.findByText('People'));
173+
174+
expect(await screen.findByTestId('studio-pillar-builder')).toBeInTheDocument();
175+
expect(locationNow()).toBe('/studio/com.acme.hr/data');
176+
});
177+
});

‎apps/console/src/components/StudioRoute.tsx‎

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { lazy, Suspense, useEffect, type ReactNode } from 'react';
3838
import { Navigate, Outlet, Route, useParams } from 'react-router-dom';
3939
import {
4040
AppHeader,
41+
CommandPalette,
42+
CommandPaletteProvider,
4143
LoadingFallback,
4244
LoadingScreen,
4345
STUDIO_ORG_SCOPE_PILLAR,
@@ -151,22 +153,30 @@ export function StudioBuilderLoading() {
151153
* and help, and the brand, then a fixed Studio crumb. The brand links to
152154
* `useHomePath()`, so the landing is never a navigation dead end, and it names
153155
* the same home as `StudioDesignSurface`'s Home button one route apart
154-
* (objectui#7256, objectui#7373). No command palette is mounted here, so the
155-
* header draws no search trigger (objectui#11912).
156+
* (objectui#7256, objectui#7373).
157+
*
158+
* The frame mounts the command palette in its `studio` scope (objectui#11863):
159+
* no app is active here, so it lists the Studio's packages, objects and flows
160+
* instead of an app's entries. Under its `CommandPaletteProvider` the header
161+
* draws the search trigger, as it does wherever a palette is mounted
162+
* (objectui#11912), and `⌘K` opens it.
156163
*/
157164
function StudioLanding() {
158165
return (
159-
<div className="flex min-h-screen flex-col bg-background text-foreground">
160-
<header className="sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4">
161-
<AppHeader variant="studio" />
162-
</header>
163-
<div className="min-h-0 flex-1 overflow-auto">
164-
{/* The frame above stays painted while the landing's chunk loads. */}
165-
<Suspense fallback={<StudioBuilderLoading />}>
166-
<BuilderLanding />
167-
</Suspense>
166+
<CommandPaletteProvider>
167+
<div className="flex min-h-screen flex-col bg-background text-foreground">
168+
<header className="sticky top-0 z-30 flex h-14 w-full shrink-0 items-center gap-2 border-b bg-background px-2 sm:px-4">
169+
<AppHeader variant="studio" />
170+
</header>
171+
<CommandPalette scope="studio" />
172+
<div className="min-h-0 flex-1 overflow-auto">
173+
{/* The frame above stays painted while the landing's chunk loads. */}
174+
<Suspense fallback={<StudioBuilderLoading />}>
175+
<BuilderLanding />
176+
</Suspense>
177+
</div>
168178
</div>
169-
</div>
179+
</CommandPaletteProvider>
170180
);
171181
}
172182

‎content/docs/guide/console.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The console opens at **http://localhost:5180** (the port is fixed in `apps/conso
2828
| **Expression Visibility** | Show/hide navigation items using `visible: "${data.role === 'admin'}"`. |
2929
| **Branding** | Per-app colors, favicons, and logos via `AppShell` branding. |
3030
| **Command Palette** | `⌘+K` opens a searchable command bar for quick navigation. |
31+
| **Search in Studio** | On the Studio landing (`/studio`) the header's search, or `⌘+K`, opens the command palette over the Studio rather than an app: its packages, their objects and their flows, matched by label or machine name. A package opens its Data pillar, an object opens in its package's Data pillar, a flow in its package's Automations pillar, and a flow that belongs to no package in `/studio/~org/automations`. App entries, record search and the full-search page are not offered there, since no app is open. |
3132
| **Studio Package Scope** | Studio home, metadata counts, quick-create links, and diagnostics follow the selected package. |
3233
| **Package-less Flows in Studio** | Flows that belong to no package — such as a clone of a packaged flow, made from Setup › Packaged automation — are listed and edited at `/studio/~org/automations`, reached from the Studio home ("Not in a package") or the package switcher. They open editable, edits save as drafts, and Publish promotes those drafts. |
3334
| **Studio Automations Rail** | The Automations pillar's flow rail has a search box that matches a flow's label or its machine name, case-insensitively. A long flow name wraps instead of being cut off. |

‎packages/app-shell/README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,16 @@ record search. Its open state and the command that opens it are provided by
10371037
`CommandPaletteProvider` (wired in by `ConsoleLayout`) and exposed via
10381038
`useCommandPalette()`.
10391039

1040+
`CommandPalette` has two scopes. Inside an app it takes `apps`, `activeApp`,
1041+
`objects`, `onAppChange` and an optional `dataSource`, and searches that app.
1042+
`<CommandPalette scope="studio" />` takes no other prop: it is the palette of the
1043+
`/studio` landing, a frame outside every app, which the console mounts under its
1044+
own `CommandPaletteProvider`. It leaves out every app-scoped group and the
1045+
full-search command (their links start with `/apps/APP`), and lists the Studio's
1046+
packages, their objects and their flows, each opening its Studio page. The
1047+
header's search trigger is drawn wherever a provider is mounted, so `AppHeader`
1048+
with `variant="studio"` shows it there.
1049+
10401050
```tsx
10411051
import { useCommandPalette } from '@object-ui/app-shell';
10421052

0 commit comments

Comments
 (0)