Skip to content

refactor(web-ui): UI polish, static URL, skills redesign, and v0.0.35 bump#170

Merged
ymkiux merged 24 commits into
mainfrom
chore/revert-version-34
May 24, 2026
Merged

refactor(web-ui): UI polish, static URL, skills redesign, and v0.0.35 bump#170
ymkiux merged 24 commits into
mainfrom
chore/revert-version-34

Conversation

@ymkiux
Copy link
Copy Markdown
Collaborator

@ymkiux ymkiux commented May 23, 2026

Summary

  • Redesign Usage tab with "River of Time" aesthetic (SVG bezier curve, delta calculations, animated empty states)
  • Disable local provider selection in codex/claude config mode (visual only, not removed)
  • Rename 'claude-local' to 'local' with system badge, remove subtitle hint
  • Redesign Skills tab to extreme minimalist single-column flow layout
  • Implement static URL: browser URL always stays at /, all state via localStorage
  • Fix /web-ui/ 404 and nested path duplication issues
  • Bump version to 0.0.35

Tests

  • Manual verification of all UI changes (config panels, skills modal, usage tab)
  • Responsive design tested on mobile viewport
  • URL routing E2E tests (test-web-ui-url-routing.js)
  • URL normalization unit tests (24 cases, all passing)
  • Share link generation tested (clipboard only, no URL mutation)

Summary by CodeRabbit

  • New Features

    • Usage tab overhaul: hero metrics with deltas, refresh, interactive wave chart, clickable day legend, and compact Top/Recent/Paths lists.
    • Skills: quick-open skills menu, single-skill import, and simplified Market flow.
    • Settings/providers: local provider can be disabled/read-only; keyboard nav for settings tabs.
  • UI / Styling

    • New delta & heatmap colors; updated responsive styles for usage, skills, disabled cards, and status-strip.
  • Behavior

    • URL canonicalization and stricter routing; legacy /web-ui endpoints redirect or 404. CSP tightened (removed unsafe-eval).
  • Tests

    • Added E2E and unit tests for routing, assets, canonicalization, and session UI.
  • Chores

    • Package version bumped; added compiler dependency and switched to Vue runtime production build.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

Warning

Review limit reached

@awsl233777, we couldn't start this review because you've used your available PR reviews for now.

Your plan currently allows 2 reviews/hour. Refill in 16 minutes and 53 seconds.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more review capacity refills, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: db8bf2e6-5149-4a0b-abfd-08bf9ebc8704

📥 Commits

Reviewing files that changed from the base of the PR and between aa3434d and 8251337.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (41)
  • cli.js
  • package.json
  • tests/e2e/test-web-ui-assets.js
  • tests/e2e/test-web-ui-session-tab.js
  • tests/e2e/test-web-ui-url-routing.js
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/test-web-ui-url-normalization.mjs
  • tests/unit/web-run-host.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • tests/unit/web-ui-url-canonicalization.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.sessions.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/sessions-filters-url.mjs
  • web-ui/modules/skills.methods.mjs
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/partials/index/panel-usage.html
  • web-ui/res/vue.runtime.global.prod.js
  • web-ui/res/web-ui-render.precompiled.js
  • web-ui/session-helpers.mjs
  • web-ui/source-bundle.cjs
  • web-ui/styles/base-theme.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-usage.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css
📝 Walkthrough

Walkthrough

Wave-based usage UI with enriched session data, server/client /web-ui canonicalization, skills market/menu with single-item import, provider-disable UX and styles, source-bundle template precompilation, and many unit/E2E test updates; package version bumped.

Changes

Usage UI and data

Layer / File(s) Summary
Session entry enrichment
web-ui/logic.sessions.mjs
Extended per-session entries with sessionId, filePath, totalTokens, contextWindow for lists/charts.
Computed summaries & wave data
web-ui/modules/app.computed.session.mjs
Reshaped sessionsUsageSelectedDaySummary; added hero metrics (usageHeroMainValue, usageHeroSubLabel, usageHeroDelta, usageHeroDeltaClass), sessionsUsageSelectedDay, and sessionUsageWave building chart-ready points/paths and hover coords.
Usage panel template update
web-ui/partials/index/panel-usage.html
Replaced daily bar view with hero + SVG wave visualization, added refresh control, updated empty/loading states, reworked right-hand Top Sessions/Recent Activity/Top Paths lists.
Theme & usage styles
web-ui/styles/base-theme.css, web-ui/styles/sessions-usage.css
Added delta and heatmap design tokens and extensive style updates for hero, wave, compact lists, heatmap, and responsive behavior.

Web UI routing and normalization

Layer / File(s) Summary
Server: /web-ui asset handling
cli.js
Added /web-ui/index.html 302→/ redirect (preserving query), early 404 for empty/index paths under /web-ui/, Cache-Control: no-store on UI assets, removed unsafe-eval from CSP, refactored non-loopback auth/rate-limit logic, and serve bundled index.html only for exact /.
Client: runtime canonicalization
web-ui/modules/sessions-filters-url.mjs, web-ui/app.js, web-ui/modules/app.methods.navigation.mjs
Add canonicalizeWebUiUrl/installWebUiUrlCanonicalization to normalize /web-ui variants, monkey-patch history writes, invoke canonicalization from navigation methods, and strip search/hash at mount time.

Sessions URL & share sync

Layer / File(s) Summary
Restore cleanup
web-ui/modules/app.methods.session-browser.mjs
Clear search/hash when restoring session filter cache to avoid stale URL state.
Build & sync share URL
web-ui/modules/sessions-filters-url.mjs
Construct root-based share URLs (origin + '/') and conditionally set s_* params; make syncSessionsFilterUrl a no-op (history writes removed).

Skills market and import

Layer / File(s) Summary
Skills actions
web-ui/modules/skills.methods.mjs
Added openSkillsMenu() and importSingleSkill(skill) (single-item import flow with API call, in-progress guards, messages, and refresh).
Market template changes
web-ui/partials/index/panel-market.html
Replaced market overview with minimalist header, target switch, installed/import skills-flow-panels, quick actions wired to new methods.
Skills-market styles
web-ui/styles/skills-market.css
Large new stylesheet section for minimalist skills flow layout, quick actions grid, spinner animation, and responsive rules.

Provider toggles and disabled-state UI

Layer / File(s) Summary
Computed/method additions
web-ui/modules/app.computed.dashboard.mjs, web-ui/modules/app.methods.claude-config.mjs
Added isLocalProviderDisabled and isClaudeLocalBridgeDisabled hooks.
Provider card templates
web-ui/partials/index/panel-config-claude.html, web-ui/partials/index/panel-config-codex.html
Cards conditionally apply disabled class, prevent interaction and remove from tab order when disabled; claude-local title adjusted.
Disabled card styles
web-ui/styles/titles-cards.css
Added .card.disabled rules to dim and suppress interactions.

Source bundling

Layer / File(s) Summary
Template extraction & compile
web-ui/source-bundle.cjs
Add HTML extraction helpers and compileWebUiTemplateToRenderScript using @vue/compiler-dom; prepend compiled render script to the executable bundle and export helpers.
Bundle/runtime wiring
web-ui/app.js, web-ui/index.html
Load runtime canonicalization at DOMContentLoaded; optionally use window.__CODEXMATE_WEB_UI_RENDER__ as app render function; switch included Vue runtime script to runtime-only build.

Tests (unit & E2E)

Layer / File(s) Summary
E2E: URL routing coverage
tests/e2e/test-web-ui-url-routing.js
New comprehensive routing assertions: root HTML served only at /, legacy /web-ui* entrypoints 404/302 behavior, resource handling, nested-path normalization, traversal security (403), private-resource protection (404), and asset src consistency.
E2E: assets expectations
tests/e2e/test-web-ui-assets.js
Expect /web-ui/index.html redirect to /, /web-ui/ 404, app.js includes precompiled render function and avoids with(...) usage.
Unit: URL normalization tests
tests/unit/test-web-ui-url-normalization.mjs, tests/unit/web-ui-url-canonicalization.test.mjs
Add normalizer helpers, test cases, install/replaceState monkey-patch tests, bundled boot-script ordering checks, and run harness exports.
Unit updates & parity
Various unit tests
Adjust template/unit assertions (runtime script change, usage/market selectors), extend parity allowlists for new methods/computeds, add compact-layout responsive assertions, and update web-run-host expectations.

Accessibility & misc UI

Layer / File(s) Summary
Header button attributes
web-ui/partials/index/layout-header.html
Added explicit type="button" to top-tab navigation buttons and a status-strip placeholder.
Settings keyboard navigation
web-ui/modules/app.methods.session-trash.mjs, web-ui/partials/index/panel-settings.html
Add onSettingsTabKeydown handler and wire @keydown on settings tab buttons for keyboard navigation.

CLI / server changes

Layer / File(s) Summary
CLI security & response behavior
cli.js
Remove 'unsafe-eval' from CSP, add optional headers to JSON responses, refactor non-loopback auth/rate-limit behavior to use shared helpers, and ensure no-store cache-control for UI assets.

Package manifest

Layer / File(s) Summary
Version bump & dependency
package.json
Updated package version from 0.1.00.0.35 and added @vue/compiler-dom dependency for template compilation.

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs:

Suggested reviewers:

  • awsl233777
  • SurviveM

"I hopped along the code-lined stream,
Waves of tokens glint and gleam.
Paths now tidy, guards stand true,
Skill flows open — new and new.
🐇💫"

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/revert-version-34

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
package.json (2)

27-35: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

CI will fail: package.json is missing scripts referenced by .github/workflows/ci.yml

.github/workflows/ci.yml runs npm run ci:install, npm run ci:lint, and npm run ci:test, but package.json no longer defines ci:install, ci:lint, or ci:test in its scripts section (so these steps will fail immediately).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 27 - 35, Add the missing CI scripts so the
workflow steps succeed: define "ci:install" (e.g., "npm ci" or the project's
install command), "ci:lint" (e.g., run your linter like "eslint ." or delegate
to an existing "lint" script), and "ci:test" (e.g., "npm test" or the project's
test command) in the package.json "scripts" object; ensure the names exactly
match ci:install, ci:lint, and ci:test so .github/workflows/ci.yml can run them,
and adjust the specific commands to match the repo's existing lint/test
toolchain if different.

27-35: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Restore the CI/test script contract in package.json

tests/unit/ci-workflow-contract.test.mjs asserts exact package.json scripts entries for:

  • ci:install: node tools/ci/run-check.js install
  • ci:lint: node tools/ci/run-check.js lint
  • ci:test: node tools/ci/run-check.js test
  • test: npm run test:unit && npm run test:e2e
  • test:ci: node tools/ci/run-check.js all
  • test:unit: node tests/unit/run.mjs
  • test:e2e: node tests/e2e/run.js

None of these scripts are present in the current package.json, so this unit test will fail and contradict any “version change only / Tests: N/A” claim.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 27 - 35, Restore the missing npm script entries in
package.json so they match the CI/test contract checked by
tests/unit/ci-workflow-contract.test.mjs: add script keys "ci:install",
"ci:lint", "ci:test", "test", "test:ci", "test:unit", and "test:e2e" with the
exact command strings required by the test (e.g., "ci:install" -> node
tools/ci/run-check.js install, "ci:lint" -> node tools/ci/run-check.js lint,
"ci:test" -> node tools/ci/run-check.js test, "test" -> npm run test:unit && npm
run test:e2e, "test:ci" -> node tools/ci/run-check.js all, "test:unit" -> node
tests/unit/run.mjs, "test:e2e" -> node tests/e2e/run.js) so the scripts keys
(ci:install, ci:lint, ci:test, test, test:ci, test:unit, test:e2e) exactly match
the expected strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 3: The package.json version was bumped to "0.0.34" but package-lock.json
still lists "0.1.0"; regenerate the lockfile so both agree by running a lockfile
update (e.g., run npm install or npm install --package-lock-only) which will
update packages[""].version and the top-level version in package-lock.json to
"0.0.34", then add and commit the regenerated package-lock.json alongside
package.json.

---

