diff --git a/.changeset/25992-objectview-chrome-split.md b/.changeset/25992-objectview-chrome-split.md
new file mode 100644
index 00000000000..c1265ee3644
--- /dev/null
+++ b/.changeset/25992-objectview-chrome-split.md
@@ -0,0 +1,12 @@
+---
+'@object-ui/app-shell': patch
+---
+
+refactor(app-shell): the Console `ObjectView`'s leaf pieces move into a sibling module
+
+Clause-②: no
+Metadata-protocol: none
+
+The "object not found" state (`ObjectNotFoundState`), the viewport check that decides whether the saved-view tab strip is drawn, and the inline-edit toggle's session-only callback move verbatim out of `views/ObjectView.tsx` into `objectViewChrome.tsx` beside it. No logic, name or rendered output changes. `ObjectView.tsx` re-exports every name it exported before, and the package entry is unchanged.
+
+What to do: nothing.
diff --git a/ui/packages/app-shell/src/views/ObjectView.tsx b/ui/packages/app-shell/src/views/ObjectView.tsx
index c80dfb5da63..455555f6f6d 100644
--- a/ui/packages/app-shell/src/views/ObjectView.tsx
+++ b/ui/packages/app-shell/src/views/ObjectView.tsx
@@ -49,7 +49,7 @@ import {
EmptyDescription,
NavigationOverlay,
} from '@object-ui/components';
-import { Plus, Upload, Table as TableIcon, Lock, MoreHorizontal, RotateCcw } from 'lucide-react';
+import { Plus, Upload, Lock, MoreHorizontal, RotateCcw } from 'lucide-react';
import { FavoriteMenuItem, useObjectListFavorite } from '../layout/SidebarItemMenu.js';
import { useMetaItemRefusal } from '../hooks/useMetaItemRefusal.js';
import { useTenancyPosture } from '../hooks/useTenancyPosture.js';
@@ -125,6 +125,7 @@ import {
isSameOptionsValue,
dispatchViewPatches,
} from './objectViewOptions.js';
+import { ObjectNotFoundState, useViewTabsFit, keepInlineEditModeForTheSession } from './objectViewChrome.js';
// objectstack#25992 slice 1: these module-level helpers moved to the sibling
// `objectView*.ts` modules above. Every name this file exported stays
@@ -155,6 +156,10 @@ export {
dispatchViewPatches,
} from './objectViewOptions.js';
+// objectstack#25992 slice 3: `ObjectNotFoundState` moved to `./objectViewChrome.tsx`
+// and stays exported from here (`AppContent` imports it by this path).
+export { ObjectNotFoundState } from './objectViewChrome.js';
+
/**
* Props of the Console `ObjectView`, the route-level object surface
* `@object-ui/app-shell` exports (objectui#7483).
@@ -186,94 +191,6 @@ export interface ConsoleObjectViewProps {
externalRefreshKey?: number;
}
-/**
- * The "object not found" state the route draws for an object it has no
- * definition of. objectui#13052 — `packageDisabled`: the server answered that
- * the object exists and its package is disabled (`OBJECT_PACKAGE_DISABLED`),
- * so the description says that instead. The console route draws it for that
- * answer (`AppContent`); this module's `ObjectView` draws it for an absence.
- * Not part of the package entry.
- */
-export function ObjectNotFoundState({ objectName, packageDisabled = false }: { objectName: string | undefined; packageDisabled?: boolean }) {
- const { t } = useObjectTranslation();
- return (
-
-
-
- {t('console.objectView.objectNotFound')}
-
- {packageDisabled
- ? t('console.objectView.objectPackageDisabled', { objectName })
- : t('console.objectView.objectNotFoundDescription', { objectName })}
- {' '}
- {t('console.objectView.objectNotFoundHint')}
-
-
-
- );
-}
-
-/**
- * The list toolbar's inline-edit toggle, as this page wires it: it writes
- * nothing (objectui#5144, triage's ruling E).
- *
- * The view's `inlineEdit` and `userActions.editInline` are the AUTHOR's
- * permission keys, and `normalizeListViewSchema` folds the first into the
- * second. `editInline` is on by default since the v18 ruling (objectstack#22605,
- * objectui#12086), so a view that declares neither key offers the toggle, and
- * `editInline: false` withdraws it. This toggle used to persist a USER's edit
- * mode into `inlineEdit` through `persistViewPatch`. After the fold, switching
- * it off stored `inlineEdit: false`, which reads as "not offered", so the
- * toggle was gone from the next load and nothing in the console could bring it
- * back.
- *
- * The edit mode is now session state. `ListView` keeps it, and seeds it from
- * the view's `inlineEdit` on each load. The callback stays wired because
- * `ListView` offers the wide toolbar toggle only to a host that wires one.
- *
- * Recorded costs: the edit mode is not remembered across loads, and an overlay
- * that already stores `inlineEdit: false` still reads off. That is existing
- * data, and the maintainer's ruling rejects migrating it. Declaring
- * `userActions.editInline: true` on such a view brings the toggle back.
- */
-/**
- * Tailwind's `sm` breakpoint (px): from here up the list page draws its
- * saved-view tabs in the list toolbar; below it the app bar's mobile view
- * switcher is the page's only view control (objectui#12440).
- */
-const VIEW_TABS_MIN_WIDTH = 640;
-
-/**
- * Is the viewport at least {@link VIEW_TABS_MIN_WIDTH} wide? The matchMedia
- * idiom of `useIsMobile` / `useIsWideViewport`, except the first answer is
- * read synchronously: the tab strip is the first thing on the page's list row,
- * and resolving it in an effect would draw the row once without it and then
- * jump. objectui#12440 — this decides whether the strip is HANDED to the list
- * at all, so below `sm` nothing is passed and nothing is drawn (the CSS
- * `hidden sm:block` it replaces only hid it).
- */
-function viewTabsFitNow(): boolean {
- return typeof window !== 'undefined' && window.innerWidth >= VIEW_TABS_MIN_WIDTH;
-}
-
-function useViewTabsFit(): boolean {
- const [fits, setFits] = useState(viewTabsFitNow);
- useEffect(() => {
- if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
- const mql = window.matchMedia(`(min-width: ${VIEW_TABS_MIN_WIDTH}px)`);
- const onChange = () => setFits(viewTabsFitNow());
- mql.addEventListener('change', onChange);
- return () => mql.removeEventListener('change', onChange);
- }, []);
- return fits;
-}
-
-function keepInlineEditModeForTheSession(): void {
- // Deliberately empty: see the docblock.
-}
-
export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey }: ConsoleObjectViewProps) {
const { objectName } = useParams();
diff --git a/ui/packages/app-shell/src/views/objectViewChrome.tsx b/ui/packages/app-shell/src/views/objectViewChrome.tsx
new file mode 100644
index 00000000000..a8ad49dfa90
--- /dev/null
+++ b/ui/packages/app-shell/src/views/objectViewChrome.tsx
@@ -0,0 +1,100 @@
+/**
+ * Leaf pieces of the Console `ObjectView` (`./ObjectView.tsx`): the "object
+ * not found" state, the viewport check that decides whether the saved-view tab
+ * strip is drawn, and the inline-edit toggle's session-only callback, moved out
+ * of that file verbatim (objectstack#25992, slice 3). `ObjectView.tsx`
+ * re-exports every name it exported before.
+ */
+
+import { useState, useEffect } from 'react';
+import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
+import { Table as TableIcon } from 'lucide-react';
+import { useObjectTranslation } from '@object-ui/i18n';
+
+/**
+ * The "object not found" state the route draws for an object it has no
+ * definition of. objectui#13052 — `packageDisabled`: the server answered that
+ * the object exists and its package is disabled (`OBJECT_PACKAGE_DISABLED`),
+ * so the description says that instead. The console route draws it for that
+ * answer (`AppContent`); this module's `ObjectView` draws it for an absence.
+ * Not part of the package entry.
+ */
+export function ObjectNotFoundState({ objectName, packageDisabled = false }: { objectName: string | undefined; packageDisabled?: boolean }) {
+ const { t } = useObjectTranslation();
+ return (
+
+
+
+ {t('console.objectView.objectNotFound')}
+
+ {packageDisabled
+ ? t('console.objectView.objectPackageDisabled', { objectName })
+ : t('console.objectView.objectNotFoundDescription', { objectName })}
+ {' '}
+ {t('console.objectView.objectNotFoundHint')}
+
+
+
+ );
+}
+
+/**
+ * The list toolbar's inline-edit toggle, as this page wires it: it writes
+ * nothing (objectui#5144, triage's ruling E).
+ *
+ * The view's `inlineEdit` and `userActions.editInline` are the AUTHOR's
+ * permission keys, and `normalizeListViewSchema` folds the first into the
+ * second. `editInline` is on by default since the v18 ruling (objectstack#22605,
+ * objectui#12086), so a view that declares neither key offers the toggle, and
+ * `editInline: false` withdraws it. This toggle used to persist a USER's edit
+ * mode into `inlineEdit` through `persistViewPatch`. After the fold, switching
+ * it off stored `inlineEdit: false`, which reads as "not offered", so the
+ * toggle was gone from the next load and nothing in the console could bring it
+ * back.
+ *
+ * The edit mode is now session state. `ListView` keeps it, and seeds it from
+ * the view's `inlineEdit` on each load. The callback stays wired because
+ * `ListView` offers the wide toolbar toggle only to a host that wires one.
+ *
+ * Recorded costs: the edit mode is not remembered across loads, and an overlay
+ * that already stores `inlineEdit: false` still reads off. That is existing
+ * data, and the maintainer's ruling rejects migrating it. Declaring
+ * `userActions.editInline: true` on such a view brings the toggle back.
+ */
+/**
+ * Tailwind's `sm` breakpoint (px): from here up the list page draws its
+ * saved-view tabs in the list toolbar; below it the app bar's mobile view
+ * switcher is the page's only view control (objectui#12440).
+ */
+const VIEW_TABS_MIN_WIDTH = 640;
+
+/**
+ * Is the viewport at least {@link VIEW_TABS_MIN_WIDTH} wide? The matchMedia
+ * idiom of `useIsMobile` / `useIsWideViewport`, except the first answer is
+ * read synchronously: the tab strip is the first thing on the page's list row,
+ * and resolving it in an effect would draw the row once without it and then
+ * jump. objectui#12440 — this decides whether the strip is HANDED to the list
+ * at all, so below `sm` nothing is passed and nothing is drawn (the CSS
+ * `hidden sm:block` it replaces only hid it).
+ */
+function viewTabsFitNow(): boolean {
+ return typeof window !== 'undefined' && window.innerWidth >= VIEW_TABS_MIN_WIDTH;
+}
+
+export function useViewTabsFit(): boolean {
+ const [fits, setFits] = useState(viewTabsFitNow);
+ useEffect(() => {
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
+ const mql = window.matchMedia(`(min-width: ${VIEW_TABS_MIN_WIDTH}px)`);
+ const onChange = () => setFits(viewTabsFitNow());
+ mql.addEventListener('change', onChange);
+ return () => mql.removeEventListener('change', onChange);
+ }, []);
+ return fits;
+}
+
+export function keepInlineEditModeForTheSession(): void {
+ // Deliberately empty: see the docblock.
+}