diff --git a/.github/workflows/webui-ci.yml b/.github/workflows/webui-ci.yml index 2ed448c01..2cfe92005 100644 --- a/.github/workflows/webui-ci.yml +++ b/.github/workflows/webui-ci.yml @@ -30,7 +30,8 @@ jobs: shell: bash run: | echo "MCODE_WEBUI_VERIFY_DIR=$RUNNER_TEMP/webui-package-checks" >> "$GITHUB_ENV" - echo "WEBUI_BROWSER_OUTPUT_DIR=$RUNNER_TEMP/webui-browser-results" >> "$GITHUB_ENV" + echo "WEBUI_PLAYWRIGHT_OUTPUT_DIR=$RUNNER_TEMP/webui-playwright-results" >> "$GITHUB_ENV" + echo "WEBUI_BROWSER_FONT_CACHE_DIR=$RUNNER_TEMP/webui-browser-font-cache" >> "$GITHUB_ENV" if [ "$RUNNER_OS" = "macOS" ]; then echo "WEBUI_BROWSER_HEADED=1" >> "$GITHUB_ENV" fi @@ -66,11 +67,17 @@ jobs: if: runner.os == 'Linux' run: pnpm exec playwright install-deps chromium - name: Test WebUI units and integrations + env: + WEBUI_BROWSER_OUTPUT_DIR: ${{ runner.temp }}/webui-browser-diagnostics/units run: pnpm test:webui - name: Test WebUI browser interactions + env: + WEBUI_BROWSER_OUTPUT_DIR: ${{ runner.temp }}/webui-browser-diagnostics/browser if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.browsers.outcome == 'success' }} run: pnpm test:webui-browser - name: Verify WebUI package installation + env: + WEBUI_BROWSER_OUTPUT_DIR: ${{ runner.temp }}/webui-browser-diagnostics/package if: ${{ !cancelled() && steps.build.outcome == 'success' }} run: pnpm test:webui-package - name: Upload WebUI browser diagnostics @@ -78,6 +85,8 @@ jobs: uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: webui-browser-${{ matrix.os }}-${{ github.run_attempt }} - path: ${{ runner.temp }}/webui-browser-results/ + path: | + ${{ runner.temp }}/webui-browser-diagnostics/ + ${{ runner.temp }}/webui-playwright-results/ retention-days: 7 if-no-files-found: ignore diff --git a/packages/local-runtime/test/unit/local-permission-facade.test.ts b/packages/local-runtime/test/unit/local-permission-facade.test.ts index e0632e817..9147b5c97 100644 --- a/packages/local-runtime/test/unit/local-permission-facade.test.ts +++ b/packages/local-runtime/test/unit/local-permission-facade.test.ts @@ -2018,11 +2018,12 @@ describe('LocalPermissionFacade', () => { }); it('write to a path inside dataDir still ASKs (sandbox is read-only)', async () => { - const { facade, dataDir } = freshFacade({ permissionMode: 'default' }); + const { facade, dataDir } = freshFacade({ permissionMode: 'default', workspaceDir: '/synthetic-workspace' }); try { const r = await facade.checkPermission({ toolName: 'write', input: { path: path.join(dataDir, 'skills', 'generated', 'SKILL.md') }, + agentName: 'synthetic-agent', }); // Writes to dataDir go through normal review. The sandbox // auto-allow is scoped to READ-ONLY tools because fs-permission's @@ -2118,7 +2119,7 @@ describe('LocalPermissionFacade', () => { describe('turn-scoped trusted exact writes', () => { it('allows only the supplied exact Plan target without persisting a rule', async () => { - const { facade, ruleStore, dataDir } = freshFacade({ permissionMode: 'default' }); + const { facade, ruleStore, dataDir } = freshFacade({ permissionMode: 'default', workspaceDir: '/synthetic-workspace' }); const planPath = path.join(dataDir, 'v2', 'sessions', 'session-a', 'artifacts', 'plan.md'); try { await expect( @@ -2126,6 +2127,7 @@ describe('LocalPermissionFacade', () => { toolName: 'write', input: { path: planPath, content: '# Plan' }, sessionId: 'session-a', + agentName: 'synthetic-agent', trustedExactWritePaths: [planPath], }), ).resolves.toMatchObject({ behavior: 'allow' }); @@ -2134,6 +2136,7 @@ describe('LocalPermissionFacade', () => { toolName: 'write', input: { path: `${planPath}.bak`, content: '# Escape' }, sessionId: 'session-a', + agentName: 'synthetic-agent', trustedExactWritePaths: [planPath], }), ).resolves.toMatchObject({ behavior: 'ask' }); diff --git a/packages/webui/docs/client/ownership.md b/packages/webui/docs/client/ownership.md index 37455cca7..a0747b5f1 100644 --- a/packages/webui/docs/client/ownership.md +++ b/packages/webui/docs/client/ownership.md @@ -87,7 +87,10 @@ and extraction revision. Channel filter writes use the selected bot's detail rev Account login uses attempt and status-read revisions across begin, poll, cancel and dialog close. Credential removal is shared by switch and sign-out: retire the credential epoch and reconcile account/usage before beginning another login. Malformed check-in and directory responses enter -recoverable error states. Settings subscribes once per resource slot, independently of repeated +recoverable error states. Repeated hover/focus on an open usage panel does not start another quota +read; explicit refresh and retry still do. The rail displays the owner's last quota result while +a new read is loading, then adopts the new answer instead of hiding the subscription name. +Settings subscribes once per resource slot, independently of repeated reads; retained model answers retire with their epoch/session resource. Composer catalogue invalidation also retires outstanding promises, preventing stale successful consumption. diff --git a/packages/webui/docs/client/transcript-rendering.md b/packages/webui/docs/client/transcript-rendering.md index 990c6fa05..c5fb0dd93 100644 --- a/packages/webui/docs/client/transcript-rendering.md +++ b/packages/webui/docs/client/transcript-rendering.md @@ -24,7 +24,12 @@ Workspace file references and delivered-file rows share the same inline media re (including SVG and PNG), supported video and audio files render their contents in the transcript; video and audio use native controls without autoplay. Markdown image syntax and recognized bare media filenames in inline code use the same workspace resource path. Non-media files keep links. -Remote HTTP(S) media and paths outside the current workspace are not automatically previewed. +Remote HTTP(S) media are not automatically previewed. Explicit absolute local paths also work +outside the session workspace: the file tab and authenticated media URL use the named file's +containing directory as their read root. Relative traversal paths remain invalid. + +Delivered-file rows keep their icon and display name together, including when a reference cannot +be opened. The icon type comes from the source filename, independently of a human-readable caption. The host serves actual workspace bytes with byte-range support. A transient media load failure retries a bounded number of times; content synchronization and media-byte availability remain @@ -69,6 +74,12 @@ Pinned by `test/unit/markdown-codeblock-c2-line-numbers.test.tsx`. - A reference of the form `` `path/to/file.ts:120` `` renders as a clickable anchor carrying the line number, not as literal text. - Clicking it invokes `onOpenFile` with the parsed reference. +- Absolute local references outside the workspace retain their absolute identity until the file + tab or media URL splits them into directory and filename. Existing workspace-relative references + keep their original meaning. +- If a relative path with directories is absent, the runtime adapter may complete a unique, + segment-aligned suffix from workspace search. Ambiguous or truncated search results do not pick + a file. Bare-filename references retain their existing workspace search behavior. - A bare `` `file.ts` `` with no line number is still a valid reference. - A string that merely contains a colon is not turned into a reference. diff --git a/packages/webui/docs/design/design-language.md b/packages/webui/docs/design/design-language.md index a9df1b0a8..09258a269 100644 --- a/packages/webui/docs/design/design-language.md +++ b/packages/webui/docs/design/design-language.md @@ -249,6 +249,13 @@ contract. is scoped to the shell, so a page-level form keeps the field contract's own scale. Popovers and menus stay non-modal. The app-wide Settings host keeps its page-like full-viewport layout as a documented exception. + Clicking the backdrop dismisses a dialog by default. A caller that must block dismissal + passes `dismissOnBackdrop={false}` or gates `onClose` while its command is running. + Escape dismissal remains opt-in. The settings dialog wrapper inherits the backdrop default. +- **Progress overview sections size to their content.** The environment keeps every action visible; + short todo lists do not reserve the long-list height. Todo and subagent lists retain bounded + scrolling. `WebuiDisclosure` owns the animated body height; section shells must not impose a + competing collapsed height or flex basis. - **Repeated generic grouping uses `WebuiInline` and `WebuiStack`.** They own direction, alignment, main-axis distribution (start/center/end/between), inline formatting and named spacing steps (`hairline`, `micro`, `tight`, `compact`, `small`, `medium`, `roomy`, `large`). `compact` maps to the shared 6px spacing token. Alignment also includes `baseline` for text-centered inline groups. Callers keep content-specific width, clipping, diff --git a/packages/webui/docs/runtime/boot.md b/packages/webui/docs/runtime/boot.md index bdffd0b29..7bb3e7954 100644 --- a/packages/webui/docs/runtime/boot.md +++ b/packages/webui/docs/runtime/boot.md @@ -96,7 +96,7 @@ session transfer · the terminal process manager (one per service) · the six sl | Entry | Path | Notes | | --- | --- | --- | -| Development server | `scripts/run-webui-server.mjs` via `pnpm --filter @mavis/webui dev:server` | Spawns itself under `tsx` with the repository's `tsconfig.standalone.json`; assembles the host, constructs `new WebuiService(...)` with the registry and the terminal manager, calls `service.start()`, and closes the service on SIGINT/SIGTERM. Serves the built client from `dist-webui/client/`. `WEBUI_SERVER_PORT` overrides 2233. `WEBUI_HOST` overrides the bind address (`127.0.0.1` by default); a wider bind prints the LAN URLs and the per-start credential, and a request that did not arrive over loopback presents that credential (ADR 0031). | +| Development server | `scripts/run-webui-server.mjs` via `pnpm --filter @mavis/webui dev:server` | Spawns itself under `tsx` with the repository's `tsconfig.standalone.json`; assembles the host, constructs `new WebuiService(...)` with the registry and the terminal manager, calls `service.start()`, and closes the service on SIGINT/SIGTERM or an authenticated `POST /webui-shutdown`. The HTTP route acknowledges the request before the same bounded process cleanup starts. Serves the built client from `dist-webui/client/`. `WEBUI_SERVER_PORT` overrides 2233. `WEBUI_HOST` overrides the bind address (`127.0.0.1` by default); a wider bind prints the LAN URLs and the per-start credential, and a request that did not arrive over loopback presents that credential (ADR 0031). | | Packaged service | `runtime/index.ts` + `server/index.ts` | The runtime's public entry and the server build entry. `scripts/build-webui.mjs` bundles both. | | npm preview package | `release/webui-npm/` | See [`../releasing.md`](../releasing.md). | diff --git a/packages/webui/src/client/features/account/ui/UserMenu.tsx b/packages/webui/src/client/features/account/ui/UserMenu.tsx index 624f5f916..c478a1313 100644 --- a/packages/webui/src/client/features/account/ui/UserMenu.tsx +++ b/packages/webui/src/client/features/account/ui/UserMenu.tsx @@ -282,6 +282,7 @@ export function UserMenu({ const loadUsage = (forceRefresh = false) => { setUsageOpen(true); setSigninOpen(false); + if (!forceRefresh && (usageOpen || accountSnapshot.usage.status === "loading")) return; void accountWorkflows?.loadUsage(forceRefresh); }; @@ -310,7 +311,7 @@ export function UserMenu({ const avatarUrl = profile?.avatarUrl?.trim() || profile?.defaultAvatarUrl?.trim(); const planLabel = formatRailPlanLabel( - usage.status === "ready" ? usage.result : undefined, + accountSnapshot.usage.result, ); const identity = objectValue(account?.identity); const auth = objectValue(account?.auth); diff --git a/packages/webui/src/client/features/sessions/ui/SessionRailRegion.tsx b/packages/webui/src/client/features/sessions/ui/SessionRailRegion.tsx index 71e0882a2..ce694d902 100644 --- a/packages/webui/src/client/features/sessions/ui/SessionRailRegion.tsx +++ b/packages/webui/src/client/features/sessions/ui/SessionRailRegion.tsx @@ -97,7 +97,7 @@ export function SessionRailRegion(props: { : null}