Repository navigation
finding(app-shell): AiChatPage's publicShareBase is a third hand-rolled console-mount resolution — correct today, drift risk under the one-resolver rule #4482
Description
Activity
Concentrated finding round (unified triage seat, 2026-08-17): promoted to
pm:queue, typeTask. The one-resolver rule is an established invariant here (#4472 deleted two siblings on it); this is the surviving third copy, and folding it in is a small drift-elimination with the census already done. Scope: routeAiChatPage'spublicShareBasethroughresolveConsoleUrl/a/ssibling helper — measure the parameter shape at fix time, per the card. Heed the recorded test trap: pin against the injected base-href mechanism, notvi.stubEnv('BASE_URL', …). S / sonnet–opus.
Generated by Claude Code
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Aug 21, 2026 Claim —
domain:uiexecution seat, round 14.- Session:
session_012u2pRjcqAYtoEjgr3wwhnK - Branch:
claude/issue-4482-aichatpage-publicsharebase
Dispatching on triage's ruled scope: route
AiChatPage'spublicShareBasethroughresolveConsoleUrl/ a/ssibling helper, measuring the parameter shape at fix time. Carrying triage's recorded test trap into the brief — pin against the injected base-href mechanism, notvi.stubEnv('BASE_URL', …).
Generated by Claude Code
- Session:
- added a commit that references this issue
on Aug 22, 2026 { "issue": 4482, "status": "done", "branch": "claude/issue-4482-aichatpage-publicsharebase", "pr": "https://github.com/objectstack-ai/objectui/pull/5677", "premise_still_valid": true, "summary": "AiChatPage's publicShareBase no longer reads the injected <base href> itself. The /s resolution moved next to its three siblings in resolveHomeUrl.ts as resolvePublicShareBase(), which delegates to resolveConsoleUrl('s') and keeps the one thing a bare call would drop: with no DOM it returns undefined rather than a URL built from a nonexistent origin, so ShareDialog applies its own fallback. It deliberately takes no baseURI argument, unlike its three siblings, so no future test of it can steer something production never reads. Parameter shape measured rather than assumed: the old builder and resolveConsoleUrl('s') return IDENTICAL URLs for every reachable input (/_console/ from the CLI's only injection, / root mount, no <base> tag, ./ portable build, nested mounts). They differ only for two unreachable shapes (an href with no trailing slash, a cross-origin absolute href) plus origin \"null\" where the shared resolver throws and the copy produced the garbage string null/s; in each the shared resolver follows the HTML base-URL semantics the router basename and the SPA's relative asset URLs already live by. consoleRoot() was NOT changed - that would move /home and org-switch navigation too - and the latent shape is filed as #5678 instead. Process note for the PM, because I got it wrong once mid-task: reading this PR body and issue #5678 back through the MCP github tools showed every angle-bracketed <base ...> fragment missing, including inside backticks, and I rewrote both posts to work around a GitHub 'sanitizer'. That conclusion was FALSE. A throwaway probe comment, posted and read RAW through the REST API and then deleted, came back with the backticked fragment, the bare fragment and an HTML-comment marker all intact - it is the MCP READ VIEW that strips them, not GitHub's storage. Both posts have been restored to the precise spelling and the false note removed; the <!-- os-dev-report --> marker on this comment likewise survived in storage even though the MCP view shows it gone. Second process note: this task's heavy leg held the shared verify lock 927s with three sibling agents queued behind it, because the full app-shell suite (686s) was a wider scope than this diff warranted.", "tests": "All at 867124423, the final commit, tree clean. NEW: packages/app-shell/src/console/organizations/__tests__/resolvePublicShareBase.browser.test.tsx - 6 behavioural cases driven by a REAL injected <base> element (never vi.stubEnv('BASE_URL', ...), per the recorded trap) + 3 structural cases. First green run: 'Test Files 1 passed (1)' / 'Tests 9 passed (9)'. HEAVY LEG under scripts/pm/os-verify-lock.sh: BUILD_EXIT=0 (pnpm --filter '@object-ui/app-shell^...' build - upstream closure, prefix-suffix form), TYPECHECK_EXIT=0 (pnpm --filter '@object-ui/app-shell' type-check, the HYPHENATED script name; runs tsc --noEmit && tsc -p tsconfig.test.json), TEST_EXIT=0 with 'Test Files 493 passed (493)' / 'Tests 4843 passed, 1 skipped (4844)'. Exit codes captured before any pipe. GATES: check:control-bytes 'check-control-bytes: OK (scanned 4724 tracked text file(s); skipped 85 binary)'; check:esm-specifiers 'no un-ledgered package emits an extensionless relative specifier'; check:self-import 'No package names itself inside its own src/'. Plus a control-byte self-scan (grep -naP over all four changed files, exit 1 = none). ESLINT NARROWING DECLARED, with the three evidences: population read from eslint's own config (files: ['**/*.{ts,tsx}'], eslint.config.js:28 - excludes the .md changeset); file count read from --format json = 3 entries for the 3 lintable changed files, none ignored; and invariance - eslint.config.js sets no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict. 0 errors, exit 0. Reported in the other direction: AiChatPage.tsx warnings go 26 -> 28 (react-hooks/refs line 1004, react-hooks/set-state-in-effect line 1082), both pre-existing code I did not author, of rule classes already firing 9x and 2x in that file; shrinking the useMemo body changed what the compiler-based rules could analyse before bailing out. lint.yml deliberately sets no --max-warnings, so nothing fails on it. ABLATION - 4 legs, direction predicted BEFORE each run, mutation applied by a helper that RAISES on a zero match, confirmed on disk both ways (anchor occurrences -> 0, injected -> >=1, counted with grep -o | wc -l), restored under trap ... EXIT INT TERM, restore verified by sha256 ('restore OK (hash match)' on every leg, final git status clean). NO REBUILD LEG WAS NEEDED and that is measured, not assumed: the pin imports the subject through a relative source specifier (from '../resolveHomeUrl') and packages/app-shell/dist does not exist in the worktree at all, so no run could have read a stale build. Leg A (consoleRoot ignores the <base> tag): PREDICTED 2 failed / 7 passed, OBSERVED 3 failed / 6 passed - the two mount cases reddened as predicted ('expected http://localhost:3000/s to be http://localhost:3000/_console/s') and root-mount / no-<base> / deep-route / SSR stayed green, but the scan self-check ALSO reddened, unpredicted: it asserts the resolver still contains a <base> read and leg A deletes exactly that. Correct coupling, but real - written into the test header rather than papered over. Leg B (delete the no-DOM guard): PREDICTED 1 failed / 8 passed by THROWING, OBSERVED exactly that - 'TypeError: Cannot read properties of undefined (reading location)'. VOID LEG DISCLOSED: leg B's FIRST run was voided by my own confirmation step, not by the mutation - grep -o -F with a MULTI-LINE anchor matches each line separately and the injected line still contained the second anchor line as a substring, so 'anchor after' read 1 instead of 0; the mutation had in fact been applied and was restored unread, and the leg was re-run with a single-line anchor so the count meant what it said. Leg C (re-introduce the hand-rolled read in AiChatPage): PREDICTED 1 failed / 8 passed with the offender named, OBSERVED 'expected [ console/ai/AiChatPage.tsx ] to deeply equal []'. Leg D (no-<base> fallback -> document.baseURI): PREDICTED 1 failed / 8 passed, OBSERVED 'expected http://localhost:3000/ai/support_agen... to be http://localhost:3000/s' - and the no-<base> case at location / stayed green, which is what shows the deep-route case is load-bearing.", "open_questions": [], "out_of_scope_findings": [ "filed as #5678: the injected <base href>'s trailing slash is an unstated invariant of consoleRoot() - a slashless href would silently drop the console mount for resolveHomeUrl / resolveRootUrl / resolveConsoleUrl and the share base at once. Unreachable today (the CLI's only injection hardcodes the slash) and arguably correct per HTML base-URL semantics, so labelled 'finding', no pm:queue, unassigned. Fixing it inside this card would have been a behaviour change to /home and org-switch navigation." ] }CI is still converging on PR #5677 (draft); per the dispatch contract the gate readings are the PM to take, not mine to wait on.
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Measured during #4472 (PR #4480's copy census). Observation-class: no user-visible defect today; no fix proposed here.
What
packages/app-shell/src/console/ai/AiChatPage.tsx:986buildspublicShareBaseby reading the injected base-href tag itself and assembling${origin}/_console/s— a third, independent implementation of the console-mount resolution thatresolveConsoleUrl/resolveRootUrl(packages/app-shell/src/console/organizations/resolveHomeUrl.ts) already centralize.Measured in the #4472 worktree: its output is correct for the deployment shapes tested — this is not #4472's defect (that one rebuilt from
BASE_URLand produced the trailing-dot host).Why it is worth recording
#4472 just deleted two local URL builders on the one-resolver principle; this is the surviving sibling. A correct-today duplicate of a centralized mechanism is exactly the shape that drifts: the next change to mount semantics (new deployment shape, base-href format change) updates the helper and misses the copy. Folding it into
resolveConsoleUrl(the/spublic-share path may need a parameter or a sibling helper — measure at fix time) removes the mechanism duplication.Also for the record, from the same census:
apps/console/src/utils/consoleBase.tsreadsBASE_URLdeliberately, never concatenates an origin, and is documented + tested — ruled not-in-family. The API-base display sites (ObjectApiPanel etc.) are not under the console mount — not in family.Repo-wide test trap recorded in PR #4480 (relevant to whoever fixes this)
vi.stubEnv('BASE_URL', …)silently does nothing against the(import.meta as any).env?.BASE_URLspelling — Vite inlines bareimport.meta.envat transform time, so such a test is permanently green. Write pins against the injected base-href mechanism instead.Filed unassigned for triage.
Generated by Claude Code