Repository navigation
feat(app-shell): Home's top bar searches, the greeting sums up approvals, compact builder chips and 56 px app tiles that say when they were last opened (objectui#12601) - #12638
Conversation
…als, builder chips and 56 px app tiles (objectui#12601) Sections one and two of the approved home artboard: - HomeLayout mounts the console's command palette, scoped to Home's host app, so the home bar draws its search field (centred, 420 px from xl) and Cmd+K opens the same palette as inside an app. - The greeting is 24 px / 600 with a 13 px line from the approvals count; "assigned to me" has no classifiable source yet, so the line names approvals only. - The two builder cover cards become two 32 px chips for admins; a withheld chip explains itself through the objectui#12303 tooltip wrapper, keeping the existing test ids. - The apps strip draws 112 px tiles with a 56 px icon; Setup leaves the tiles for a muted System Settings link; an admin gets a trailing Add app tile on the marketplace route, gated as the shortcut is. - New keys home.search, home.summary.*, home.addApp in all ten packs. Claude-Session: https://claude.ai/code/session_01JZnppgHRp4sREcouhkQYDB Co-authored-by: Claude <noreply@anthropic.com>
…rom the recent-items store (objectui#12601) The card's sixth target item and its fifth acceptance test were not on the inherited branch: each tile now draws an 11 px "Opened 2 hours ago" line under its name. The time is the newest recent-items entry opened inside the app (its `/apps/<segment>/...` href, resolved to the app with `matchAppBySegment`); an app with no entry left in the store draws no line. New key `home.appOpened` in all ten packs. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude <noreply@anthropic.com>
…ards read (objectui#12601) `home.heroTagline`, `home.build.subtitle` and `home.template.subtitle` had one reader each, the hero tagline and the two cover cards this card retires; `pnpm check:i18n-dead-keys` listed them after that change. Removed from all ten packs. The changeset now says so, and states the `@object-ui/i18n` half of the public surface: the exported `en` pack is typed by its literal content, so its type moves with the keys. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude <noreply@anthropic.com>
… left in HomeAppsStrip (objectui#12601) Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude <noreply@anthropic.com>
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…als count is an answer; no spec-named local type (objectui#12601) Seat patch round 1 on PR #12638, two items. The line under the greeting said "Nothing is waiting for you today" while the approvals read was in flight, after it failed, or before it was asked: the count hook hands back the value and drops the feed's status. A sibling, `useSharedPendingApprovals`, returns the store's `SharedFeedSnapshot` (the count hook now reads `.value` off it, so its two callers are unchanged), and the line speaks only on `ready`. On anything else it keeps its height and stays empty. Not exported from the package entry. `lastOpenedByApp` declared a local `AppLike`, a name `@objectstack/spec/system` exports, and `pnpm check:spec-symbols` refused it. It now takes the app list as `matchAppBySegment`'s own parameter type and the store entry as `Pick<RecentItem, 'href' | 'visitedAt'>`, so no second shape is declared. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
… its one inbox seam (objectui#12601) 042577e had HomePage call `useSharedPendingApprovals` directly, beside the `useHomeInbox` it already reads. Every HomePage harness mocks `useHomeInbox` and routes `fetch`, so the new direct read sent a real approvals request: each file's afterEach (no request escapes the double) failed, its `cleanup()` after that assertion never ran, and the next case found the previous DOM still mounted ("Found multiple elements") — seven files in CI shards 5, 10, 13, 14 and 15. The status now travels the page's one seam: `useHomeInbox` reads the approvals snapshot and returns `pendingApprovalsStatus` beside the count, as it returns `notificationsStatus` for the inbox. HomePage reads only `useHomeInbox`. `HomeActionCenter.unansweredInbox` dials the count through the snapshot read it now goes through; `useInboxBell` keeps the count hook. A new hook test pins the pass-through (loading, ready with the count, error, and a 404 as the store's ready answer); the Home last-opened case gains an entry with no readable time, which draws no line. The changeset states the ready-only line. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…n every pack (objectui#12601) Seat patch round 2 on PR #12638, two items. Measured live at 1280 px, en: "Opened 6 seconds ago" read "Opened 6 secon..." in the 112 px tile. The line is now drawn in Intl's compact form, `TILE_OPENED_TIME_FORMAT` = `{ style: 'short', numeric: 'always' }` ("3 hr. ago", "1 day ago", zh "3小时前打开"). `formatRelativeTime` (@object-ui/i18n) and `timeAgo` (app-shell) take Intl's own `style` / `numeric` as an optional argument; without it their output is unchanged, so one helper keeps the unit thresholds. `home.appOpened` keeps a verb only where verb and time still fit: zh, ko and ar unchanged, ja "{{time}}に起動", en, de, fr, es, pt and ru the time alone. `appTileOpenedLine-12601.test.ts` holds every pack's line, 6 s to 12 y, to a 93 px budget (112 - 2 x 8 px padding - 3 px reserve for the width estimate's measured error), pins the en and zh strings at 6 s, 5 min, 3 h and 2 d, and shows the old long form over budget. The changeset's Greeting bullet said the count is "the approvals count the bell badges"; the badge also adds unread notification topics. It now names the shared pending-approvals read and says what the badge adds, and the Apps bullet and the i18n surface sentence describe the compact line and the new optional argument. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude <noreply@anthropic.com>
…t raw characters (objectui#12601) The file was written with the two full-width ranges materialized as raw characters, U+3000 (ideographic space) among them, which `no-irregular-whitespace` refuses. Same regex, escape spelling. Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…s a legitimate identity (objectui#12601)
`home.appOpened` is the tile's time hole plus a verb where both fit the
112 px tile; ru, like en, has no room for one, so its value equals en's
`{{time}}`. `untranslated-identity-4376` names that case's remedy: a
format string goes in LEGITIMATE_IDENTITIES with its reason, as
`grid.summary.pattern` does.
Claude-Session: https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o
Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Face: the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT ·
|
Fixes #12601
Clause-②: yes
Sections ① and ② of the approved artboard "系统首页 · 四段式" on
/home: a search field in the top bar that opens the console's command palette, a 24 px greeting with a one-line work summary, the admin's two builder chips beside it, and an apps section with 112 px tiles, a 56 px icon, a compact last-opened line ("2 hr. ago"; zh "2小时前打开") read from the recent-items store, Setup moved off the tiles onto a "System Settings" link, and an admin's trailing "Add app" tile. Sections ③ and ④ are objectui#12602 and objectui#12604; they are not touched here.Resumed work
This card's first dev (session
session_01JZnppgHRp4sREcouhkQYDB) ran out of tokens after pushing3cc9fa1, with no PR and no report. The seat took the card over (claim6209656150). This PR is that commit, read hunk by hunk against the card, plus what it lacked:3cc9fa1)HomeLayout: mountsCommandPaletteProviderand the existingCommandPalette, scoped to Home's host appAppHeader: thehomevariant centres the search field where a palette is mounted, 420 px fromxl, labelledhome.searchAppHeaderchange, no new propHomePage: greeting 24 px / 600, summary line, builder chips with the tooltip wrapper, 1120 px column, Setup off the tilesHomeAppsStrip: 15 px heading and count pill, 112 px tiles, 56 px icon, settings link, "Add app" tileHomePage.authoringCapabilityGate.test.tsxedit9aa8059)Commits after the inherited one:
f2ef2c1merge oforigin/main,9aa8059the last-opened line,efe6e9dthe key removal and the changeset correction,42e5761a blank line.What the tile's time reads
The recent-items store records no app visits. It records what was opened inside an app, each entry with the
hrefit was opened at (/apps/SEGMENT/...) and itsvisitedAt.lastOpenedByApp(new,console/home/appLastOpened.ts) takes, for each app, the newest entry whose segment resolves to it throughmatchAppBySegment, so both the package id and the name alias count. An app with no entry left in the store gets no line. The line ishome.appOpenedwith the locale's relative time in Intl's compact form (TILE_OPENED_TIME_FORMAT={ style: 'short', numeric: 'always' }, passed throughtimeAgotoformatRelativeTime, the helper the recent rail uses). A pack keeps a verb only where verb and time fit the 112 px tile at 11 px: zh, ko and ar keep theirs, ja reads the time plus に起動, and en, de, fr, es, pt and ru carry the time alone ("2 hr. ago"). Every pack's line from 6 s to 12 y is held to a derived 93 px budget byappTileOpenedLine-12601.test.ts. (Seat edit, patch round 2: this paragraph originally read "Opened 2 hours ago", which truncated in the live reading.)Measurements against the seat's hypotheses
H1, public surface (measured on the built declarations).
@object-ui/app-shellbuilt at this branch and again withpackages/app-shell/srcatorigin/main63d00da, the declaration closure ofdist/index.d.tswalked through relative specifiers: 180 files on both sides, the same file list. Two files differ,console/home/HomeLayout.d.tsandlayout/AppHeader.d.ts, and both differ in doc comments only.HomePage()takes no props on either side;HomeLayoutPropsandAppHeaderPropsare byte-identical. Outside the closure,HomeAppsStrip.d.tsgains two optional props (settingsHref,lastOpened) andappLastOpened.d.tsis new; neither is reachable from the entry. On@object-ui/i18n, the exportedenpack is typed by its literal content, so its type gains the new keys and loses the three removed ones. Patch round 2 also gives the exportedformatRelativeTimean optional third argument (style/numericofIntl.RelativeTimeFormat); omitted, the output is unchanged. The changeset states all of it.H2, the summary's sources. On
mainthe approvals count has a source:useSharedPendingApprovalsCount(the bell's badge read,GET /api/v1/approvals/requests?status=pending). Nothing onmainclassifies an item as "assigned to me": an inbox row carries a free-texttopicand no declared kind. So the line names approvals only ("今天有 N 项待审批", or "今天没有待办" at zero). No endpoint or data source was added. objectui#12602's Target 5 names the same fork for its "分配给我" column.H3, test ids and the edited test.
home-build-appandhome-start-templateare on the chips' buttons;home-authoring-gate-reasonandhome-marketplace-disabled-reasonare on the visually hidden reasons the chips are described by. The other ids the page had (browse-marketplace-btn,app-tile-NAME,apps-show-all,home-build-with-ai,home-ask-ai) are unchanged. The one edit to an existing test,HomePage.authoringCapabilityGate.test.tsx, line by line: it removed a comment saying the reason is "on screen — not only in a tooltip" and one assertion,getByTestId('home-authoring-gate-reason')has the text«home.build.noCapability»; it added the samegetByTestIdwith the sametoHaveTextContent, plus two assertions thathome-build-appandhome-start-templatecarryaria-describedbypointing at that reason. No assertion was weakened: the removed one never asserted visibility, and the visible paragraph it described is what the card moves into the tooltip wrapper.H4, Setup, the time line, non-admins. Each has its own pin, and each pin fails when the behaviour is removed (ablations below).
H5, one palette.
HomeLayoutmountsCommandPaletteProviderandCommandPalettefromchrome/CommandPalette.tsx, the componentAppContentmounts inside an app, with the same props (apps,activeApp,objects,onAppChange,dataSource). The bar's existingaction:command-palette:openbutton callsuseCommandPalette().openCommandPalette, andAppHeaderdraws it only whereuseCommandPaletteProviderMounted()answers yes./homemounts noConsoleLayout, so this is the only palette in the tree. With no openable app (a zero-app workspace, or the app list still loading) no palette is mounted and no field is drawn.Acceptance tests and their ablations
Each mutation landed through
ablation-replace.mjs(anchor hit, blob changed), the file was restored and proven equal toHEAD. Run at9aa8059; the later commits touch no line any of these anchors or tests read.HomeLayout.searchOpensPalette-12601.test.tsx› the search field opens the command paletteHomeLayout:{hostApp ? (to{false ? (HomePage.greetingAndApps-12601.test.tsx› a non-admin renders no builder chips{isAdmin && (to{true && (if (!disabled) return button;toreturn button;launcherAppsset toactiveAppslastOpened={lastOpened}tolastOpened={undefined}appLastOpened-12601.test.tspins the reading itself: the newest entry wins in any order, the name alias resolves to the same app as the package id, and an entry outside every app, under an unknown app or with no readable time names nothing.Verification (at
42e5761; superseded for the patch rounds by the section below)Every local reading below was taken on
42e5761(git rev-parse --short HEAD), after the last commit, through the container's shared verify lock where it builds or tests. Wall-clock figures are from a shared box.pnpm --filter '@object-ui/app-shell^...' build(dependency closure)pnpm --filter @object-ui/i18n buildpnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json, the second coverssrc/**/*.test.ts(x))pnpm --filter @object-ui/i18n type-checkpnpm exec vitest run packages/app-shell/src/console/home/ packages/app-shell/src/layout/ packages/app-shell/src/console/__tests__/ packages/app-shell/src/__tests__/ packages/app-shell/src/chrome/plusviews/__tests__/phase1-page-blocks.render.test.tsxandviews/metadata-admin/previews/__tests__/exclusion-reason-truthfulness.test.tspnpm exec vitest run packages/i18n/pnpm exec vitest runover the sevenapps/consoletests that mountDefaultHomeLayoutor the apppnpm check:i18n-keys,check:i18n-drift,check:i18n-dead-keyspnpm check:test-path-roots,check:changeset-claims,changeset:check,check:unreferenced-sources,check:esm-specifiers,check:self-importnode scripts/check-changeset-presence.mjs,check-changeset-overwrite.mjs,check-new-cross-file-line-citations.mjs,check-control-bytes.mjs,check-vi-mock-inherit.mjs,check-vi-mock-override-shape.mjs,check-vi-mock-specifiers.mjspnpm exec eslintover the 19 changed.ts/.tsxfilesno-explicit-anyand unused stub arguments, the same kinds the files and the stub idiom they copy already carry)pnpm check:i18nnamed on the card does not exist in this repository; the threecheck:i18n-*gates above are the real ones. The gate list is derived by hand frompackage.jsonand.github/workflows/, against this diff.Patch rounds (seat-appended)
Seat REWORK round 1 is recorded on objectui#12601; the dev's reports are comments
6211824280(round 2) and6212550215(round 3).fc73410:AppLikeis no longer hand-declared, socheck:spec-symbolsis green.ready. The status travels throughuseHomeInbox, the page's one inbox seam.042577e: a direct approvals read escaped their fetch guard. Ablations S1/S2 go 4 red each.70bfa2f:short→long) and T2 (en back to "Opened …") each turn the 93 px budget pin red.70bfa2f:pnpm exec vitest run packages/app-shell/src/console/home/ packages/app-shell/src/hooks/ packages/app-shell/src/layout/ packages/i18n/: Test Files 252 passed (252), Tests 2596 passed, 13 skipped.useHomeInbox.ts,sharedUserFeeds.ts(both not exported),@object-ui/i18nformatRelativeTime(optional argument),app-shellutils/relativeTime.ts, and oneLEGITIMATE_IDENTITIESentry forhome.appOpenedinuntranslated-identity-4376.test.ts.NOT MEASURED
@object-ui/app-shellpackage run (1436 test files) and the fullpnpm test; narrowed to the directories that import the changed modules and the tests that name them. CI's shardedpnpm testruns all of it.performance-budget.yml, eager closure), reason: it needs every package and the console built; it runs on this PR in CI.HomeLayoutnow importsCommandPalettestatically andHomePageimportsresolveSetupAppPathfromConsoleShell.Acceptance notes
Top bar height. The artboard's bar is 48 px; Home's stays 56 px (
h-14). The same 56 px header is drawn byAppShell,HomeLayout, the organization layouts and the AI chat page, so changing Home alone would make the bar jump on the way into an app. Carrier: objectui#12639 (filed by the seat; both approved artboards drawheader style="height:48px"), which lands after this PR. (Seat edit: this line originally named objectui#12605 as the carrier.)The bar's right side (help, a 28 px avatar) is left as it is:
AppHeaderchanges are limited to the/homesearch entry while objectui#12605 holds the file. The left side already matches (a 28 px mark on--primary, the 14 px / 600 name, the preview chip).The search field keeps the shared trigger's
rounded-mdand 12 px label; the artboard draws an 8 px radius and 13 px text. One element serves Home and every app.The summary's zero claim is fixed in this PR (patch round 1,
fc73410). The line speaks only when the approvals read is an answer (ready); while it loads, after it fails or before it is asked, the line is empty. A 404 (no approvals plugin) is the store's ruledreadyanswer of 0, so the line says "Nothing is waiting for you today" there, which the seat accepts. The status travels throughuseHomeInbox(pendingApprovalsStatus). (Seat edit: this line originally carried the defect to objectui#12602.)Tile colour. The tile keeps the launcher's deterministic gradient per app, which the artboard draws too; it does not read
branding.primaryColor(the ruling thatAppMarkimplements names the bar's pill, the switcher rows and the drawer header). Carrier: none.packages/i18n/src/__tests__/makerEmptyState-v1-scope-8329.test.tsnameshome.build.subtitlein a scope comment; the key is removed here. Comment only, left as it is.Adds no product-specific widget, dialog, hook or string (see AGENTS.md › Product-neutral renderer)
Session:
https://claude.ai/code/session_011K9g23W67dy2zwnyzaoB6o(thedomain:uiseat 1 PM; this run is itsos-devsubagent).Generated by Claude Code