Outside diff comments:
In `@package.json`:
- Around line 27-35: Add the missing CI scripts so the workflow steps succeed:
define "ci:install" (e.g., "npm ci" or the project's install command), "ci:lint"
(e.g., run your linter like "eslint ." or delegate to an existing "lint"
script), and "ci:test" (e.g., "npm test" or the project's test command) in the
package.json "scripts" object; ensure the names exactly match ci:install,
ci:lint, and ci:test so .github/workflows/ci.yml can run them, and adjust the
specific commands to match the repo's existing lint/test toolchain if different.
- Around line 27-35: Restore the missing npm script entries in package.json so
they match the CI/test contract checked by
tests/unit/ci-workflow-contract.test.mjs: add script keys "ci:install",
"ci:lint", "ci:test", "test", "test:ci", "test:unit", and "test:e2e" with the
exact command strings required by the test (e.g., "ci:install" -> node
tools/ci/run-check.js install, "ci:lint" -> node tools/ci/run-check.js lint,
"ci:test" -> node tools/ci/run-check.js test, "test" -> npm run test:unit && npm
run test:e2e, "test:ci" -> node tools/ci/run-check.js all, "test:unit" -> node
tests/unit/run.mjs, "test:e2e" -> node tests/e2e/run.js) so the scripts keys
(ci:install, ci:lint, ci:test, test, test:ci, test:unit, test:e2e) exactly match
the expected strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 73bbea5c-ac4b-4fc2-a2a1-099b9279b9af

📥 Commits

Reviewing files that changed from the base of the PR and between aa3434d and e34e971.

📒 Files selected for processing (1)
  • package.json
📜 Review details
🔇 Additional comments (1)
package.json (1)

27-35: ⚡ Quick win

PR objectives claim "version change only" but scripts were removed.

The PR description states "N/A — version change only" for tests, but the AI summary and the annotated changes show that multiple scripts have been removed from the scripts section, including:

  • CI-related scripts (ci:install, ci:lint, ci:test)
  • Test-related scripts (test, test:ci, test:unit, test:e2e)
  • Other development scripts

This is not a "version change only" PR. The removal of these scripts has significant implications for CI workflows and test execution.

Comment thread package.json Outdated
- Merge current session bar with hero metrics for unified focus
- Replace bar chart with SVG bezier curve wave visualization
- Add automatic delta calculation (7d vs prior 7d, 30d vs prior 30d)
- Redesign empty states with animated SVG illustrations
- Simplify list views with bullet-point compact style
- Enhance color system with delta indicators and heatmap levels
@ymkiux ymkiux changed the title chore: revert version to 0.0.34 refactor(web-ui): redesign Usage tab with 'River of Time' aesthetic May 23, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-ui/modules/app.computed.session.mjs`:
- Around line 608-610: The logic currently computes prevKey (and similarly
deltaKey at the other location) even when sessionsUsageTimeRange === 'all',
causing compare text to appear incorrectly; update the code that sets prevKey
(using baseMs, rangeDays, dayMs) to first check that sessionsUsageTimeRange !==
'all' (and that baseMs is finite) and only then compute the date string,
otherwise set prevKey to '' (apply the same guard to the other occurrence around
deltaKey at the later block). Ensure you reference the existing variables
prevKey, baseMs, rangeDays, dayMs and sessionsUsageTimeRange so the compute is
skipped when range === 'all'.
- Around line 717-726: usageHeroDelta and usageHeroDeltaClass compute previous
tokens from this.sessionsUsageList while current tokens are taken from
sessionUsageCharts.summary (filtered to supported sources), causing inconsistent
deltas; update the calculations so both current and previous use the same
dataset and filters — either switch the previous-period aggregation in the loop
to use sessionUsageCharts.summary (apply the same supported-source filter and
time-window logic) or apply the exact supported-source filter and aggregation
logic used by sessionUsageCharts.summary to this.sessionsUsageList; adjust the
logic in the methods/blocks named usageHeroDelta and usageHeroDeltaClass (also
mirror the change where similar logic appears around the other block at the
754-763 region) so both sides of the delta are computed from the identical
filtered dataset and token-aggregation routine.

In `@web-ui/partials/index/panel-usage.html`:
- Around line 95-99: Replace the non-interactive <span> elements used for
session wave labels with semantic interactive elements (e.g., <button
type="button">) so keyboard users can operate them; keep the same Vue bindings
and class names (sessionUsageWave.labels, :key="label.key", :class="{ active:
sessionsUsageSelectedDay === label.key }",
`@click`="selectSessionsUsageDay(label.key)", usage-wave-label) and add any
necessary accessible attributes (e.g., aria-pressed or aria-label) as
appropriate; make the same change for the other identical blocks referenced (the
occurrences around the other ranges mentioned).
- Around line 58-60: Replace the hardcoded English labels ("tokens", "API",
"total", "msgs") with your i18n translation calls so the UI uses localization
keys instead of literal strings; update the template expressions that build
usage labels (for example where usageCurrentSessionStats.tokenLabel,
usageCurrentSessionStats.apiDurationLabel,
usageCurrentSessionStats.totalDurationLabel, and any
usageCurrentSessionStats.msgs label are rendered) to use the project's
translation helper (e.g., $t or t) and corresponding keys (like "usage.tokens",
"usage.api", "usage.total", "usage.msgs"); apply the same change to the other
occurrences mentioned (lines around 106-109, 147, 162) so all literal labels are
replaced by translation lookups.

In `@web-ui/styles/sessions-usage.css`:
- Around line 303-305: The CSS selector .usage-card-hourly-heatmap doesn't match
the template's class .usage-hourly-heatmap so the overflow rule is never
applied; update the style rule to target .usage-hourly-heatmap (or alternatively
change the template class to .usage-card-hourly-heatmap) so the overflow-x: auto
style takes effect—look for the selector in the CSS file and align it with the
template class name (.usage-hourly-heatmap).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cfa3fabf-df01-400e-b94d-48311659d7a4

📥 Commits

Reviewing files that changed from the base of the PR and between e34e971 and 4b116bb.

📒 Files selected for processing (5)
  • web-ui/logic.sessions.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/partials/index/panel-usage.html
  • web-ui/styles/base-theme.css
  • web-ui/styles/sessions-usage.css
📜 Review details
🔇 Additional comments (2)
web-ui/logic.sessions.mjs (1)

552-553: LGTM!

Also applies to: 559-560

web-ui/styles/base-theme.css (1)

29-37: LGTM!

Comment on lines +608 to 610
const prevKey = Number.isFinite(baseMs)
? new Date(baseMs - (rangeDays * dayMs)).toISOString().slice(0, 10)
: '';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Selected-day “prev/delta” should be gated when range is all.

For sessionsUsageTimeRange === 'all', prevKey is still generated (using 7-day offset), so compare text appears even when compare mode is not applicable.

Suggested fix
+ const compareEnabled = this.sessionsUsageTimeRange !== 'all';
  const prevKey = Number.isFinite(baseMs)
-     ? new Date(baseMs - (rangeDays * dayMs)).toISOString().slice(0, 10)
+     && compareEnabled
+     ? new Date(baseMs - (rangeDays * dayMs)).toISOString().slice(0, 10)
      : '';

Also applies to: 675-676

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.computed.session.mjs` around lines 608 - 610, The logic
currently computes prevKey (and similarly deltaKey at the other location) even
when sessionsUsageTimeRange === 'all', causing compare text to appear
incorrectly; update the code that sets prevKey (using baseMs, rangeDays, dayMs)
to first check that sessionsUsageTimeRange !== 'all' (and that baseMs is finite)
and only then compute the date string, otherwise set prevKey to '' (apply the
same guard to the other occurrence around deltaKey at the later block). Ensure
you reference the existing variables prevKey, baseMs, rangeDays, dayMs and
sessionsUsageTimeRange so the compute is skipped when range === 'all'.

Comment on lines +717 to +726
for (const session of (Array.isArray(this.sessionsUsageList) ? this.sessionsUsageList : [])) {
if (!session || typeof session !== 'object') continue;
const updatedAtMs = Date.parse(session.updatedAt || '');
if (!Number.isFinite(updatedAtMs)) continue;
if (updatedAtMs >= prevStartMs && updatedAtMs < prevEndMs) {
const sessionTokens = Number.isFinite(Number(session.totalTokens))
? Math.max(0, Math.floor(Number(session.totalTokens)))
: 0;
prevTokens += sessionTokens;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Delta is computed from a different dataset than the headline total.

usageHeroDelta/usageHeroDeltaClass derive previous tokens from this.sessionsUsageList, while current tokens come from sessionUsageCharts.summary (filtered to supported sources). This can skew delta direction and percentage.

Suggested fix
- for (const session of (Array.isArray(this.sessionsUsageList) ? this.sessionsUsageList : [])) {
+ const comparableSessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions)
+     ? this.sessionUsageCharts.filteredSessions
+     : (Array.isArray(this.sessionsUsageList) ? this.sessionsUsageList : []);
+ for (const session of comparableSessions) {

Also applies to: 754-763

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.computed.session.mjs` around lines 717 - 726,
usageHeroDelta and usageHeroDeltaClass compute previous tokens from
this.sessionsUsageList while current tokens are taken from
sessionUsageCharts.summary (filtered to supported sources), causing inconsistent
deltas; update the calculations so both current and previous use the same
dataset and filters — either switch the previous-period aggregation in the loop
to use sessionUsageCharts.summary (apply the same supported-source filter and
time-window logic) or apply the exact supported-source filter and aggregation
logic used by sessionUsageCharts.summary to this.sessionsUsageList; adjust the
logic in the methods/blocks named usageHeroDelta and usageHeroDeltaClass (also
mirror the change where similar logic appears around the other block at the
754-763 region) so both sides of the delta are computed from the identical
filtered dataset and token-aggregation routine.

Comment on lines +58 to +60
<span class="usage-hero-active-stat">{{ usageCurrentSessionStats.tokenLabel }} tokens</span>
<span class="usage-hero-active-stat">{{ usageCurrentSessionStats.apiDurationLabel }} API</span>
<span class="usage-hero-active-stat">{{ usageCurrentSessionStats.totalDurationLabel }} total</span>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

New hardcoded English labels bypass i18n.

Strings like tokens, API, total, and msgs should use translation keys to avoid mixed-language UI.

Also applies to: 106-109, 147-147, 162-162

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-usage.html` around lines 58 - 60, Replace the
hardcoded English labels ("tokens", "API", "total", "msgs") with your i18n
translation calls so the UI uses localization keys instead of literal strings;
update the template expressions that build usage labels (for example where
usageCurrentSessionStats.tokenLabel, usageCurrentSessionStats.apiDurationLabel,
usageCurrentSessionStats.totalDurationLabel, and any
usageCurrentSessionStats.msgs label are rendered) to use the project's
translation helper (e.g., $t or t) and corresponding keys (like "usage.tokens",
"usage.api", "usage.total", "usage.msgs"); apply the same change to the other
occurrences mentioned (lines around 106-109, 147, 162) so all literal labels are
replaced by translation lookups.

Comment on lines +95 to +99
<span v-for="label in sessionUsageWave.labels" :key="label.key"
class="usage-wave-label"
:class="{ active: sessionsUsageSelectedDay === label.key }"
@click="selectSessionsUsageDay(label.key)">
{{ label.text }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clickable non-button elements break keyboard accessibility.

span/div elements with @click here are not keyboard-operable by default. Please switch to semantic <button type="button"> (or add tabindex + key handlers) for these interactive items.

Also applies to: 143-149, 158-164

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-usage.html` around lines 95 - 99, Replace the
non-interactive <span> elements used for session wave labels with semantic
interactive elements (e.g., <button type="button">) so keyboard users can
operate them; keep the same Vue bindings and class names
(sessionUsageWave.labels, :key="label.key", :class="{ active:
sessionsUsageSelectedDay === label.key }",
`@click`="selectSessionsUsageDay(label.key)", usage-wave-label) and add any
necessary accessible attributes (e.g., aria-pressed or aria-label) as
appropriate; make the same change for the other identical blocks referenced (the
occurrences around the other ranges mentioned).

Comment on lines 303 to 305
.usage-card-hourly-heatmap {
overflow-x: auto;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Hourly heatmap container selector doesn’t match template class.

CSS targets .usage-card-hourly-heatmap, but the template uses .usage-hourly-heatmap. The overflow behavior won’t apply.

Suggested fix
-.usage-card-hourly-heatmap {
+.usage-hourly-heatmap {
     overflow-x: auto;
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.usage-card-hourly-heatmap {
overflow-x: auto;
}
.usage-hourly-heatmap {
overflow-x: auto;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/styles/sessions-usage.css` around lines 303 - 305, The CSS selector
.usage-card-hourly-heatmap doesn't match the template's class
.usage-hourly-heatmap so the overflow rule is never applied; update the style
rule to target .usage-hourly-heatmap (or alternatively change the template class
to .usage-card-hourly-heatmap) so the overflow-x: auto style takes effect—look
for the selector in the CSS file and align it with the template class name
(.usage-hourly-heatmap).

ymkiux added 2 commits May 23, 2026 22:40
- Server: handle /web-ui/ requests by returning index.html
- Server: explicitly serve index.html for /web-ui/index.html requests
- Client: normalize URL on mount to fix /web-ui/web-ui/ duplicates
- Client: redirect /web-ui/ and /web-ui/index.html to /web-ui

Fixes the issue where refreshing in sessions tab caused
URL to become /web-ui/web-ui/index.html
- Use do-while loop to iteratively remove all /web-ui/web-ui/ patterns
- Handle edge cases like /web-ui/web-ui/web-ui/index.html
- Add additional check for /web-ui/web-ui/ prefix pattern
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-ui/app.js`:
- Around line 434-438: The normalization only handles '/web-ui/web-ui/' with a
trailing slash, leaving '/web-ui/web-ui' unmodified; update the logic around
url.pathname (the replace and startsWith checks) to also detect the exact
'/web-ui/web-ui' case (or broaden the regex to match both with and without
trailing slash) and rewrite it to '/web-ui' so both '/web-ui/web-ui' and
'/web-ui/web-ui/' are canonicalized to '/web-ui'.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dc579630-149a-4e25-b86f-b8d8d2ccbeda

📥 Commits

Reviewing files that changed from the base of the PR and between 686ec81 and 05035ac.

📒 Files selected for processing (1)
  • web-ui/app.js

Comment thread web-ui/app.js Outdated
- Add test-web-ui-url-routing.js E2E test for server routing
- Add test-web-ui-url-normalization.mjs unit test (24 cases)
- Update test-web-ui-assets.js expectations for /web-ui/ fix
- Fix shouldReplace check logic in app.js (use originalPathname)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/e2e/test-web-ui-url-routing.js`:
- Around line 198-208: The for-loop over htmlResponses uses a const loop counter
which causes a runtime error when incrementing i; change the declaration in the
loop header from const to let (for example in the loop that reads "for (const i
= 0; i < htmlResponses.length; i++)") so i is mutable, leaving the assertions
against htmlResponses[i], response.body.includes, and the error messages
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cad651be-3bb4-452b-b0e8-4b8c75efb327

📥 Commits

Reviewing files that changed from the base of the PR and between 05035ac and 3ded8c3.

📒 Files selected for processing (4)
  • tests/e2e/test-web-ui-assets.js
  • tests/e2e/test-web-ui-url-routing.js
  • tests/unit/test-web-ui-url-normalization.mjs
  • web-ui/app.js
📜 Review details
🔇 Additional comments (7)
web-ui/app.js (2)

430-444: Approved with note: past review comment still applies.

The fix to capture originalPathname at line 430 and use it for the comparison at line 442 correctly ensures shouldReplace is set when the pathname has changed from its initial value.

However, the past review comment regarding /web-ui/web-ui (without trailing slash) not being normalized remains valid. The current regex and startsWith checks only handle paths with trailing slashes like /web-ui/web-ui/.


422-429: LGTM!

Also applies to: 445-459

tests/e2e/test-web-ui-assets.js (1)

66-75: LGTM!

tests/e2e/test-web-ui-url-routing.js (1)

1-197: LGTM!

tests/unit/test-web-ui-url-normalization.mjs (3)

10-44: LGTM!


49-187: LGTM!


189-249: LGTM!

Comment on lines +198 to +208
for (const i = 0; i < htmlResponses.length; i++) {
const response = htmlResponses[i];
assert(
response.body.includes('src="/web-ui/app.js"'),
`HTML response ${i} should use absolute path for app.js`
);
assert(
!response.body.includes('src="web-ui/app.js"'),
`HTML response ${i} should not use relative path for app.js`
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix syntax error: loop counter must use let, not const.

The loop declares i as const but then increments it, which will throw a runtime error.

🐛 Proposed fix
-    for (const i = 0; i < htmlResponses.length; i++) {
+    for (let i = 0; i < htmlResponses.length; i++) {
         const response = htmlResponses[i];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const i = 0; i < htmlResponses.length; i++) {
const response = htmlResponses[i];
assert(
response.body.includes('src="/web-ui/app.js"'),
`HTML response ${i} should use absolute path for app.js`
);
assert(
!response.body.includes('src="web-ui/app.js"'),
`HTML response ${i} should not use relative path for app.js`
);
}
for (let i = 0; i < htmlResponses.length; i++) {
const response = htmlResponses[i];
assert(
response.body.includes('src="/web-ui/app.js"'),
`HTML response ${i} should use absolute path for app.js`
);
assert(
!response.body.includes('src="web-ui/app.js"'),
`HTML response ${i} should not use relative path for app.js`
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-web-ui-url-routing.js` around lines 198 - 208, The for-loop
over htmlResponses uses a const loop counter which causes a runtime error when
incrementing i; change the declaration in the loop header from const to let (for
example in the loop that reads "for (const i = 0; i < htmlResponses.length;
i++)") so i is mutable, leaving the assertions against htmlResponses[i],
response.body.includes, and the error messages unchanged.

- Server: /web-ui, /web-ui/, /web-ui/index.html now return 404
- Root path / is the only HTML entry point
- Client: auto-redirect from /web-ui/* to / with hash/params preserved
- Assets still served at /web-ui/app.js, /web-ui/styles.css, etc.
- Simplified routing logic, removed redundant path handling
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/test-web-ui-url-routing.js (1)

194-201: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix logic error: these responses are 404, not HTML.

Lines 66-76 assert that webUiNoSlash and webUiWithSlash have status code 404. Per the server implementation (cli.js:11424), 404 responses return plain text "Not Found", not HTML. These assertions will fail because the response body does not contain 'id="app"'.

🐛 Proposed fix: remove invalid HTML content checks on 404 responses
-    // 验证 /web-ui 和 /web-ui/ 返回相同内容(除了可能的细微差异)
-    assert(
-        webUiNoSlash.body.includes('id="app"'),
-        '/web-ui response should contain Vue app'
-    );
-    assert(
-        webUiWithSlash.body.includes('id="app"'),
-        '/web-ui/ response should contain Vue app'
-    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-web-ui-url-routing.js` around lines 194 - 201, The assertions
that check HTML content on 404 responses are incorrect—remove the two asserts
that test webUiNoSlash.body.includes('id="app"') and
webUiWithSlash.body.includes('id="app"') and either omit body checks entirely or
replace them with a plain-text 404 check (e.g., assert the body contains 'Not
Found') so the tests align with the server behavior for the webUiNoSlash and
webUiWithSlash responses.
♻️ Duplicate comments (2)
web-ui/app.js (1)

428-433: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The regex still doesn't handle /web-ui/web-ui without a trailing slash.

The pattern /\/+web-ui\/+web-ui\/+/g requires at least one slash after the second web-ui, so a path like /web-ui/web-ui (no trailing slash) remains unchanged and won't be normalized to /web-ui.

🔧 Suggested fix

Match zero or more trailing slashes to handle both cases:

 do {
     prevPathname = pathname;
-    pathname = pathname.replace(/\/+web-ui\/+web-ui\/+/g, '/web-ui/');
+    pathname = pathname.replace(/\/+web-ui\/+web-ui\/*/g, '/web-ui/');
 } while (pathname !== prevPathname);

Alternatively, use a lookahead to preserve exact trailing-slash behavior:

-    pathname = pathname.replace(/\/+web-ui\/+web-ui\/+/g, '/web-ui/');
+    pathname = pathname.replace(/\/+web-ui\/+web-ui(?=\/|$)/g, '/web-ui');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/app.js` around lines 428 - 433, The current loop that normalizes
repeated "/web-ui/web-ui" uses pathname.replace(/\/+web-ui\/+web-ui\/+/g,
'/web-ui/'), which misses cases like "/web-ui/web-ui" (no trailing slash);
update the replace call on pathname (the code referencing prevPathname and
pathname) to use a pattern that allows zero or more trailing slashes or a
lookahead (e.g., match "/+web-ui/+web-ui/*" or "/+web-ui/+web-ui(?=\/|$)"), and
replace with the normalized "/web-ui" (or "/web-ui/" if you want to keep a
trailing slash consistently) so the do/while loop cleans both "/web-ui/web-ui"
and "/web-ui/web-ui/" forms.
tests/e2e/test-web-ui-url-routing.js (1)

204-215: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix two issues: loop counter must use let, and HTML checks on 404 responses.

  1. The loop counter i is declared as const but incremented, which will throw a runtime error.
  2. The htmlResponses array includes webUiNoSlash, webUiWithSlash, and webUiIndexHtml, which are all 404 responses (asserted at lines 67-82). These 404 responses return plain text "Not Found", not HTML, so checking for 'src="/web-ui/app.js"' will fail.
🐛 Proposed fix: use `let` and remove invalid HTML checks
-    // 验证所有 HTML 响应都指向正确的静态资源路径
-    const htmlResponses = [webUiNoSlash, webUiWithSlash, webUiIndexHtml];
-    for (const i = 0; i < htmlResponses.length; i++) {
-        const response = htmlResponses[i];
-        assert(
-            response.body.includes('src="/web-ui/app.js"'),
-            `HTML response ${i} should use absolute path for app.js`
-        );
-        assert(
-            !response.body.includes('src="web-ui/app.js"'),
-            `HTML response ${i} should not use relative path for app.js`
-        );
-    }

Replace with checks on actual HTML responses:

+    // 验证实际的 HTML 响应(根路径)指向正确的静态资源路径
+    assert(
+        rootPath.body.includes('src="/web-ui/app.js"'),
+        'Root HTML response should use absolute path for app.js'
+    );
+    assert(
+        !rootPath.body.includes('src="web-ui/app.js"'),
+        'Root HTML response should not use relative path for app.js'
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-web-ui-url-routing.js` around lines 204 - 215, Change the loop
counter declaration from const to let for the for-loop that iterates
htmlResponses (variable names: htmlResponses and i). Remove the invalid HTML
assertions against the known-404 responses webUiNoSlash, webUiWithSlash, and
webUiIndexHtml; instead assert their status is 404 and that response.body
includes the plain-text "Not Found". If you still need to test HTML asset paths,
iterate only over actual HTML responses (not
webUiNoSlash/webUiWithSlash/webUiIndexHtml) or filter htmlResponses to exclude
404 entries before performing the 'src="/web-ui/app.js"' checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/e2e/test-web-ui-url-routing.js`:
- Around line 194-201: The assertions that check HTML content on 404 responses
are incorrect—remove the two asserts that test
webUiNoSlash.body.includes('id="app"') and
webUiWithSlash.body.includes('id="app"') and either omit body checks entirely or
replace them with a plain-text 404 check (e.g., assert the body contains 'Not
Found') so the tests align with the server behavior for the webUiNoSlash and
webUiWithSlash responses.

---

Duplicate comments:
In `@tests/e2e/test-web-ui-url-routing.js`:
- Around line 204-215: Change the loop counter declaration from const to let for
the for-loop that iterates htmlResponses (variable names: htmlResponses and i).
Remove the invalid HTML assertions against the known-404 responses webUiNoSlash,
webUiWithSlash, and webUiIndexHtml; instead assert their status is 404 and that
response.body includes the plain-text "Not Found". If you still need to test
HTML asset paths, iterate only over actual HTML responses (not
webUiNoSlash/webUiWithSlash/webUiIndexHtml) or filter htmlResponses to exclude
404 entries before performing the 'src="/web-ui/app.js"' checks.

In `@web-ui/app.js`:
- Around line 428-433: The current loop that normalizes repeated
"/web-ui/web-ui" uses pathname.replace(/\/+web-ui\/+web-ui\/+/g, '/web-ui/'),
which misses cases like "/web-ui/web-ui" (no trailing slash); update the replace
call on pathname (the code referencing prevPathname and pathname) to use a
pattern that allows zero or more trailing slashes or a lookahead (e.g., match
"/+web-ui/+web-ui/*" or "/+web-ui/+web-ui(?=\/|$)"), and replace with the
normalized "/web-ui" (or "/web-ui/" if you want to keep a trailing slash
consistently) so the do/while loop cleans both "/web-ui/web-ui" and
"/web-ui/web-ui/" forms.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4e90178-56a8-4919-bb3e-12e8dac438b8

📥 Commits

Reviewing files that changed from the base of the PR and between 3ded8c3 and 1a7ebd8.

📒 Files selected for processing (5)
  • cli.js
  • tests/e2e/test-web-ui-assets.js
  • tests/e2e/test-web-ui-url-routing.js
  • tests/unit/test-web-ui-url-normalization.mjs
  • web-ui/app.js
📜 Review details
🔇 Additional comments (13)
cli.js (2)

11422-11425: LGTM!


11524-11535: LGTM!

tests/e2e/test-web-ui-assets.js (1)

47-53: LGTM!

tests/e2e/test-web-ui-url-routing.js (5)

1-30: LGTM!


32-43: LGTM!


44-94: LGTM!


96-135: LGTM!


137-189: LGTM!

tests/unit/test-web-ui-url-normalization.mjs (5)

1-9: LGTM!


10-35: LGTM!


40-169: LGTM!


174-217: LGTM!


219-231: LGTM!

ymkiux added 7 commits May 24, 2026 01:34
- filter out 'local' provider from displayProvidersList when configMode is codex/claude
- prevents users from selecting local provider in these apps due to existing issues
- other modes (terminal) remain unaffected
- local provider card shows as disabled (50% opacity, non-clickable) in codex mode
- claude-local bridge card shows as disabled in claude mode
- adds CSS .card.disabled style with pointer-events: none
- cards remain visible but cannot be selected
- fix operator precedence issue in click handlers
- use proper ternary expressions with correct grouping
- prevents white screen error on config load
- isLocalProviderDisabled is a computed returning boolean, not a function
- should be accessed as property without () in template
- fixes white screen error on config load
- display name changed from 'claude-local' to 'local'
- adds system badge like codex provider readonly indicator
- internal id 'claude-local' remains unchanged for compatibility
- removes '勾选参与负载均衡的提供商' subtitle
- keeps card layout cleaner with just title and system badge
- Remove cluttered overview section with 6 stat cards
- Consolidate header with inline target switcher
- Single-column flow layout for better mobile experience
- Add count badges directly in panel headers
- Add quick action buttons in import panel
- Add individual import button for each skill
- Remove help panel (content available elsewhere)
- Remove root path display (moved to tooltip)
- Preserve all functionality while reducing visual noise

Design principles:
- Progressive disclosure: scroll to discover
- Large touch targets: 56px min height
- Visual hierarchy: clear content grouping
- Cleaner information architecture
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
web-ui/modules/skills.methods.mjs (1)

488-488: ⚡ Quick win

Align single-import in-flight guards with other mutating actions.

This guard omits skillsDeleting, skillsScanningImports, and skillsExporting, so this method can overlap with other mutations if called outside the disabled UI path.

Proposed fix
-            if (this.skillsImporting || this.skillsZipImporting) return;
+            if (
+                this.skillsDeleting
+                || this.skillsScanningImports
+                || this.skillsImporting
+                || this.skillsZipImporting
+                || this.skillsExporting
+            ) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/skills.methods.mjs` at line 488, The single-import guard
currently checks only skillsImporting and skillsZipImporting, allowing this
method to run concurrently with other mutations; update the condition (the if
that reads "if (this.skillsImporting || this.skillsZipImporting) return;") to
also include this.skillsDeleting, this.skillsScanningImports, and
this.skillsExporting so the method returns whenever any skills mutating flag is
set (or replace with a single helper like isMutatingSkills that ORs those flags
and use it here).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-ui/modules/skills.methods.mjs`:
- Around line 492-495: The single-import branch in importSelectedSkills() uses
the wrong payload key ("imports") for the 'import-skills' API call; change the
payload to match the bulk-import contract by sending items: [skill] (i.e., use
items instead of imports) while keeping targetApp: this.skillsTargetApp so the
backend reads the item correctly and the single-skill import is not a no-op.

In `@web-ui/partials/index/panel-market.html`:
- Around line 97-99: The import button (button with class "skills-flow-add" that
calls importSingleSkill(skill)) is icon-only and lacks an accessible name; add
an explicit accessible label by setting an aria-label or adding visually-hidden
text that includes the skill context (e.g., dynamic aria-label using skill.name)
and ensure the :disabled and `@click` behavior remains unchanged; update the
button element to include a descriptive attribute like :aria-label="`Import
skill ${skill.name}`" or insert a <span class="sr-only">Import {{ skill.name
}}</span> inside the button so screen readers announce the action.

---

Nitpick comments:
In `@web-ui/modules/skills.methods.mjs`:
- Line 488: The single-import guard currently checks only skillsImporting and
skillsZipImporting, allowing this method to run concurrently with other
mutations; update the condition (the if that reads "if (this.skillsImporting ||
this.skillsZipImporting) return;") to also include this.skillsDeleting,
this.skillsScanningImports, and this.skillsExporting so the method returns
whenever any skills mutating flag is set (or replace with a single helper like
isMutatingSkills that ORs those flags and use it here).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 433d5d46-78a8-4294-9155-71c84652fe02

📥 Commits

Reviewing files that changed from the base of the PR and between bd2f543 and 8511a41.

📒 Files selected for processing (5)
  • web-ui/modules/skills.methods.mjs
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-market.html
  • web-ui/styles/skills-market.css
📜 Review details
🧰 Additional context used
🪛 HTMLHint (1.9.2)
web-ui/partials/index/panel-config-claude.html

[error] 109-109: Special characters must be escaped : [ > ].

(spec-char-escape)

🔇 Additional comments (3)
web-ui/partials/index/panel-config-claude.html (1)

103-114: LGTM!

web-ui/partials/index/panel-config-codex.html (1)

123-123: LGTM!

web-ui/styles/skills-market.css (1)

1-294: LGTM!

Comment on lines +492 to +495
const res = await api('import-skills', {
targetApp: this.skillsTargetApp,
imports: [skill]
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the same import-skills payload contract as bulk import.

importSelectedSkills() sends items, but this path sends imports. If the backend only reads items, single import silently fails or no-ops.

Proposed fix
-                const res = await api('import-skills', {
-                    targetApp: this.skillsTargetApp,
-                    imports: [skill]
-                });
+                const res = await api('import-skills', {
+                    targetApp: this.skillsTargetApp,
+                    items: [{
+                        name: skill && skill.name,
+                        sourceApp: skill && skill.sourceApp
+                    }]
+                });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/skills.methods.mjs` around lines 492 - 495, The single-import
branch in importSelectedSkills() uses the wrong payload key ("imports") for the
'import-skills' API call; change the payload to match the bulk-import contract
by sending items: [skill] (i.e., use items instead of imports) while keeping
targetApp: this.skillsTargetApp so the backend reads the item correctly and the
single-skill import is not a no-op.

Comment on lines +97 to +99
<button type="button" class="skills-flow-add" @click="importSingleSkill(skill)" :disabled="loading || !!initError || skillsMarketBusy">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M10 4v6m3-3l-3 3-3-3"/></svg>
</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add an accessible name to the icon-only import button.

This action button has no text/ARIA label, so screen readers won’t announce its purpose.

Proposed fix
-                                    <button type="button" class="skills-flow-add" `@click`="importSingleSkill(skill)" :disabled="loading || !!initError || skillsMarketBusy">
+                                    <button
+                                        type="button"
+                                        class="skills-flow-add"
+                                        `@click`="importSingleSkill(skill)"
+                                        :disabled="loading || !!initError || skillsMarketBusy"
+                                        :aria-label="`${t('market.action.importSingle')}: ${skill.displayName || skill.name}`"
+                                        :title="`${t('market.action.importSingle')}: ${skill.displayName || skill.name}`">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button type="button" class="skills-flow-add" @click="importSingleSkill(skill)" :disabled="loading || !!initError || skillsMarketBusy">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M10 4v6m3-3l-3 3-3-3"/></svg>
</button>
<button
type="button"
class="skills-flow-add"
`@click`="importSingleSkill(skill)"
:disabled="loading || !!initError || skillsMarketBusy"
:aria-label="`${t('market.action.importSingle')}: ${skill.displayName || skill.name}`"
:title="`${t('market.action.importSingle')}: ${skill.displayName || skill.name}`">
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M10 4v6m3-3l-3 3-3-3"/></svg>
</button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-market.html` around lines 97 - 99, The import
button (button with class "skills-flow-add" that calls importSingleSkill(skill))
is icon-only and lacks an accessible name; add an explicit accessible label by
setting an aria-label or adding visually-hidden text that includes the skill
context (e.g., dynamic aria-label using skill.name) and ensure the :disabled and
`@click` behavior remains unchanged; update the button element to include a
descriptive attribute like :aria-label="`Import skill ${skill.name}`" or insert
a <span class="sr-only">Import {{ skill.name }}</span> inside the button so
screen readers announce the action.

ymkiux added 2 commits May 24, 2026 02:41
- Remove all URL routing, URL stays as pure '/'
- Delete query parameters and hash from browser address bar
- Remove history.pushState/replaceState operations for tab switching
- Remove session filter URL synchronization
- Share links still work (generate URL with params for clipboard)
- When opening share link, params are applied then URL cleaned
- All state managed via memory + localStorage

Changes:
- app.js: Clean URL on mount, remove resource path fix logic
- app.methods.navigation.mjs: Remove URL operations in switchMainTab
- sessions-filters-url.mjs: Empty syncSessionsFilterUrl, fix share URL base
- app.methods.session-browser.mjs: Clean URL after applying URL state
@ymkiux ymkiux changed the title refactor(web-ui): redesign Usage tab with 'River of Time' aesthetic refactor(web-ui): UI polish, static URL, skills redesign, and v0.0.35 bump May 23, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
web-ui/modules/sessions-filters-url.mjs (1)

75-78: 💤 Low value

Consider removing no-op function and its call sites.

syncSessionsFilterUrl is now an intentional no-op, but it's still called 6 times across app.methods.session-browser.mjs (lines 481, 506, 512, 518, 576, 587). Calling an empty function creates visual noise and may confuse future maintainers.

♻️ Refactor options

Option 1: Remove the function and all call sites.

Option 2: Keep the function as a no-op stub for potential future URL sync requirements (current approach).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/sessions-filters-url.mjs` around lines 75 - 78, Remove the
no-op exported function syncSessionsFilterUrl and delete every invocation of
syncSessionsFilterUrl across the codebase (there are ~6 call sites in the
session-browser methods), and also remove its export/imports; alternatively, if
you want to keep a placeholder for future use, leave a single minimal stub named
syncSessionsFilterUrl with a clear comment and remove the redundant calls so
callers no longer invoke it; ensure imports are updated so there are no unused
imports or undefined references.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Line 3: The package.json "version" field was incorrectly set to 0.0.35 while
the PR intention is to revert to 0.0.34; open package.json, change the "version"
property value from "0.0.35" to "0.0.34" (ensure the "version" key is updated),
and then check and update any related metadata (lockfile or CI npm/publish
config) to remain consistent with that version.

In `@web-ui/app.js`:
- Around line 426-434: The redirect code for pathname checks currently clears
url.search and url.hash before redirecting, which discards query/hash; update
the flow so you first parse and apply query params and hash to the app state
(reuse the existing restoreSessionFilterCache logic or call the same routine
used at/near restoreSessionFilterCache) and only then perform a redirect that
preserves or cleans the URL as intended; specifically, in the block that handles
pathname === '/web-ui'... use the code that extracts window.location.search and
window.location.hash, pass them into the same state-restoration function (or
invoke restoreSessionFilterCache) to apply filters/share links, and afterward
call window.location.replace with a URL that either retains the original
search/hash or cleans them according to the PR objective.

---

Nitpick comments:
In `@web-ui/modules/sessions-filters-url.mjs`:
- Around line 75-78: Remove the no-op exported function syncSessionsFilterUrl
and delete every invocation of syncSessionsFilterUrl across the codebase (there
are ~6 call sites in the session-browser methods), and also remove its
export/imports; alternatively, if you want to keep a placeholder for future use,
leave a single minimal stub named syncSessionsFilterUrl with a clear comment and
remove the redundant calls so callers no longer invoke it; ensure imports are
updated so there are no unused imports or undefined references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 28bd0039-5d23-48c9-9492-b08548019394

📥 Commits

Reviewing files that changed from the base of the PR and between 8511a41 and 260c613.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • package.json
  • web-ui/app.js
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/sessions-filters-url.mjs
📜 Review details
🔇 Additional comments (4)
package.json (1)

3-3: ⚡ Quick win

Regenerate package-lock.json to match package.json version.

As flagged in previous reviews, if package-lock.json exists in this repository, it must be regenerated after the version change to ensure consistency.

Run the following script to check if package-lock.json needs updating:

#!/bin/bash
# Check if package-lock.json exists and verify version consistency

if [ -f package-lock.json ]; then
  echo "=== package.json version ==="
  jq -r '.version' package.json
  
  echo -e "\n=== package-lock.json version ==="
  jq -r '.version' package-lock.json
  
  echo -e "\n=== package-lock.json packages root version ==="
  jq -r '.packages[""].version' package-lock.json
else
  echo "package-lock.json not found"
fi
web-ui/modules/app.methods.session-browser.mjs (1)

267-273: LGTM!

web-ui/modules/app.methods.navigation.mjs (1)

422-422: LGTM!

web-ui/modules/sessions-filters-url.mjs (1)

60-63: LGTM!

Comment thread package.json
{
"name": "codexmate",
"version": "0.1.0",
"version": "0.0.35",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Version conflicts with PR objectives.

The PR description states: "Revert package.json version from 0.0.36 to 0.0.34; npm versions 0.0.35 and 0.0.36 are unpublished." However, this change sets the version to 0.0.35, which contradicts the stated goal and may conflict with the unpublished version.

🔧 Proposed fix
-  "version": "0.0.35",
+  "version": "0.0.34",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"version": "0.0.35",
"version": "0.0.34",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 3, The package.json "version" field was incorrectly set
to 0.0.35 while the PR intention is to revert to 0.0.34; open package.json,
change the "version" property value from "0.0.35" to "0.0.34" (ensure the
"version" key is updated), and then check and update any related metadata
(lockfile or CI npm/publish config) to remain consistent with that version.

Comment thread web-ui/app.js
Comment on lines +426 to +434
if (pathname === '/web-ui' || pathname === '/web-ui/' || pathname === '/web-ui/index.html') {
const url = new URL(window.location.href);
url.pathname = '/';
// 移除查询参数和 hash,保持 URL 纯净
url.search = '';
url.hash = '';
window.location.replace(url.toString());
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Redirect discards query parameters and hash, conflicting with PR objectives.

The PR description states: "auto-redirect /web-ui/* → / while preserving hash and query params." However, lines 430-431 explicitly clear search and hash before redirecting, which would lose information from URLs like /web-ui?tab=sessions#section.

If the design intent is to apply params before cleaning (as mentioned in the PR for "share links"), the current logic should:

  1. Read and apply params/hash to view model state (similar to restoreSessionFilterCache which already does this at line 516)
  2. Then redirect and clean the URL
🔍 Proposed approach
             const pathname = window.location.pathname;
             if (pathname === '/web-ui' || pathname === '/web-ui/' || pathname === '/web-ui/index.html') {
                 const url = new URL(window.location.href);
+                // Don't clear params yet - let restoreSessionFilterCache handle them first
+                // and then clear them after state is extracted
                 url.pathname = '/';
-                // 移除查询参数和 hash,保持 URL 纯净
-                url.search = '';
-                url.hash = '';
                 window.location.replace(url.toString());
                 return;
             }

Then ensure lines 436-441 still clean up after restoreSessionFilterCache runs (line 516).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/app.js` around lines 426 - 434, The redirect code for pathname checks
currently clears url.search and url.hash before redirecting, which discards
query/hash; update the flow so you first parse and apply query params and hash
to the app state (reuse the existing restoreSessionFilterCache logic or call the
same routine used at/near restoreSessionFilterCache) and only then perform a
redirect that preserves or cleans the URL as intended; specifically, in the
block that handles pathname === '/web-ui'... use the code that extracts
window.location.search and window.location.hash, pass them into the same
state-restoration function (or invoke restoreSessionFilterCache) to apply
filters/share links, and afterward call window.location.replace with a URL that
either retains the original search/hash or cleans them according to the PR
objective.

ymkiux added 2 commits May 24, 2026 02:59
- Fix sessionUsageDaily -> sessionUsageHourlyHeatmap assertion
- Remove obsolete loadSkillsMarketOverview and market-action-grid checks
- Update skills-target-chip selector (was market-target-chip)
- Fix provider card disabled state and tabindex assertions
- Remove market.pill importableDirect/importMissing checks
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/config-tabs-ui.test.mjs (1)

250-250: ⚡ Quick win

Provider-card keyboard coverage dropped Space-key behavior in this assertion.

Please keep Space-key handling asserted alongside Enter for this role="button" path, so disabled/local logic doesn’t regress keyboard accessibility coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/config-tabs-ui.test.mjs` at line 250, The test's regex for the
provider card button only asserts Enter key handling and omits Space key
coverage; update the assertion to include Space-key handling the same way as
Enter by matching an additional `@keydown.space.self.prevent` pattern that follows
the same ternary check "(provider.name === 'local' && isLocalProviderDisabled) ?
null : switchProvider(provider.name)", ensuring the disabled/local logic and
keyboard accessibility for role="button" remains covered; look for symbols
switchProvider, isLocalProviderDisabled, displayCurrentProvider, and
provider.name to locate the provider-card assertion to extend.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/web-ui-behavior-parity.test.mjs`:
- Around line 614-617: The test failure is due to missing new state fields from
the data-key allowlists; update the relevant allowedExtraCurrentKeys arrays in
web-ui-behavior-parity.test.mjs to include the new keys:
isLocalProviderDisabled, sessionUsageWave, sessionsUsageSelectedDay,
usageHeroDelta, usageHeroDeltaClass, usageHeroMainValue, and usageHeroSubLabel
so the parity checks accept the expanded UI state; locate the
allowedExtraCurrentKeys definitions referenced for the affected components
(search for allowedExtraCurrentKeys and the entries around 'pasteAgentsContent',
'importSingleSkill', 'openSkillsMenu', 'isClaudeLocalBridgeDisabled') and append
these new keys to the appropriate allowlist arrays.

---

Nitpick comments:
In `@tests/unit/config-tabs-ui.test.mjs`:
- Line 250: The test's regex for the provider card button only asserts Enter key
handling and omits Space key coverage; update the assertion to include Space-key
handling the same way as Enter by matching an additional
`@keydown.space.self.prevent` pattern that follows the same ternary check
"(provider.name === 'local' && isLocalProviderDisabled) ? null :
switchProvider(provider.name)", ensuring the disabled/local logic and keyboard
accessibility for role="button" remains covered; look for symbols
switchProvider, isLocalProviderDisabled, displayCurrentProvider, and
provider.name to locate the provider-card assertion to extend.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e0d9821a-972d-4895-8a72-04254603b58a

📥 Commits

Reviewing files that changed from the base of the PR and between 260c613 and 060f5e9.

📒 Files selected for processing (4)
  • package.json
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/web-run-host.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
✅ Files skipped from review due to trivial changes (1)
  • package.json
📜 Review details
🧰 Additional context used
🪛 GitHub Actions: ci / 0_ci (22).txt
tests/unit/web-ui-behavior-parity.test.mjs

[error] 740-740: Assertion failed (deepStrictEqual) in bundled app skeleton data key drift vs parity baseline. Expected [] but received keys: ['isLocalProviderDisabled','sessionUsageWave','sessionsUsageSelectedDay','usageHeroDelta','usageHeroDeltaClass','usageHeroMainValue','usageHeroSubLabel'].

🪛 GitHub Actions: ci / 1_ci (18).txt
tests/unit/web-ui-behavior-parity.test.mjs

[error] 740-740: Unit test failed (web UI behavior parity). AssertionError: Expected bundled app skeleton data keys to exactly match parity baseline. deepStrictEqual failed. Actual keys include: 'isLocalProviderDisabled', 'sessionUsageWave', 'sessionsUsageSelectedDay', 'usageHeroDelta', 'usageHeroDeltaClass', 'usageHeroMainValue', 'usageHeroSubLabel'; expected: []

🪛 GitHub Actions: ci / 2_ci (20).txt
tests/unit/web-ui-behavior-parity.test.mjs

[error] 740-740: Unit test failed: AssertionError: Expected values to be strictly deep-equal (deepStrictEqual). Differences: actual keys ['isLocalProviderDisabled','sessionUsageWave','sessionsUsageSelectedDay','usageHeroDelta','usageHeroDeltaClass','usageHeroMainValue','usageHeroSubLabel'] vs expected [].

🪛 GitHub Actions: ci / ci (18)
tests/unit/web-ui-behavior-parity.test.mjs

[error] 740-740: Unit test failed (TAP). captured bundled app skeleton only exposes expected data key drift versus parity baseline. AssertionError: Expected values to be strictly deep-equal; actual keys included ['isLocalProviderDisabled','sessionUsageWave','sessionsUsageSelectedDay','usageHeroDelta','usageHeroDeltaClass','usageHeroMainValue','usageHeroSubLabel'] but expected [] (deepStrictEqual).

🪛 GitHub Actions: ci / ci (20)
tests/unit/web-ui-behavior-parity.test.mjs

[error] 740-740: Test failed: 'captured bundled app skeleton only exposes expected data key drift versus parity baseline' assertion error. Expected deepStrictEqual to [] but actual contained keys: isLocalProviderDisabled, sessionUsageWave, sessionsUsageSelectedDay, usageHeroDelta, usageHeroDeltaClass, usageHeroMainValue, usageHeroSubLabel.

🪛 GitHub Actions: ci / ci (22)
tests/unit/web-ui-behavior-parity.test.mjs

[error] 740-740: Unit test failed: 'captured bundled app skeleton only exposes expected data key drift versus parity baseline' (AssertionError). Expected strictly deep-equal to [], but received keys: ['isLocalProviderDisabled','sessionUsageWave','sessionsUsageSelectedDay','usageHeroDelta','usageHeroDeltaClass','usageHeroMainValue','usageHeroSubLabel']

🔇 Additional comments (1)
tests/unit/web-run-host.test.mjs (1)

746-758: LGTM!

Also applies to: 774-791

Comment thread tests/unit/web-ui-behavior-parity.test.mjs
ymkiux added 2 commits May 24, 2026 10:36
- isLocalProviderDisabled (data key)
- sessionUsageWave, sessionsUsageSelectedDay, usageHeroDelta, usageHeroDeltaClass (computed keys)
- usageHeroMainValue
- usageHeroSubLabel
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/web-ui-behavior-parity.test.mjs (1)

412-452: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Incomplete allowlist update causes CI failure—add missing usageHeroMainValue and usageHeroSubLabel.

The allowlist was partially updated but is missing two keys that the current bundle exposes. CI fails because usageHeroMainValue and usageHeroSubLabel are not in allowedExtraCurrentKeys. The past review comment identified seven keys to add; this change added only five.

🔧 Add the missing keys
     allowedExtraCurrentKeys.push(
         'lang',
         'configTemplateContext',
         'configTemplateDiffVisible',
         'configTemplateDiffLoading',
         'configTemplateDiffError',
         'configTemplateDiffLines',
         'configTemplateDiffStats',
         'configTemplateDiffHasChangesValue',
         'configTemplateDiffFingerprint',
         '_configTemplateDiffPreviewRequestToken',
         'configTemplateDiffConfirmEnabled',
         'healthCheckBatchTotal',
         'healthCheckBatchDone',
         'healthCheckBatchFailed',
         'showHealthCheckModal',
         'showCodexBridgePoolModal',
         'showClaudeBridgePoolModal',
         'pluginsActiveId',
         'pluginsLoading',
         'pluginsError',
         'promptTemplatesListRaw',
         'promptTemplatesLoadedOnce',
         'promptTemplatesKeyword',
         'promptTemplateSelectedId',
         'promptTemplateDraftRaw',
         'promptTemplateVarValuesRaw',
         'promptTemplatesMode',
         'promptComposerCommand',
         'promptComposerPickerVisible',
         'promptComposerPickerKeyword',
         'promptComposerSelectedTemplateId',
         'promptComposerVarValuesRaw',
         'sessionConverting',
         'sessionImportingNative',
         'sessionSortMode',
         'sessionsUsageCompareEnabled',
         'sessionsUsageSelectedDayKey',
         'currentModels',
-        'isLocalProviderDisabled'
+        'isLocalProviderDisabled',
+        'usageHeroMainValue',
+        'usageHeroSubLabel'
     );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/web-ui-behavior-parity.test.mjs` around lines 412 - 452, The
test's allowlist update omitted two keys causing CI failures: add
"usageHeroMainValue" and "usageHeroSubLabel" to the allowedExtraCurrentKeys push
in the web-ui-behavior-parity test (the array pushed in
tests/unit/web-ui-behavior-parity.test.mjs); locate the
allowedExtraCurrentKeys.push(...) invocation and append those two string keys
alongside the existing entries so the test reflects the current bundle output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/unit/web-ui-behavior-parity.test.mjs`:
- Around line 412-452: The test's allowlist update omitted two keys causing CI
failures: add "usageHeroMainValue" and "usageHeroSubLabel" to the
allowedExtraCurrentKeys push in the web-ui-behavior-parity test (the array
pushed in tests/unit/web-ui-behavior-parity.test.mjs); locate the
allowedExtraCurrentKeys.push(...) invocation and append those two string keys
alongside the existing entries so the test reflects the current bundle output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 047fd19c-6c51-4c2a-b172-8eb35ff09219

📥 Commits

Reviewing files that changed from the base of the PR and between 060f5e9 and 8f00f2a.

📒 Files selected for processing (1)
  • tests/unit/web-ui-behavior-parity.test.mjs
📜 Review details
🧰 Additional context used
🪛 GitHub Actions: ci / 0_ci (20).txt
tests/unit/web-ui-behavior-parity.test.mjs

[error] 745-745: Unit test failed: AssertionError (ERR_ASSERTION) in web-ui behavior parity. Expected values to be strictly deep-equal; actual returned unexpected key drift array ['isLocalProviderDisabled','usageHeroMainValue','usageHeroSubLabel'] vs expected [].

🪛 GitHub Actions: ci / 1_ci (18).txt
tests/unit/web-ui-behavior-parity.test.mjs

[error] 745-745: Unit test failed (web-ui behavior parity). AssertionError: Expected values to be strictly deep-equal. Data key drift vs parity baseline was detected. Actual keys: ['isLocalProviderDisabled','usageHeroMainValue','usageHeroSubLabel']; expected: []

🪛 GitHub Actions: ci / 2_ci (22).txt
tests/unit/web-ui-behavior-parity.test.mjs

[error] 745-745: Unit test failed (assertion). AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: actual ['isLocalProviderDisabled','usageHeroMainValue','usageHeroSubLabel'] vs expected [] (deepStrictEqual).

🪛 GitHub Actions: ci / ci (18)
tests/unit/web-ui-behavior-parity.test.mjs

[error] 745-745: Unit test failed (web UI behavior parity). AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal. Expected: [] but actual drift keys: ['isLocalProviderDisabled','usageHeroMainValue','usageHeroSubLabel'].

🪛 GitHub Actions: ci / ci (20)
tests/unit/web-ui-behavior-parity.test.mjs

[error] 745-745: AssertionError in 'captured bundled app skeleton only exposes expected data key drift versus parity baseline'. Expected no data key drift, but actual drift keys were: ['isLocalProviderDisabled', 'usageHeroMainValue', 'usageHeroSubLabel']. (deepStrictEqual)

🪛 GitHub Actions: ci / ci (22)
tests/unit/web-ui-behavior-parity.test.mjs

[error] 745-745: Unit test failed: captured bundled app skeleton only exposes expected data key drift versus parity baseline. AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal. Actual drift keys: ['isLocalProviderDisabled','usageHeroMainValue','usageHeroSubLabel']; expected: [].

Move isLocalProviderDisabled from data keys to computed keys
as it is defined as a computed property in app.computed.dashboard.mjs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/web-ui-behavior-parity.test.mjs (1)

451-451: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

'isLocalProviderDisabled' is a computed property and must be in the computed allowlist, not the data allowlist.

CI is failing because 'isLocalProviderDisabled' is categorized as a data key (line 451) but is actually defined as a computed property in web-ui/modules/app.computed.dashboard.mjs. This causes the data key drift assertion at line 458 to fail with actual: ['isLocalProviderDisabled'], expected: [].

🔧 Proposed fix

Remove 'isLocalProviderDisabled' from the data allowlist:

         'sessionsUsageCompareEnabled',
         'sessionsUsageSelectedDayKey',
         'currentModels',
-        'isLocalProviderDisabled'
     );

Then add it to the computed allowlist around line 719:

         'displayProviderUrl',
         'isTransformProvider',
+        'isLocalProviderDisabled',
         'activeProviderModel',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/web-ui-behavior-parity.test.mjs` at line 451, Remove
'isLocalProviderDisabled' from the data allowlist in
tests/unit/web-ui-behavior-parity.test.mjs and add it to the computed allowlist
instead; specifically locate the array containing data allowlist entries (which
currently includes 'isLocalProviderDisabled') and delete that entry, then locate
the computed allowlist array (near where other computed keys are listed) and add
'isLocalProviderDisabled' so the test reflects its actual definition in
web-ui/modules/app.computed.dashboard.mjs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/unit/web-ui-behavior-parity.test.mjs`:
- Line 451: Remove 'isLocalProviderDisabled' from the data allowlist in
tests/unit/web-ui-behavior-parity.test.mjs and add it to the computed allowlist
instead; specifically locate the array containing data allowlist entries (which
currently includes 'isLocalProviderDisabled') and delete that entry, then locate
the computed allowlist array (near where other computed keys are listed) and add
'isLocalProviderDisabled' so the test reflects its actual definition in
web-ui/modules/app.computed.dashboard.mjs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6555d93e-6d53-49cf-8bc3-0c4688166115

📥 Commits

Reviewing files that changed from the base of the PR and between 8f00f2a and 35fb61c.

📒 Files selected for processing (1)
  • tests/unit/web-ui-behavior-parity.test.mjs
📜 Review details
🧰 Additional context used
🪛 GitHub Actions: ci / 0_ci (20).txt
tests/unit/web-ui-behavior-parity.test.mjs

[error] 747-747: Test failed: Web UI behavior parity assertion mismatch. Expected no data key drift versus parity baseline, but actual contained ['isLocalProviderDisabled']. (deepStrictEqual)

🪛 GitHub Actions: ci / 1_ci (22).txt
tests/unit/web-ui-behavior-parity.test.mjs

[error] 747-747: Unit test failed: bundled app skeleton exposed unexpected data key drift vs parity baseline. AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal. Actual: ['isLocalProviderDisabled'], Expected: [].

🪛 GitHub Actions: ci / ci (20)
tests/unit/web-ui-behavior-parity.test.mjs

[error] 747-747: Assertion failed in web UI behavior parity test: captured bundled app skeleton exposed unexpected data key. AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal (actual ['isLocalProviderDisabled'] vs expected []).

🪛 GitHub Actions: ci / ci (22)
tests/unit/web-ui-behavior-parity.test.mjs

[error] 747-747: Unit test failed (AssertionError). expected bundled app skeleton to match parity baseline with no unexpected data key drift, but actual contains unexpected key: 'isLocalProviderDisabled'. Assertion operator: deepStrictEqual.

🔇 Additional comments (1)
tests/unit/web-ui-behavior-parity.test.mjs (1)

709-710: LGTM!

@awsl233777
Copy link
Copy Markdown
Collaborator

本轮更新已处理会话浏览 tab 相关的静态 URL 回退问题:

  • 在主 tab / config tab 切换链路中加入运行时 URL 规范化,切入或切出“会话浏览”时会把遗留的 /web-ui/index.html、重复 /web-ui/web-ui/ 路径压回根路由,并清理 search/hash,避免地址栏重新跳到 legacy entry。
  • 顶部主导航按钮显式加 type="button",避免按钮默认 submit 行为在异常 DOM/浏览器场景下触发非预期导航。
  • 增加 session tab E2E 回归:从 /web-ui/index.html?tab=sessions#stale 切出会话 tab 时必须规范化回 http://127.0.0.1:3737/
  • 更新对应 UI 结构断言以兼容新增的 button type。

验证:

  • node tests/e2e/run.js --grep session-tab
  • node tests/unit/run.mjs(524 tests passed)
  • npm test
  • npm run lint(197 files passed)

剩余风险:目前没有发现新的阻塞;这轮主要覆盖会话 tab 切入/切出时的 legacy URL 污染路径。

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 24, 2026

@awsl233777 感谢您的详细更新说明!本轮针对会话浏览 tab 的静态 URL 回退改动看起来很完整,尤其是:

  • 在 tab 切换链路中加入规范化逻辑,有效防止 legacy entry 的 URL 污染;
  • 为顶部导航按钮显式声明 type="button" 是个好的防御性编码习惯;
  • 新增 E2E 回归覆盖从 /web-ui/index.html?tab=sessions#stale 切出的场景,测试覆盖很到位。

我现在开始对本次 PR 进行全面审查。

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/config-tabs-ui.test.mjs`:
- Line 66: The test's regex uses [\s\S]* which can span across elements; update
the assertion in tests/unit/config-tabs-ui.test.mjs so the pattern only matches
a single start-tag by replacing the [\s\S]* portion with a pattern that confines
matches to the tag's attributes (e.g. use [^>]* between attribute checks).
Target the assertion line that references v-if="taskOrchestrationTabEnabled",
class="top-tab" and id="tab-orchestration" so all three attributes must appear
within the same element's opening tag.

In `@web-ui/modules/app.methods.navigation.mjs`:
- Around line 67-73: The collapse loop that normalizes duplicate "/web-ui"
segments fails to catch "/web-ui/web-ui" without a trailing slash; update the
replace regex in the do/while (the pathname = pathname.replace(...) call) to
match the second "/web-ui" followed by either one or more slashes OR
end-of-string (e.g. use a pattern that permits (?:\/+|$) after the second
web-ui) so the loop collapses both "/web-ui/web-ui" and "/web-ui/web-ui/"
variants into a single "/web-ui/" and then the existing subsequent check can
canonicalize to "/".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ef26a964-3da0-423f-b113-1453805d1e29

📥 Commits

Reviewing files that changed from the base of the PR and between 35fb61c and a201a23.

📒 Files selected for processing (5)
  • tests/e2e/test-web-ui-session-tab.js
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/partials/index/layout-header.html
✅ Files skipped from review due to trivial changes (1)
  • web-ui/partials/index/layout-header.html
📜 Review details
🔇 Additional comments (3)
web-ui/modules/app.methods.navigation.mjs (1)

167-167: LGTM!

Also applies to: 442-442

tests/e2e/test-web-ui-session-tab.js (1)

22-40: LGTM!

Also applies to: 111-113, 141-144, 176-176

tests/unit/web-ui-behavior-parity.test.mjs (1)

450-450: LGTM!

Also applies to: 725-726

Comment thread tests/unit/config-tabs-ui.test.mjs
Comment on lines +67 to +73
do {
previousPathname = pathname;
pathname = pathname.replace(/\/+web-ui\/+web-ui\/+/, '/web-ui/');
} while (pathname !== previousPathname);
if (pathname === '/web-ui' || pathname === '/web-ui/' || pathname === '/web-ui/index.html') {
pathname = '/';
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize /web-ui/web-ui without trailing slash too.

The current collapse pattern only matches when there is a trailing slash after the second web-ui, so /web-ui/web-ui can bypass canonicalization.

Suggested patch
-                pathname = pathname.replace(/\/+web-ui\/+web-ui\/+/, '/web-ui/');
+                pathname = pathname.replace(/\/+web-ui\/+web-ui(?=\/|$)/, '/web-ui');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.navigation.mjs` around lines 67 - 73, The collapse
loop that normalizes duplicate "/web-ui" segments fails to catch
"/web-ui/web-ui" without a trailing slash; update the replace regex in the
do/while (the pathname = pathname.replace(...) call) to match the second
"/web-ui" followed by either one or more slashes OR end-of-string (e.g. use a
pattern that permits (?:\/+|$) after the second web-ui) so the loop collapses
both "/web-ui/web-ui" and "/web-ui/web-ui/" variants into a single "/web-ui/"
and then the existing subsequent check can canonicalize to "/".

@awsl233777
Copy link
Copy Markdown
Collaborator

Fixed the Web UI URL/layout regression on this PR.

Changes in latest commit aa88b1f:

  • Canonicalize /web-ui/index.html to /, preserving query params on the server redirect.
  • Install a Web UI runtime guard around history.replaceState / history.pushState so later client-side code cannot reintroduce /web-ui/index.html into the address bar.
  • Keep session filter share URLs rooted at / instead of inheriting stale /web-ui/index.html paths.
  • Add Cache-Control: no-store, max-age=0 for Web UI HTML/dynamic assets to avoid stale browser JS keeping the old URL behavior alive.
  • Stabilize compact/mobile layout by using a fixed viewport shell, scrolling inside .main-panel, and keeping topbar/status strip height stable across tabs.

Validation performed locally:

  • npm run test:unit → 528 tests passed
  • npm run test:e2e → passed
  • Chrome/CDP mobile probe:
    • Opening http://127.0.0.1:3737/web-ui/index.html?tab=sessions no longer leaves /web-ui/index.html in the final address.
    • Forced history.replaceState(..., '/web-ui/index.html?tab=config&s_source=codex') resolves to /?tab=config&s_source=codex.
    • Forced history.pushState(..., '/web-ui/index.html?tab=settings#data') resolves to /?tab=settings#data.
    • Mobile scroll remains on .main-panel with window.scrollY = 0, avoiding whole-page jump on tab switches.

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 24, 2026

@awsl233777 感谢详细的验证说明!本次 commit aa88b1f 的改动很全面,尤其是:

  • 服务端重定向保留了 query params(之前 /web-ui/index.html?tab=sessions 会丢失参数),这是之前轮次遗留的一个细节;
  • history.replaceState/pushState 运行时守卫从根本上阻断了客户端代码重新写入旧路径的可能性;
  • Cache-Control: no-store 避免浏览器缓存残留旧 JS 行为;
  • 移动端布局的 .main-panel 滚动固定也是之前版本的一个痛点。

我现在开始对最新提交进行全面审查。

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cli.js (2)

10647-10664: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't require CODEXMATE_HTTP_TOKEN for remote webhooks.

/hooks/* now hits the shared non-loopback auth gate before handleAutomationHook(), so valid GitHub/GitLab deliveries will start failing with 401/403 unless CODEXMATE_HTTP_TOKEN is also configured. That breaks remote automation even when the webhook secret is correct.

🔧 Suggested fix
         if (
             requestPath === '/api'
             || requestPath.startsWith('/api/import-')
             || requestPath.startsWith('/hooks/')
             || requestPath.startsWith('/download/')
         ) {
             const remoteAddr = req && req.socket ? req.socket.remoteAddress : '';
             const isLoopback = !remoteAddr || isLoopbackRemoteAddress(remoteAddr);
             if (!isLoopback) {
                 const rateLimitKey = (remoteAddr || 'unknown') + ':' + requestPath;
                 if (!checkRateLimit(rateLimitKey)) {
                     writeJsonResponse(res, 429, { error: 'Rate limit exceeded' }, { 'Retry-After': '60' });
                     return;
                 }
-                const auth = assertRequestAuthorized(req, res);
-                if (!auth.ok) {
-                    return;
+                if (!requestPath.startsWith('/hooks/')) {
+                    const auth = assertRequestAuthorized(req, res);
+                    if (!auth.ok) {
+                        return;
+                    }
                 }
             }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 10647 - 10664, The current auth gate requires
CODEXMATE_HTTP_TOKEN for non-loopback requests and incorrectly forces /hooks/*
through assertRequestAuthorized; change the logic so requests with requestPath
starting with '/hooks/' are not forced through assertRequestAuthorized here
(allow them to proceed to handleAutomationHook which validates webhook secrets).
Concretely, modify the conditional around assertRequestAuthorized in the block
that checks isLoopback so that if requestPath.startsWith('/hooks/') it skips
calling assertRequestAuthorized (or early-returns only on rate limit) and does
not require CODEXMATE_HTTP_TOKEN; keep rate limiting and loopback checks intact
and leave handleAutomationHook to perform its own signature validation.

11414-11418: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Legacy /web-ui entrypoints still 404 instead of canonicalizing.

/web-ui/ reaches the asset branch and returns 404 when relativePath === '', and /web-ui falls through to the final non-root 404. If the goal is to collapse legacy Web UI entry URLs back to /, both variants need the same redirect as /web-ui/index.html.

🔧 Suggested fix
-        } else if (requestPath.startsWith('/web-ui/')) {
+        } else if (requestPath === '/web-ui' || requestPath === '/web-ui/' || requestPath.startsWith('/web-ui/')) {
+            if (requestPath === '/web-ui' || requestPath === '/web-ui/') {
+                const rawUrl = typeof req.url === 'string' ? req.url : '';
+                const queryIndex = rawUrl.indexOf('?');
+                const query = queryIndex >= 0 ? rawUrl.slice(queryIndex) : '';
+                res.writeHead(302, {
+                    'Location': `/${query}`,
+                    'Content-Type': 'text/plain; charset=utf-8',
+                    'Cache-Control': 'no-store, max-age=0'
+                });
+                res.end('Found');
+                return;
+            }
             // Skip the /web-ui/ directory itself, which is handled above
             const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, '');
             const filePath = path.join(__dirname, normalized);
@@
-            if (relativePath === '' || relativePath === 'index.html') {
-                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
-                res.end('Not Found');
+            if (relativePath === '' || relativePath === 'index.html') {
+                const rawUrl = typeof req.url === 'string' ? req.url : '';
+                const queryIndex = rawUrl.indexOf('?');
+                const query = queryIndex >= 0 ? rawUrl.slice(queryIndex) : '';
+                res.writeHead(302, {
+                    'Location': `/${query}`,
+                    'Content-Type': 'text/plain; charset=utf-8',
+                    'Cache-Control': 'no-store, max-age=0'
+                });
+                res.end('Found');
                 return;
             }

Also applies to: 11525-11539

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 11414 - 11418, The handler currently returns 404 when
relativePath === '' or 'index.html' causing both /web-ui/ and /web-ui/index.html
(and the bare /web-ui fall-through) to 404; change the logic so that when
relativePath === '' or relativePath === 'index.html' you send an HTTP redirect
(301/302) to '/' instead of writing 404 (update the branch that checks
relativePath and replace the res.writeHead/res.end 404 with a redirect
response), and ensure the same canonicalization is applied to the code path that
previously let bare '/web-ui' fall through to the final non-root 404 so all
legacy /web-ui variants are redirected to '/'.
🧹 Nitpick comments (1)
tests/e2e/test-web-ui-assets.js (1)

47-50: ⚡ Quick win

Fix stale inline comment to match the redirect contract.

Line 47 says /web-ui/index.html returns 404, but Lines 49-50 assert a 302 redirect. Please update the comment to avoid confusion during failures.

✏️ Suggested patch
-    // /web-ui/index.html now returns 404 (only root path serves HTML)
+    // /web-ui/index.html now redirects to / (only root path serves HTML)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-web-ui-assets.js` around lines 47 - 50, Update the stale
inline comment above the getText call: the comment that currently says
"/web-ui/index.html now returns 404 (only root path serves HTML)" is incorrect
given the assertions on bundledIndex.statusCode === 302 and
bundledIndex.headers.location === '/'; change the comment to state that
/web-ui/index.html redirects (302) to the canonical root URL so it matches the
behavior being asserted around the getText call and the bundledIndex checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-ui/styles/responsive.css`:
- Around line 89-95: The .container rule uses fixed 100vh which can be clipped
on mobile; update both height and min-height to prefer dynamic viewport units by
adding a 100dvh fallback order: keep the existing 100vh then add 100dvh after it
(e.g. height: 100vh; height: 100dvh; and min-height: 100vh; min-height: 100dvh;)
so browsers that understand 100dvh use it while older browsers fall back to
100vh.

---

Outside diff comments:
In `@cli.js`:
- Around line 10647-10664: The current auth gate requires CODEXMATE_HTTP_TOKEN
for non-loopback requests and incorrectly forces /hooks/* through
assertRequestAuthorized; change the logic so requests with requestPath starting
with '/hooks/' are not forced through assertRequestAuthorized here (allow them
to proceed to handleAutomationHook which validates webhook secrets). Concretely,
modify the conditional around assertRequestAuthorized in the block that checks
isLoopback so that if requestPath.startsWith('/hooks/') it skips calling
assertRequestAuthorized (or early-returns only on rate limit) and does not
require CODEXMATE_HTTP_TOKEN; keep rate limiting and loopback checks intact and
leave handleAutomationHook to perform its own signature validation.
- Around line 11414-11418: The handler currently returns 404 when relativePath
=== '' or 'index.html' causing both /web-ui/ and /web-ui/index.html (and the
bare /web-ui fall-through) to 404; change the logic so that when relativePath
=== '' or relativePath === 'index.html' you send an HTTP redirect (301/302) to
'/' instead of writing 404 (update the branch that checks relativePath and
replace the res.writeHead/res.end 404 with a redirect response), and ensure the
same canonicalization is applied to the code path that previously let bare
'/web-ui' fall through to the final non-root 404 so all legacy /web-ui variants
are redirected to '/'.

---

Nitpick comments:
In `@tests/e2e/test-web-ui-assets.js`:
- Around line 47-50: Update the stale inline comment above the getText call: the
comment that currently says "/web-ui/index.html now returns 404 (only root path
serves HTML)" is incorrect given the assertions on bundledIndex.statusCode ===
302 and bundledIndex.headers.location === '/'; change the comment to state that
/web-ui/index.html redirects (302) to the canonical root URL so it matches the
behavior being asserted around the getText call and the bundledIndex checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e0a89c3-f79a-40ef-99ff-11e3660eb192

📥 Commits

Reviewing files that changed from the base of the PR and between a201a23 and aa88b1f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (20)
  • cli.js
  • package.json
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/run.mjs
  • tests/unit/web-run-host.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • tests/unit/web-ui-url-canonicalization.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/sessions-filters-url.mjs
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/res/vue.runtime.global.prod.js
  • web-ui/source-bundle.cjs
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/responsive.css
📜 Review details
🔇 Additional comments (24)
web-ui/index.html (1)

9-9: LGTM!

tests/unit/run.mjs (1)

16-16: LGTM!

tests/unit/compact-layout-ui.test.mjs (1)

42-48: LGTM!

Also applies to: 78-78

web-ui/partials/index/layout-header.html (2)

3-3: LGTM!

Also applies to: 13-13, 23-23, 34-34, 44-44, 54-54, 64-64, 74-74, 84-84


482-487: LGTM!

tests/unit/web-ui-source-bundle.test.mjs (1)

26-27: LGTM!

web-ui/styles/navigation-panels.css (1)

41-53: LGTM!

tests/unit/web-ui-url-canonicalization.test.mjs (1)

1-82: LGTM!

web-ui/modules/app.methods.session-trash.mjs (1)

221-249: LGTM!

package.json (1)

49-49: LGTM!

web-ui/app.js (1)

10-13: LGTM!

Also applies to: 31-31, 674-680

tests/unit/web-ui-behavior-parity.test.mjs (1)

518-518: LGTM!

web-ui/source-bundle.cjs (1)

10-13: LGTM!

Also applies to: 65-111, 267-267, 279-280

web-ui/partials/index/panel-settings.html (1)

22-23: LGTM!

Also applies to: 32-33

tests/unit/web-run-host.test.mjs (1)

720-720: LGTM!

Also applies to: 732-732, 774-790

cli.js (1)

10061-10065: LGTM!

Also applies to: 10624-10624, 11393-11403, 11425-11428, 11519-11522

tests/unit/config-tabs-ui.test.mjs (2)

68-68: The overly permissive [\s\S]* regex pattern that can span across elements was already flagged in a previous review with a suggested fix.


37-38: LGTM!

Also applies to: 61-62

web-ui/modules/sessions-filters-url.mjs (6)

66-75: LGTM!


77-87: LGTM!


89-115: LGTM!


117-132: LGTM!


134-137: LGTM!


58-64: Clarify legacy path coverage: /web-ui/web-ui/ is handled outside canonicalizeWebUiUrl

  • canonicalizeWebUiUrl in web-ui/modules/sessions-filters-url.mjs only rewrites pathname === '/web-ui/index.html' to / (used for sessions-filter URL/history canonicalization).
  • The duplicated legacy segments like /web-ui/web-ui/ are normalized elsewhere: canonicalizeWebUiRuntimeUrl in web-ui/modules/app.methods.navigation.mjs collapses repeated /web-ui/.../web-ui/... and redirects /web-ui//web-ui///web-ui/index.html to / (covered by tests/unit/test-web-ui-url-normalization.mjs cases).

Comment on lines +89 to +95
.container {
display: flex;
flex-direction: column;
height: 100vh;
min-height: 100vh;
overflow: hidden;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use dynamic viewport units to avoid mobile viewport clipping.

Line 92 and Line 93 rely on 100vh, which can truncate usable height on mobile browsers with dynamic address bars. Add 100dvh fallback to keep the panel reliably scrollable.

📱 Suggested patch
     .container {
         display: flex;
         flex-direction: column;
         height: 100vh;
         min-height: 100vh;
+        height: 100dvh;
+        min-height: 100dvh;
         overflow: hidden;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.container {
display: flex;
flex-direction: column;
height: 100vh;
min-height: 100vh;
overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
height: 100vh;
min-height: 100vh;
height: 100dvh;
min-height: 100dvh;
overflow: hidden;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/styles/responsive.css` around lines 89 - 95, The .container rule uses
fixed 100vh which can be clipped on mobile; update both height and min-height to
prefer dynamic viewport units by adding a 100dvh fallback order: keep the
existing 100vh then add 100dvh after it (e.g. height: 100vh; height: 100dvh; and
min-height: 100vh; min-height: 100dvh;) so browsers that understand 100dvh use
it while older browsers fall back to 100vh.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cli.js (1)

11393-11418: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redirect all legacy Web UI entry paths, not just /web-ui/index.html.

Line 11415 still returns 404 for /web-ui/, and Lines 11525-11539 do the same for /web-ui. Normalized duplicates like /web-ui/web-ui/index.html also fall through to 404, so legacy bookmarks/share URLs still won't canonicalize back to /.

Proposed fix
-        } else if (requestPath === '/web-ui/index.html') {
+        } else if (requestPath === '/web-ui' || requestPath === '/web-ui/' || requestPath === '/web-ui/index.html') {
             const rawUrl = typeof req.url === 'string' ? req.url : '';
             const queryIndex = rawUrl.indexOf('?');
             const query = queryIndex >= 0 ? rawUrl.slice(queryIndex) : '';
             res.writeHead(302, {
                 'Location': `/${query}`,
@@
-            // Empty relativePath means direct /web-ui/ access - return 404
-            if (relativePath === '' || relativePath === 'index.html') {
-                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
-                res.end('Not Found');
+            if (
+                relativePath === ''
+                || relativePath === 'index.html'
+                || relativePath === 'web-ui'
+                || relativePath === 'web-ui/index.html'
+            ) {
+                const rawUrl = typeof req.url === 'string' ? req.url : '';
+                const queryIndex = rawUrl.indexOf('?');
+                const query = queryIndex >= 0 ? rawUrl.slice(queryIndex) : '';
+                res.writeHead(302, {
+                    'Location': `/${query}`,
+                    'Content-Type': 'text/plain; charset=utf-8',
+                    'Cache-Control': 'no-store, max-age=0'
+                });
+                res.end('Found');
                 return;
             }

Also applies to: 11525-11539

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 11393 - 11418, The handler that checks requestPath and
computes normalized/filePath/relativePath currently returns 404 for legacy entry
URLs like '/web-ui', '/web-ui/', '/web-ui/index.html', and nested variants
(e.g., '/web-ui/web-ui/index.html'); update the conditional that now tests if
relativePath === '' || relativePath === 'index.html' to instead treat those
cases as redirects back to '/' preserving the original query: use
requestPath/rawUrl/query to send a 302 Location of `/${query}` for requestPath
'/web-ui' or '/web-ui/' and when relativePath is '' or 'index.html' or
startsWith('web-ui/index.html') (or otherwise indicates a legacy entry), rather
than returning 404; keep the security check using isPathInside(filePath, webDir)
and other logic intact.
♻️ Duplicate comments (12)
web-ui/modules/skills.methods.mjs (1)

492-495: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align importSingleSkill payload with the existing import-skills contract.

This path sends imports, while the existing import flow uses items. That mismatch can cause single-skill import to no-op depending on backend parsing.

Suggested fix
-                const res = await api('import-skills', {
-                    targetApp: this.skillsTargetApp,
-                    imports: [skill]
-                });
+                const res = await api('import-skills', {
+                    targetApp: this.skillsTargetApp,
+                    items: [{
+                        name: skill && skill.name,
+                        sourceApp: skill && skill.sourceApp
+                    }]
+                });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/skills.methods.mjs` around lines 492 - 495, The
importSingleSkill function currently calls api('import-skills', { targetApp:
this.skillsTargetApp, imports: [skill] }) using the wrong payload key; change
the payload to match the existing import-skills contract by sending items:
[skill] (and preserve targetApp) so the backend receives the same shape as the
bulk import flow. Locate importSingleSkill and the api('import-skills', ...)
invocation and replace the imports property with items to align with the
existing contract.
web-ui/modules/app.computed.session.mjs (2)

717-726: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the same filtered dataset for current and previous hero delta.

Current value is from sessionUsageCharts.summary, but previous-period aggregation here reads raw sessionsUsageList. This can produce incorrect delta direction/percentage.

Also applies to: 754-763

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.computed.session.mjs` around lines 717 - 726, The
previous-period aggregation is iterating raw this.sessionsUsageList while the
current value uses the filtered dataset sessionUsageCharts.summary, causing
incorrect deltas; change the loop(s) that compute prevTokens (and the similar
block at 754-763) to iterate over the same filtered array
(sessionUsageCharts.summary or the shared filtered variable used for current
calculations) and keep the same validation (typeof session check, Date.parse on
session.updatedAt, Number(session.totalTokens) handling) so both current and
previous hero values are computed from the identical data source.

608-610: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Gate prev/delta fields when range is all.

At Line 608–610, prevKey is still produced for all, so compare text can render when comparison should be disabled.

Also applies to: 675-676

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.computed.session.mjs` around lines 608 - 610, The computed
is still producing prev/delta values when the selected range is "all"; update
the prevKey calculation and the other delta/prev field generation to return
empty/disabled values whenever range === 'all' (or equivalent sentinel) instead
of computing with rangeDays/dayMs. Concretely, wrap the existing prevKey logic
(references: prevKey, rangeDays, dayMs) with a guard like if (range === 'all')
return '' (or set delta-related outputs to null/''), and apply the same gate to
the other occurrence noted (the block producing the delta at the second
location) so compare text cannot render for the "all" range.
web-ui/app.js (1)

425-444: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Defer URL query/hash cleanup until after filter-state hydration.

At Line 437–443, search/hash are removed before restoreSessionFilterCache() runs on Line 518. That can drop share-link/session params before they are applied.

Also applies to: 518-518

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/app.js` around lines 425 - 444, The URL search/hash cleanup runs too
early and can strip share-link/session params before restoreSessionFilterCache()
consumes them; leave the initial /web-ui -> / redirect logic (location.replace)
intact but remove or defer the second cleanup block that calls
window.history.replaceState('', ...) and clears url.search/url.hash, and instead
perform that cleanup only after restoreSessionFilterCache() has completed (i.e.,
move the cleanup to execute after the restoreSessionFilterCache() invocation).
Ensure you reference the pathname-check/redirect block and the cleanup currently
using window.history.replaceState and restoreSessionFilterCache() so reviewers
can locate and adjust the code.
web-ui/modules/app.methods.navigation.mjs (1)

69-70: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle /web-ui/web-ui without trailing slash in canonicalization.

Line 69 only matches the duplicated segment when a trailing slash exists. /web-ui/web-ui still slips through.

Suggested patch
-                pathname = pathname.replace(/\/+web-ui\/+web-ui\/+/, '/web-ui/');
+                pathname = pathname.replace(/\/+web-ui\/+web-ui(?=\/|$)/, '/web-ui');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.navigation.mjs` around lines 69 - 70, The current
canonicalization replace call on the pathname variable (inside the do/while
loop) only removes duplicated "/web-ui/web-ui/" when a trailing slash exists;
update the regex used in the pathname.replace(...) so it also matches the case
without a trailing slash (e.g., "/web-ui/web-ui") by making the trailing slash
optional or by matching one or more consecutive "/web-ui" segments, and ensure
the replacement normalizes to a single "/web-ui/" (maintaining the existing loop
and comparison to previousPathname).
web-ui/styles/sessions-usage.css (1)

303-305: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix hourly heatmap selector mismatch.

The selector at Line 303 doesn’t match the template class, so overflow-x: auto won’t apply.

Suggested patch
-.usage-card-hourly-heatmap {
+.usage-hourly-heatmap {
    overflow-x: auto;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/styles/sessions-usage.css` around lines 303 - 305, The CSS rule
targets .usage-card-hourly-heatmap but the template uses a different class name,
so overflow-x: auto never applies; locate the actual class used in the template
(e.g., the element rendering the hourly heatmap) and either rename the template
class to usage-card-hourly-heatmap or update the selector in
web-ui/styles/sessions-usage.css (replace .usage-card-hourly-heatmap) to exactly
match the template's class (or add the template class variant alongside
.usage-card-hourly-heatmap) so the overflow-x: auto rule is applied to the
correct element.
web-ui/partials/index/panel-usage.html (2)

58-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move new literal labels to i18n keys.

Hardcoded text (tokens, API, total, msgs) bypasses localization and causes mixed-language UI.

Also applies to: 106-109, 147-147, 162-162

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-usage.html` around lines 58 - 60, The template
currently uses hardcoded labels ("tokens", "API", "total", "msgs") in spans like
usageCurrentSessionStats.tokenLabel, usageCurrentSessionStats.apiDurationLabel
and usageCurrentSessionStats.totalDurationLabel; replace those literals with
i18n keys by calling the project's translation helper (e.g., $t or i18n filter)
in the template and update the locale JSON/YAML to add keys (e.g., usage.tokens,
usage.api, usage.total, usage.msgs); ensure you change the occurrences in this
block and the other reported locations (lines around 106-109, 147, 162) so all
UI text is localized and no hardcoded strings remain.

95-100: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use semantic buttons for click targets in Usage panel.

Lines 95–100 and 143–164 use clickable span/div elements, which are not keyboard-operable by default. Convert these to <button type="button"> (or add full keyboard semantics).

Also applies to: 143-149, 158-164

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-usage.html` around lines 95 - 100, Replace the
non-interactive span/div click targets in the Usage panel with semantic <button
type="button"> elements (or add full keyboard semantics) so they are
keyboard-operable; specifically change the v-for span that calls
selectSessionsUsageDay(label.key) and uses sessionsUsageSelectedDay to a button
preserving :key, class="usage-wave-label", :class binding, and the `@click`
handler, and do the same for the other click targets referenced (the blocks
around lines 143–149 and 158–164) ensuring any visual classes are kept,
tabindex/role removed, and aria-pressed or aria-current is added where
appropriate to reflect selection for accessibility.
tests/unit/config-tabs-ui.test.mjs (1)

68-68: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten orchestration tab regex to a single start-tag.

Line 68 uses a cross-element pattern and can false-pass. Constrain the match to one <button ...> opening tag with attribute lookaheads.

Suggested patch
-    assert.match(html, /v-if="taskOrchestrationTabEnabled"[^>]*class="top-tab"[\s\S]*id="tab-orchestration"/);
+    assert.match(
+        html,
+        /<button(?=[^>]*v-if="taskOrchestrationTabEnabled")(?=[^>]*class="top-tab")(?=[^>]*id="tab-orchestration")[^>]*>/
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/config-tabs-ui.test.mjs` at line 68, The test's regex allows
matching across multiple elements and can false-pass; update the assertion that
inspects the html variable (the assert.match call) to require a single opening
<button> start-tag by using a regex with a <button\b start, negative/positive
lookaheads that check for v-if="taskOrchestrationTabEnabled", class="top-tab",
and id="tab-orchestration" inside the same tag, and then match until the first
closing '>' so only one start-tag is matched.
web-ui/styles/responsive.css (1)

92-93: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prefer dynamic viewport units for mobile shell height.

Line 92–93 should include 100dvh after 100vh to avoid viewport clipping on mobile browsers with dynamic bars.

Suggested patch
     .container {
         display: flex;
         flex-direction: column;
         height: 100vh;
+        height: 100dvh;
         min-height: 100vh;
+        min-height: 100dvh;
         overflow: hidden;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/styles/responsive.css` around lines 92 - 93, Update the CSS
declarations that set the shell height (the height and min-height properties) to
include dynamic viewport units for mobile: keep the existing 100vh values but
add 100dvh as a fallback/alternative (i.e., specify both units so browsers that
support dynamic viewport height use 100dvh to avoid clipping by mobile UI bars
while older browsers fall back to 100vh).
tests/e2e/test-web-ui-url-routing.js (1)

205-205: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Use a mutable loop counter in the for loop.

Line 205 declares i as const and then increments it, which throws at runtime and breaks this E2E test.

Proposed fix
-    for (const i = 0; i < htmlResponses.length; i++) {
+    for (let i = 0; i < htmlResponses.length; i++) {
#!/bin/bash
# Verify there is no incrementing for-loop with a const counter in this file.
# Expected result: no output.
rg -nP 'for\s*\(\s*const\s+\w+\s*=\s*[^;]+;\s*[^;]+;\s*\w+\+\+\s*\)' tests/e2e/test-web-ui-url-routing.js
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-web-ui-url-routing.js` at line 205, The for-loop declares the
loop counter as const which prevents incrementing; change the loop counter
declaration from const to let in the for statement (the loop starting "for
(const i = 0; i < htmlResponses.length; i++) {") so i is mutable, and scan the
file for any other incrementing for-loops that mistakenly use const.
web-ui/partials/index/panel-market.html (1)

97-99: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Import icon button still has no accessible name.

Line 97 remains an icon-only control without an accessible label.

Proposed fix
-<button type="button" class="skills-flow-add" `@click`="importSingleSkill(skill)" :disabled="loading || !!initError || skillsMarketBusy">
+<button
+    type="button"
+    class="skills-flow-add"
+    `@click`="importSingleSkill(skill)"
+    :disabled="loading || !!initError || skillsMarketBusy"
+    :aria-label="`${t('market.action.importSingle')}: ${skill.displayName || skill.name}`"
+    :title="`${t('market.action.importSingle')}: ${skill.displayName || skill.name}`">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-market.html` around lines 97 - 99, The icon-only
import button lacks an accessible name; update the button with a clear
accessible label (e.g., add aria-label="Import skill" or include visually-hidden
text) so screen readers can describe its action; modify the element with class
"skills-flow-add" and handler importSingleSkill(skill) to include the aria
attribute or hidden label while keeping the existing :disabled binding (loading,
initError, skillsMarketBusy) intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/e2e/test-web-ui-url-routing.js`:
- Around line 92-93: The test's content-type assertion is too strict (only
matches application/javascript); update the assertion that checks
appJs.headers['content-type'] so it accepts both application/javascript and
text/javascript (and still handles optional charset), e.g. change the regex used
with String(appJs.headers['content-type'] || '') to allow either "application"
or "text" before "/javascript" so the test won't falsely fail when responses use
text/javascript.

In `@tests/unit/test-web-ui-url-normalization.mjs`:
- Around line 222-223: Currently runTests() is invoked unconditionally on
import; change this to only run when the module is executed directly by adding
an ESM guard using import.meta (e.g. compare process.argv[1] to
fileURLToPath(import.meta.url) from 'url'), call await runTests() inside that
guard, and set process.exitCode = 1 when runTests() returns a falsy/failed
result so CLI failures propagate a non-zero exit code; reference the runTests()
invocation and update surrounding top-level invocation to be conditional.

In `@web-ui/partials/index/panel-config-claude.html`:
- Line 103: The disabled local bridge card lacks ARIA disabled semantics for
screen readers; update the card element (the div with role="button" that uses
isClaudeLocalBridgeDisabled(), applyClaudeLocalBridge(), and
currentClaudeConfig) to include an :aria-disabled binding that reflects
isClaudeLocalBridgeDisabled() (e.g., aria-disabled="true" when disabled and
"false" otherwise) while keeping the existing tabindex and click/keydown guards
so assistive tech announces the control as disabled.

In `@web-ui/partials/index/panel-config-codex.html`:
- Line 123: The card acting as a button should expose its disabled state to
assistive tech: update the element rendered for displayProvidersList (the card
that uses displayCurrentProvider, isLocalProviderDisabled and switchProvider) to
add aria-disabled="true" when (provider.name === 'local' &&
isLocalProviderDisabled) and aria-disabled="false" or omit when enabled; keep
the current tabindex logic (set to -1 when disabled) and retain the
role="button" and existing click/keydown guards so the UI remains
non-interactive when disabled.

In `@web-ui/partials/index/panel-market.html`:
- Around line 44-47: The icon-only action button that calls refreshSkillsList({
silent: false }) (and the other icon-only button around lines 71-74) lacks an
accessible name; add an explicit accessible label by adding aria-label and title
attributes (e.g. aria-label="Refresh skills list", title="Refresh skills list")
or a localized equivalent (use your i18n helper like $t(...) if available), or
include a visually-hidden <span> with the action text inside the button so
screen readers can announce it; ensure the attributes/text are present on the
<button> elements with `@click`="refreshSkillsList({ silent: false })" and the
other icon-only button(s).

---

Outside diff comments:
In `@cli.js`:
- Around line 11393-11418: The handler that checks requestPath and computes
normalized/filePath/relativePath currently returns 404 for legacy entry URLs
like '/web-ui', '/web-ui/', '/web-ui/index.html', and nested variants (e.g.,
'/web-ui/web-ui/index.html'); update the conditional that now tests if
relativePath === '' || relativePath === 'index.html' to instead treat those
cases as redirects back to '/' preserving the original query: use
requestPath/rawUrl/query to send a 302 Location of `/${query}` for requestPath
'/web-ui' or '/web-ui/' and when relativePath is '' or 'index.html' or
startsWith('web-ui/index.html') (or otherwise indicates a legacy entry), rather
than returning 404; keep the security check using isPathInside(filePath, webDir)
and other logic intact.

---

Duplicate comments:
In `@tests/e2e/test-web-ui-url-routing.js`:
- Line 205: The for-loop declares the loop counter as const which prevents
incrementing; change the loop counter declaration from const to let in the for
statement (the loop starting "for (const i = 0; i < htmlResponses.length; i++)
{") so i is mutable, and scan the file for any other incrementing for-loops that
mistakenly use const.

In `@tests/unit/config-tabs-ui.test.mjs`:
- Line 68: The test's regex allows matching across multiple elements and can
false-pass; update the assertion that inspects the html variable (the
assert.match call) to require a single opening <button> start-tag by using a
regex with a <button\b start, negative/positive lookaheads that check for
v-if="taskOrchestrationTabEnabled", class="top-tab", and id="tab-orchestration"
inside the same tag, and then match until the first closing '>' so only one
start-tag is matched.

In `@web-ui/app.js`:
- Around line 425-444: The URL search/hash cleanup runs too early and can strip
share-link/session params before restoreSessionFilterCache() consumes them;
leave the initial /web-ui -> / redirect logic (location.replace) intact but
remove or defer the second cleanup block that calls
window.history.replaceState('', ...) and clears url.search/url.hash, and instead
perform that cleanup only after restoreSessionFilterCache() has completed (i.e.,
move the cleanup to execute after the restoreSessionFilterCache() invocation).
Ensure you reference the pathname-check/redirect block and the cleanup currently
using window.history.replaceState and restoreSessionFilterCache() so reviewers
can locate and adjust the code.

In `@web-ui/modules/app.computed.session.mjs`:
- Around line 717-726: The previous-period aggregation is iterating raw
this.sessionsUsageList while the current value uses the filtered dataset
sessionUsageCharts.summary, causing incorrect deltas; change the loop(s) that
compute prevTokens (and the similar block at 754-763) to iterate over the same
filtered array (sessionUsageCharts.summary or the shared filtered variable used
for current calculations) and keep the same validation (typeof session check,
Date.parse on session.updatedAt, Number(session.totalTokens) handling) so both
current and previous hero values are computed from the identical data source.
- Around line 608-610: The computed is still producing prev/delta values when
the selected range is "all"; update the prevKey calculation and the other
delta/prev field generation to return empty/disabled values whenever range ===
'all' (or equivalent sentinel) instead of computing with rangeDays/dayMs.
Concretely, wrap the existing prevKey logic (references: prevKey, rangeDays,
dayMs) with a guard like if (range === 'all') return '' (or set delta-related
outputs to null/''), and apply the same gate to the other occurrence noted (the
block producing the delta at the second location) so compare text cannot render
for the "all" range.

In `@web-ui/modules/app.methods.navigation.mjs`:
- Around line 69-70: The current canonicalization replace call on the pathname
variable (inside the do/while loop) only removes duplicated "/web-ui/web-ui/"
when a trailing slash exists; update the regex used in the pathname.replace(...)
so it also matches the case without a trailing slash (e.g., "/web-ui/web-ui") by
making the trailing slash optional or by matching one or more consecutive
"/web-ui" segments, and ensure the replacement normalizes to a single "/web-ui/"
(maintaining the existing loop and comparison to previousPathname).

In `@web-ui/modules/skills.methods.mjs`:
- Around line 492-495: The importSingleSkill function currently calls
api('import-skills', { targetApp: this.skillsTargetApp, imports: [skill] })
using the wrong payload key; change the payload to match the existing
import-skills contract by sending items: [skill] (and preserve targetApp) so the
backend receives the same shape as the bulk import flow. Locate
importSingleSkill and the api('import-skills', ...) invocation and replace the
imports property with items to align with the existing contract.

In `@web-ui/partials/index/panel-market.html`:
- Around line 97-99: The icon-only import button lacks an accessible name;
update the button with a clear accessible label (e.g., add aria-label="Import
skill" or include visually-hidden text) so screen readers can describe its
action; modify the element with class "skills-flow-add" and handler
importSingleSkill(skill) to include the aria attribute or hidden label while
keeping the existing :disabled binding (loading, initError, skillsMarketBusy)
intact.

In `@web-ui/partials/index/panel-usage.html`:
- Around line 58-60: The template currently uses hardcoded labels ("tokens",
"API", "total", "msgs") in spans like usageCurrentSessionStats.tokenLabel,
usageCurrentSessionStats.apiDurationLabel and
usageCurrentSessionStats.totalDurationLabel; replace those literals with i18n
keys by calling the project's translation helper (e.g., $t or i18n filter) in
the template and update the locale JSON/YAML to add keys (e.g., usage.tokens,
usage.api, usage.total, usage.msgs); ensure you change the occurrences in this
block and the other reported locations (lines around 106-109, 147, 162) so all
UI text is localized and no hardcoded strings remain.
- Around line 95-100: Replace the non-interactive span/div click targets in the
Usage panel with semantic <button type="button"> elements (or add full keyboard
semantics) so they are keyboard-operable; specifically change the v-for span
that calls selectSessionsUsageDay(label.key) and uses sessionsUsageSelectedDay
to a button preserving :key, class="usage-wave-label", :class binding, and the
`@click` handler, and do the same for the other click targets referenced (the
blocks around lines 143–149 and 158–164) ensuring any visual classes are kept,
tabindex/role removed, and aria-pressed or aria-current is added where
appropriate to reflect selection for accessibility.

In `@web-ui/styles/responsive.css`:
- Around line 92-93: Update the CSS declarations that set the shell height (the
height and min-height properties) to include dynamic viewport units for mobile:
keep the existing 100vh values but add 100dvh as a fallback/alternative (i.e.,
specify both units so browsers that support dynamic viewport height use 100dvh
to avoid clipping by mobile UI bars while older browsers fall back to 100vh).

In `@web-ui/styles/sessions-usage.css`:
- Around line 303-305: The CSS rule targets .usage-card-hourly-heatmap but the
template uses a different class name, so overflow-x: auto never applies; locate
the actual class used in the template (e.g., the element rendering the hourly
heatmap) and either rename the template class to usage-card-hourly-heatmap or
update the selector in web-ui/styles/sessions-usage.css (replace
.usage-card-hourly-heatmap) to exactly match the template's class (or add the
template class variant alongside .usage-card-hourly-heatmap) so the overflow-x:
auto rule is applied to the correct element.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f74a2fe7-4c8a-4063-a91e-abf7e11daefe

📥 Commits

Reviewing files that changed from the base of the PR and between aa3434d and aa88b1f.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (38)
  • cli.js
  • package.json
  • tests/e2e/test-web-ui-assets.js
  • tests/e2e/test-web-ui-session-tab.js
  • tests/e2e/test-web-ui-url-routing.js
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/run.mjs
  • tests/unit/test-web-ui-url-normalization.mjs
  • tests/unit/web-run-host.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • tests/unit/web-ui-url-canonicalization.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.sessions.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/sessions-filters-url.mjs
  • web-ui/modules/skills.methods.mjs
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/partials/index/panel-usage.html
  • web-ui/res/vue.runtime.global.prod.js
  • web-ui/source-bundle.cjs
  • web-ui/styles/base-theme.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-usage.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css
📜 Review details
🧰 Additional context used
🪛 HTMLHint (1.9.2)
web-ui/partials/index/panel-config-claude.html

[error] 109-109: Special characters must be escaped : [ > ].

(spec-char-escape)

🔇 Additional comments (23)
tests/unit/web-ui-source-bundle.test.mjs (1)

26-27: LGTM!

package.json (1)

3-3: LGTM!

Also applies to: 49-49

web-ui/partials/index/panel-settings.html (1)

22-23: LGTM!

Also applies to: 32-33

web-ui/styles/navigation-panels.css (1)

41-45: LGTM!

Also applies to: 51-52

web-ui/partials/index/layout-header.html (1)

3-94: LGTM!

Also applies to: 482-487

web-ui/index.html (1)

9-9: ⚡ Quick win

Vue runtime-only switch is safe here (precompiled render is used)

  • web-ui/app.js assigns appOptions.render = window.__CODEXMATE_WEB_UI_RENDER__ (when present) before createApp(...).mount('#app'), so the UI doesn’t rely on the Vue template compiler.
  • The served /web-ui/app.js is expected to include window.__CODEXMATE_WEB_UI_RENDER__ (tests/e2e/test-web-ui-assets.js), aligning with the runtime-only build.
web-ui/modules/app.methods.claude-config.mjs (1)

269-271: LGTM!

tests/unit/compact-layout-ui.test.mjs (1)

42-48: LGTM!

Also applies to: 78-78

tests/unit/run.mjs (1)

16-16: LGTM!

web-ui/styles/titles-cards.css (1)

157-169: LGTM!

web-ui/modules/app.methods.session-trash.mjs (1)

221-249: LGTM!

web-ui/modules/app.methods.session-browser.mjs (1)

267-273: LGTM!

tests/e2e/test-web-ui-assets.js (1)

47-59: LGTM!

Also applies to: 68-69

web-ui/styles/base-theme.css (1)

29-37: LGTM!

tests/unit/web-ui-behavior-parity.test.mjs (1)

518-518: LGTM!

Also applies to: 615-619, 705-710, 726-727

web-ui/modules/app.computed.dashboard.mjs (1)

95-97: LGTM!

tests/unit/web-run-host.test.mjs (1)

720-720: LGTM!

Also applies to: 732-732, 746-758, 774-791

web-ui/source-bundle.cjs (1)

10-13: LGTM!

Also applies to: 65-112, 267-267, 279-280

web-ui/logic.sessions.mjs (1)

552-553: LGTM!

Also applies to: 559-560

tests/e2e/test-web-ui-session-tab.js (1)

22-40: LGTM!

Also applies to: 111-114, 141-144, 176-176

tests/unit/web-ui-url-canonicalization.test.mjs (1)

1-83: LGTM!

web-ui/styles/skills-market.css (1)

1-293: LGTM!

cli.js (1)

10061-10065: LGTM!

Also applies to: 10621-10625, 10652-10663, 11425-11428, 11455-11458, 11519-11522

Comment on lines +92 to +93
/^application\/javascript\b/.test(String(appJs.headers['content-type'] || '')),
'/web-ui/app.js should return javascript content type'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Loosen JavaScript content-type assertion to avoid false negatives.

Line 92 only accepts application/javascript, but valid responses may use text/javascript.

Proposed fix
-        /^application\/javascript\b/.test(String(appJs.headers['content-type'] || '')),
+        /^(application|text)\/javascript\b/.test(String(appJs.headers['content-type'] || '')),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/^application\/javascript\b/.test(String(appJs.headers['content-type'] || '')),
'/web-ui/app.js should return javascript content type'
/^(application|text)\/javascript\b/.test(String(appJs.headers['content-type'] || '')),
'/web-ui/app.js should return javascript content type'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-web-ui-url-routing.js` around lines 92 - 93, The test's
content-type assertion is too strict (only matches application/javascript);
update the assertion that checks appJs.headers['content-type'] so it accepts
both application/javascript and text/javascript (and still handles optional
charset), e.g. change the regex used with String(appJs.headers['content-type']
|| '') to allow either "application" or "text" before "/javascript" so the test
won't falsely fail when responses use text/javascript.

Comment on lines +222 to +223
runTests();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard direct execution and propagate failure exit code.

Line 222 always runs tests on import, and failures only return false, so direct CLI execution can still exit with code 0. Gate execution to direct runs and set process.exitCode on failure.

Proposed fix
+import { fileURLToPath } from 'url';
+
 ...
-runTests();
+if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
+    const ok = runTests();
+    if (!ok) process.exitCode = 1;
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test-web-ui-url-normalization.mjs` around lines 222 - 223,
Currently runTests() is invoked unconditionally on import; change this to only
run when the module is executed directly by adding an ESM guard using
import.meta (e.g. compare process.argv[1] to fileURLToPath(import.meta.url) from
'url'), call await runTests() inside that guard, and set process.exitCode = 1
when runTests() returns a falsy/failed result so CLI failures propagate a
non-zero exit code; reference the runTests() invocation and update surrounding
top-level invocation to be conditional.


<div class="card-list">
<div :class="['card', { active: currentClaudeConfig === 'claude-local' }]" @click="applyClaudeLocalBridge()" @keydown.enter.self.prevent="applyClaudeLocalBridge()" @keydown.space.self.prevent="applyClaudeLocalBridge()" tabindex="0" role="button" :aria-current="currentClaudeConfig === 'claude-local' ? 'true' : null">
<div :class="['card', { active: currentClaudeConfig === 'claude-local', disabled: isClaudeLocalBridgeDisabled() }]" @click="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" @keydown.enter.self.prevent="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" @keydown.space.self.prevent="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" :tabindex="isClaudeLocalBridgeDisabled() ? -1 : 0" role="button" :aria-current="currentClaudeConfig === 'claude-local' ? 'true' : null">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add aria-disabled on disabled local bridge card.

Line 103 blocks activation and removes focus, but the control still lacks disabled semantics for screen readers.

Suggested patch
-<div :class="['card', { active: currentClaudeConfig === 'claude-local', disabled: isClaudeLocalBridgeDisabled() }]" `@click`="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" `@keydown.enter.self.prevent`="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" `@keydown.space.self.prevent`="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" :tabindex="isClaudeLocalBridgeDisabled() ? -1 : 0" role="button" :aria-current="currentClaudeConfig === 'claude-local' ? 'true' : null">
+<div :class="['card', { active: currentClaudeConfig === 'claude-local', disabled: isClaudeLocalBridgeDisabled() }]" `@click`="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" `@keydown.enter.self.prevent`="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" `@keydown.space.self.prevent`="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" :tabindex="isClaudeLocalBridgeDisabled() ? -1 : 0" role="button" :aria-disabled="isClaudeLocalBridgeDisabled() ? 'true' : null" :aria-current="currentClaudeConfig === 'claude-local' ? 'true' : null">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-config-claude.html` at line 103, The disabled
local bridge card lacks ARIA disabled semantics for screen readers; update the
card element (the div with role="button" that uses
isClaudeLocalBridgeDisabled(), applyClaudeLocalBridge(), and
currentClaudeConfig) to include an :aria-disabled binding that reflects
isClaudeLocalBridgeDisabled() (e.g., aria-disabled="true" when disabled and
"false" otherwise) while keeping the existing tabindex and click/keydown guards
so assistive tech announces the control as disabled.


<div v-if="!loading && !initError" class="card-list">
<div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name }]" @click="switchProvider(provider.name)" @keydown.enter.self.prevent="switchProvider(provider.name)" @keydown.space.self.prevent="switchProvider(provider.name)" tabindex="0" role="button" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
<div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name, disabled: provider.name === 'local' && isLocalProviderDisabled }]" @click="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.enter.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.space.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" :tabindex="provider.name === 'local' && isLocalProviderDisabled ? -1 : 0" role="button" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Expose disabled card state with aria-disabled.

Line 123 disables interaction/focus, but the button-like card does not announce disabled semantics to assistive tech.

Suggested patch
-<div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name, disabled: provider.name === 'local' && isLocalProviderDisabled }]" `@click`="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" `@keydown.enter.self.prevent`="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" `@keydown.space.self.prevent`="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" :tabindex="provider.name === 'local' && isLocalProviderDisabled ? -1 : 0" role="button" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
+<div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name, disabled: provider.name === 'local' && isLocalProviderDisabled }]" `@click`="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" `@keydown.enter.self.prevent`="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" `@keydown.space.self.prevent`="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" :tabindex="provider.name === 'local' && isLocalProviderDisabled ? -1 : 0" role="button" :aria-disabled="provider.name === 'local' && isLocalProviderDisabled ? 'true' : null" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name, disabled: provider.name === 'local' && isLocalProviderDisabled }]" @click="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.enter.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.space.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" :tabindex="provider.name === 'local' && isLocalProviderDisabled ? -1 : 0" role="button" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
<div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name, disabled: provider.name === 'local' && isLocalProviderDisabled }]" `@click`="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" `@keydown.enter.self.prevent`="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" `@keydown.space.self.prevent`="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" :tabindex="provider.name === 'local' && isLocalProviderDisabled ? -1 : 0" role="button" :aria-disabled="provider.name === 'local' && isLocalProviderDisabled ? 'true' : null" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-config-codex.html` at line 123, The card acting
as a button should expose its disabled state to assistive tech: update the
element rendered for displayProvidersList (the card that uses
displayCurrentProvider, isLocalProviderDisabled and switchProvider) to add
aria-disabled="true" when (provider.name === 'local' && isLocalProviderDisabled)
and aria-disabled="false" or omit when enabled; keep the current tabindex logic
(set to -1 when disabled) and retain the role="button" and existing
click/keydown guards so the UI remains non-interactive when disabled.

Comment on lines +44 to +47
<button type="button" class="btn-mini" @click="refreshSkillsList({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
<svg v-if="!skillsLoading" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14"><path d="M17 1l4 4-4 4"/><path d="M3 11V9a4 4 0 014-4h14"/><path d="M3 19l-4-4 4-4"/><path d="M17 9v2a4 4 0 01-4 4H3"/></svg>
<svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14" class="spin"><path d="M10 2v4m0 8v4M2 10h4m8 0h4m-3.5-6.5l2.5 2.5M4 4l2.5 2.5M16 16l-2.5-2.5M4 16l2.5-2.5"/></svg>
</button>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add accessible names to icon-only action buttons.

Line 44 and Line 71 use icon-only buttons without accessible names, so assistive tech cannot announce their actions.

Proposed fix
-<button type="button" class="btn-mini" `@click`="refreshSkillsList({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
+<button
+    type="button"
+    class="btn-mini"
+    `@click`="refreshSkillsList({ silent: false })"
+    :disabled="loading || !!initError || skillsMarketBusy"
+    aria-label="Refresh installed skills"
+    title="Refresh installed skills">
 ...
-<button type="button" class="btn-mini" `@click`="scanImportableSkills({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
+<button
+    type="button"
+    class="btn-mini"
+    `@click`="scanImportableSkills({ silent: false })"
+    :disabled="loading || !!initError || skillsMarketBusy"
+    aria-label="Scan importable skills"
+    title="Scan importable skills">

Also applies to: 71-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-market.html` around lines 44 - 47, The icon-only
action button that calls refreshSkillsList({ silent: false }) (and the other
icon-only button around lines 71-74) lacks an accessible name; add an explicit
accessible label by adding aria-label and title attributes (e.g.
aria-label="Refresh skills list", title="Refresh skills list") or a localized
equivalent (use your i18n helper like $t(...) if available), or include a
visually-hidden <span> with the action text inside the button so screen readers
can announce it; ensure the attributes/text are present on the <button> elements
with `@click`="refreshSkillsList({ silent: false })" and the other icon-only
button(s).

@awsl233777
Copy link
Copy Markdown
Collaborator

Follow-up fix pushed in 8d5cc34 for the Termux/runtime regression:

  • Removed the runtime dependency on @vue/compiler-dom when serving /web-ui/app.js.
  • Added a checked-in precompiled Web UI render script at web-ui/res/web-ui-render.precompiled.js.
  • web-ui/source-bundle.cjs now prefers the checked-in precompiled render and only falls back to local compilation if that file is missing.
  • Added a unit regression that monkey-patches require('@vue/compiler-dom') to fail and verifies the executable Web UI bundle still includes window.__CODEXMATE_WEB_UI_RENDER__.

Validation performed after this fix:

  • npm run test:unit → 529 tests passed
  • npm run test:e2e → passed
  • git diff --check → passed
  • HTTP smoke test against local server:
    • /web-ui/app.js → 200, includes window.__CODEXMATE_WEB_UI_RENDER__, no @vue/compiler-dom is required error
    • /web-ui/index.html?tab=sessions → 302 /?tab=sessions, Cache-Control: no-store, max-age=0
  • Chrome/CDP URL guard probe still passes after this change.

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 24, 2026

@awsl233777 感谢说明!commit 8d5cc34 的修复思路很清晰:

  • 预编译 render 脚本web-ui/res/web-ui-render.precompiled.js)随代码一起提交,彻底消除了在 Termux/受限环境中对 @vue/compiler-dom 的运行时依赖——这是比之前动态编译更稳健的方案。
  • 按需降级(precompiled 不存在时才本地编译)保留了开发侧的灵活性。
  • monkey-patch 单元回归覆盖了 @vue/compiler-dom 不可用的场景,防止未来引入退化。

529 unit + E2E 全通过 + URL guard probe 继续生效,整体看起来很扎实。我现在开始对最新提交进行全面审查。

(=^・ω・^=)🐇

✅ Actions performed

Full review triggered.

@awsl233777
Copy link
Copy Markdown
Collaborator

Follow-up fix pushed in 8251337 for the sessions tab visual refresh regression.

Root cause:

  • Leaving the Sessions tab suspended/finalized the sessions render state.
  • Returning to the Sessions browser tab then ran prepareSessionTabRender(), which reset list/preview render counters even though session data was already loaded.
  • That did not necessarily issue a new list-sessions request, but it made the UI visibly re-render/flash like a refresh.

Fix:

  • The Web UI now preserves rendered session list/preview state across normal tab navigation.
  • Returning to the Sessions browser tab skips prepareSessionTabRender() when list and preview rendering are already enabled.
  • Explicit refresh/filter changes still call loadSessions() as before.

Validation performed:

  • npm run test:unit → 530 tests passed
  • npm run test:e2e → passed
  • git diff --check → passed
  • Chrome/CDP mobile probe for Sessions → Usage → Sessions:
    • before fix: observed suspendSessionTabRender, finalizeSessionTabTeardown, resetSessionListRender, resetSessionPreviewMessageRender, prepareSessionTabRender
    • after fix: no loadSessions, no prepareSessionTabRender, no list/preview reset on returning to Sessions
    • state stayed stable: listEnabled=true, previewEnabled=true, listCount=20, previewCount=2
  • Existing URL canonicalization and /web-ui/app.js runtime compiler smoke checks still pass.

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 24, 2026

@awsl233777 感谢详细的说明与 CDP 追踪对比!commit 8251337 的修复思路很清晰:

  • 根因定位准确prepareSessionTabRender() 在 list/preview 渲染状态已就绪时仍被执行,触发了不必要的计数器重置和 UI 闪烁。
  • 防御性守卫:在 list 和 preview 渲染已启用时跳过 prepareSessionTabRender(),既解决了视觉抖动,又不影响显式刷新/过滤器变更的正常路径。
  • CDP 前后对比清楚地验证了修复效果——再次进入 Sessions tab 时没有 loadSessions、没有 render 重置,状态保持稳定(listEnabled=truelistCount=20 等)。

530 个单元测试全通过 + E2E 通过,整体看起来很扎实。我现在开始对最新提交进行全面审查。

(=^・ω・^=)🐇

✅ Actions performed

Full review triggered.

@ymkiux ymkiux merged commit bead873 into main May 24, 2026
8 of 9 checks passed
@ymkiux ymkiux deleted the chore/revert-version-34 branch May 24, 2026 05:55
ymkiux added a commit that referenced this pull request May 24, 2026
… bump (#170)

* chore: revert version to 0.0.34 after npm unpublish

* refactor(web-ui): redesign Usage tab with "River of Time" aesthetic

- Merge current session bar with hero metrics for unified focus
- Replace bar chart with SVG bezier curve wave visualization
- Add automatic delta calculation (7d vs prior 7d, 30d vs prior 30d)
- Redesign empty states with animated SVG illustrations
- Simplify list views with bullet-point compact style
- Enhance color system with delta indicators and heatmap levels

* fix(web-ui): resolve /web-ui/ 404 and duplicate path issues

- Server: handle /web-ui/ requests by returning index.html
- Server: explicitly serve index.html for /web-ui/index.html requests
- Client: normalize URL on mount to fix /web-ui/web-ui/ duplicates
- Client: redirect /web-ui/ and /web-ui/index.html to /web-ui

Fixes the issue where refreshing in sessions tab caused
URL to become /web-ui/web-ui/index.html

* fix(web-ui): strengthen URL normalization to handle nested duplicates

- Use do-while loop to iteratively remove all /web-ui/web-ui/ patterns
- Handle edge cases like /web-ui/web-ui/web-ui/index.html
- Add additional check for /web-ui/web-ui/ prefix pattern

* test(web-ui): add URL routing and normalization tests

- Add test-web-ui-url-routing.js E2E test for server routing
- Add test-web-ui-url-normalization.mjs unit test (24 cases)
- Update test-web-ui-assets.js expectations for /web-ui/ fix
- Fix shouldReplace check logic in app.js (use originalPathname)

* refactor(web-ui): remove /web-ui route, use / as single entry point

- Server: /web-ui, /web-ui/, /web-ui/index.html now return 404
- Root path / is the only HTML entry point
- Client: auto-redirect from /web-ui/* to / with hash/params preserved
- Assets still served at /web-ui/app.js, /web-ui/styles.css, etc.
- Simplified routing logic, removed redundant path handling

* feat(web): hide local provider in codex/claude config mode

- filter out 'local' provider from displayProvidersList when configMode is codex/claude
- prevents users from selecting local provider in these apps due to existing issues
- other modes (terminal) remain unaffected

* feat(web): disable local provider in codex/claude config mode

- local provider card shows as disabled (50% opacity, non-clickable) in codex mode
- claude-local bridge card shows as disabled in claude mode
- adds CSS .card.disabled style with pointer-events: none
- cards remain visible but cannot be selected

* fix(web): correct disabled provider click logic

- fix operator precedence issue in click handlers
- use proper ternary expressions with correct grouping
- prevents white screen error on config load

* fix(web): remove parentheses from computed property

- isLocalProviderDisabled is a computed returning boolean, not a function
- should be accessed as property without () in template
- fixes white screen error on config load

* feat(web): rename claude-local to local with system badge

- display name changed from 'claude-local' to 'local'
- adds system badge like codex provider readonly indicator
- internal id 'claude-local' remains unchanged for compatibility

* feat(web): remove local provider subtitle hint

- removes '勾选参与负载均衡的提供商' subtitle
- keeps card layout cleaner with just title and system badge

* refactor(web): redesign skills tab to minimalist single-column layout

- Remove cluttered overview section with 6 stat cards
- Consolidate header with inline target switcher
- Single-column flow layout for better mobile experience
- Add count badges directly in panel headers
- Add quick action buttons in import panel
- Add individual import button for each skill
- Remove help panel (content available elsewhere)
- Remove root path display (moved to tooltip)
- Preserve all functionality while reducing visual noise

Design principles:
- Progressive disclosure: scroll to discover
- Large touch targets: 56px min height
- Visual hierarchy: clear content grouping
- Cleaner information architecture

* refactor(web): implement static URL, always stay at /

- Remove all URL routing, URL stays as pure '/'
- Delete query parameters and hash from browser address bar
- Remove history.pushState/replaceState operations for tab switching
- Remove session filter URL synchronization
- Share links still work (generate URL with params for clipboard)
- When opening share link, params are applied then URL cleaned
- All state managed via memory + localStorage

Changes:
- app.js: Clean URL on mount, remove resource path fix logic
- app.methods.navigation.mjs: Remove URL operations in switchMainTab
- sessions-filters-url.mjs: Empty syncSessionsFilterUrl, fix share URL base
- app.methods.session-browser.mjs: Clean URL after applying URL state

* chore: bump version to 0.0.35

* fix(ci): restore missing CI scripts in package.json

* test: update unit tests to match new web-ui structure

- Fix sessionUsageDaily -> sessionUsageHourlyHeatmap assertion
- Remove obsolete loadSkillsMarketOverview and market-action-grid checks
- Update skills-target-chip selector (was market-target-chip)
- Fix provider card disabled state and tabindex assertions
- Remove market.pill importableDirect/importMissing checks

* test: add new keys to parity baseline allowlists

- isLocalProviderDisabled (data key)
- sessionUsageWave, sessionsUsageSelectedDay, usageHeroDelta, usageHeroDeltaClass (computed keys)

* test: add more usage hero keys to parity baseline

- usageHeroMainValue
- usageHeroSubLabel

* test: fix isLocalProviderDisabled key type

Move isLocalProviderDisabled from data keys to computed keys
as it is defined as a computed property in app.computed.dashboard.mjs

* fix(web-ui): canonicalize session tab navigation URL

* fix: stabilize web ui navigation and canonical urls

* fix: avoid runtime vue compiler dependency

* fix: preserve sessions tab render on navigation

---------

Co-authored-by: ymkiux <ymkiux@users.noreply.github.com>
Co-authored-by: awsl233777 <awsl233777@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants