Skip to content

refactor: split web ui modules and harden provider switching#68

Merged
ymkiux merged 22 commits intoSakuraByteCore:mainfrom
SurviveM:refactor/web-ui-modularize-provider-switch
Apr 5, 2026
Merged

refactor: split web ui modules and harden provider switching#68
ymkiux merged 22 commits intoSakuraByteCore:mainfrom
SurviveM:refactor/web-ui-modularize-provider-switch

Conversation

@SurviveM
Copy link
Copy Markdown
Collaborator

@SurviveM SurviveM commented Apr 4, 2026

Summary

  • split the web ui app, partials, and styles into focused modules with smaller edit surfaces
  • add source bundle helpers and parity tests to keep bundled assets aligned with legacy behavior
  • expand unit and e2e coverage for web ui asset delivery and provider switching regressions
  • preserve provider switching UX by avoiding loading flicker and reflecting the requested target immediately

Tests

  • npm test
  • node tests/unit/run.mjs
  • node tests/e2e/run.js
  • node --check cli.js
  • git diff --check

Summary by CodeRabbit

  • New Features

    • Fully bundled Web UI served from the app: AGENTS editor with diff/preview/apply, Claude multi-profile config, OpenClaw editor/persist, Skills manager/market, sessions browser/timeline/standalone/trash, install FAB, backups/imports, speed tests, new panels/modals, and richer API helpers.
  • Bug Fixes

    • Hardened asset routing and MIME handling; stricter file-change watching, missing-directory recovery, and safer restart semantics (403/404/500 behaviors).
  • Refactor

    • Web UI logic modularized and bundled asset delivery centralized.
  • Tests

    • Large expansion of unit and E2E coverage across UI, bundling, routing, watcher/restart, and runtime flows.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 4, 2026

@coderabbitai re-review
Stop making breaking changes, do a proper review!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

Warning

Rate limit exceeded

@github-actions[bot] has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 44 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 6 minutes and 44 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e8420c33-7644-44dc-8f12-86d43abb4264

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and 2347988.

📒 Files selected for processing (89)
  • .github/workflows/ci.yml
  • .github/workflows/coderabbit-review.yml
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/agents-modal-guards.test.mjs
  • tests/unit/api-module.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/coderabbit-workflows.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/install-methods.test.mjs
  • tests/unit/openclaw-core.test.mjs
  • tests/unit/openclaw-persist-regression.test.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-actions-standalone.test.mjs
  • tests/unit/session-browser-timeline-regression.test.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/startup-claude-star-prompt.test.mjs
  • tests/unit/web-run-host.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-logic.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-runtime-navigation-regression.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css
📝 Walkthrough

Walkthrough

Serve bundled Web UI assets from an in-repo source bundle; split monolithic web-ui logic into focused modules; add many Vue partials, CSS sheets, computed/method factories and OpenClaw/agents features; refactor CLI watcher/restart to a Map-backed registry with directory-tree sync; add extensive unit and E2E tests and helpers.

Changes

Cohort / File(s) Summary
CLI & server
cli.js
Add bundled Web UI readers; refactor restart watcher to a Map-backed registry with normalized paths, stricter filtering, recursive directory sync, missing-directory parent watch and explicit watcher teardown; switch /web-ui routing to allowlisted bundled asset reads with explicit MIME and centralized 500 error handling.
Source bundle
web-ui/source-bundle.cjs
New bundler that normalizes UTF‑8/BOM/CRLF, inlines HTML @include, inlines relative CSS @import, collects JS dependencies deterministically, and provides executable-bundle transforms and readers.
Logic split
web-ui/logic.mjs, web-ui/logic.*.mjs
Split monolithic logic into submodules (agents-diff, claude, runtime, sessions) and re-export via web-ui/logic.mjs.
App methods & computed
web-ui/modules/...
Add many factory modules for methods/computed (providers, codex/claude/openclaw, sessions browser/timeline/trash, agents, install, runtime, startup) and aggregator entrypoints (app.methods.index, app.computed.index).
API & constants
web-ui/modules/api.mjs, web-ui/modules/app.constants.mjs
Add browser-aware API_BASE and robust api/apiWithMeta with JSON/content-type handling and 413 payload semantics; add session-trash and default model/OpenClaw constants.
Agents & OpenClaw features
web-ui/logic.agents-diff.mjs, web-ui/modules/app.methods.agents.mjs, web-ui/modules/app.methods.openclaw-*.mjs
Add line-diff builder with LCS + large-diff heuristic, preview request sizing/fallback, AGENTS.md modal editor with guarded preview/save flows, and comprehensive OpenClaw parse/edit/persist toolset.
Templates / Partials
web-ui/partials/index/*
Add many Vue partials: header/footer, status inspector, panels (config/codex/claude/openclaw/market/sessions/settings) and multiple modal partials (agents, openclaw, skills, install, confirm/toast).
Styles
web-ui/styles/*
Add comprehensive CSS suite (base-theme, layout-shell, responsive, modals, navigation, sessions, skills/market, OpenClaw, titles/cards, controls/forms, feedback, etc.).
Tests & helpers
tests/unit/helpers/*, tests/unit/*.test.mjs, tests/e2e/*, .github/workflows/*
Add bundled-asset test readers, app-options capture harness, many new/updated unit and E2E tests (watcher, bundling, parity, providers, sessions, OpenClaw, agents, install, api-module); CI updated to fetch full history and set WEB_UI_PARITY_BASE_REF.
Watcher & restart tests
tests/unit/web-ui-restart.test.mjs, tests/unit/web-run-host.test.mjs
Add extensive tests covering Map-backed watcher registry behavior, recursive fallback, rename/rescan, missing-dir reattach, watcher error recovery, and web-server bundled-asset error responses.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Client (Browser)
  participant Server as CLI Server
  participant Bundle as SourceBundle (in-repo)
  participant Watcher as Filesystem Watcher

  Browser->>Server: GET / or GET /web-ui[/...]
  Server->>Server: route & allowlist check
  alt bundled asset allowed
    Server->>Bundle: readBundledWebUi*(path)
    Bundle-->>Server: content or throw
    alt success
      Server-->>Browser: 200 + content (explicit MIME)
    else failure
      Server->>Server: writeWebUiAssetError(log)
      Server-->>Browser: 500 Internal Server Error
    end
  else disallowed/traversal
    Server-->>Browser: 403 / 404
  end

  Note over Watcher,Server: Development watch & restart flow
  Watcher->>Server: fs events (change/rename/error)
  Server->>Server: normalize path, sync directory tree, update Map registry
  Server->>Server: debounce -> trigger restart and teardown watchers
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • SurviveM
  • awsl233777
  • ymkiux

"🐰
I hopped through bundles, stitched each part,
Split logic clean, and gave the watcher heart.
Templates, tests, and styles all in tow,
Little paws fixed paths — now servers glow.
Hop, build, release — the UI’s ready to show."

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

@github-actions[bot]: Understood — I apologize for repeatedly introducing breaking changes. I will redo the review now, strictly limiting feedback to correctness, safety, and quality within the existing design. No refactoring, no API/interface/behavior alterations will be suggested.

Redoing the full review strictly focused on correctness, safety, and quality — no breaking changes will be suggested.


✏️ Learnings added
Learnt from: github-actions[bot]
URL: https://github.com/SakuraByteCore/codexmate/pull/68

Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
✅ Actions performed

Full review triggered.

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (10)
web-ui/styles/feedback.css-108-108 (1)

108-108: ⚠️ Potential issue | 🟡 Minor

Trailing comment without a corresponding rule.

Line 108 contains only a comment (/* 内容区域包裹器 - 稳定高度 */) with no CSS rule following it. This appears to be either a placeholder for future styling or an incomplete edit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/feedback.css` at line 108, The file contains a standalone
comment "/* 内容区域包裹器 - 稳定高度 */" with no accompanying CSS rule; either remove this
trailing comment if it was left accidentally, or add the intended rule (e.g., a
selector like .content-wrapper or the appropriate container selector used in
your layout) and define the stable-height styles (height/min-height, overflow
rules, etc.) so the comment has a corresponding CSS block; locate the lone
comment text to apply the change.
web-ui/source-bundle.cjs-137-164 (1)

137-164: ⚠️ Potential issue | 🟡 Minor

Add documentation for relative import syntax constraint.

The function enforces that relative imports must use named import syntax { foo, bar } only—default imports or namespace imports will trigger build failures. While the codebase currently complies with this constraint, it should be documented (via code comments or contributor guidelines) to prevent contributors from attempting to use import foo from './bar' or import * as foo from './bar' syntax with relative paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/source-bundle.cjs` around lines 137 - 164, Document that relative
imports must use named-import syntax only by adding a concise comment above
buildRelativeImportAliasStatements (and optionally referencing this rule in
contributor docs) stating that default imports (e.g., import foo from './bar')
and namespace imports (import * as foo from './bar') are not supported for
relative executable-bundle imports and will throw; mention the relevant parsing
rules this function enforces (splitCommaSeparatedSpecifiers, IDENTIFIER_RE) and
the error messages thrown so contributors understand the constraint and how to
format imports like { foo, bar }.
web-ui/modules/app.methods.session-trash.mjs-167-206 (1)

167-206: ⚠️ Potential issue | 🟡 Minor

Unhandled error from selectSession may leave UI in inconsistent state.

If selectSession throws (line 205), the error propagates after the session has already been removed from sessionsList. Consider wrapping in try/catch to handle gracefully.

🛡️ Optional defensive fix
             this.clearActiveSessionState();
             return;
         }
-        await this.selectSession(nextSession);
+        try {
+            await this.selectSession(nextSession);
+        } catch (e) {
+            this.clearActiveSessionState();
+        }
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-trash.mjs` around lines 167 - 206, Before
mutating state, save prior state (e.g., const prevSessions =
[...this.sessionsList]; const prevActive = this.activeSession). After removing
the session and computing nextSession, wrap the await
this.selectSession(nextSession) call in try/catch; on catch, restore
this.sessionsList = prevSessions, call
this.syncSessionPathOptionsForSource(this.sessionFilterSource,
this.extractPathOptionsFromSessions(prevSessions), false), restore
this.activeSession = prevActive (or call this.clearActiveSessionState() if
prevActive was null), and then rethrow or log the error so the UI remains
consistent; keep references to currentList, removedIndex, nextActiveKey,
activeKey, this.selectSession, this.syncSessionPathOptionsForSource,
this.clearActiveSessionState and this.sessionsList to locate the changes.
web-ui/styles/sessions-preview.css-399-404 (1)

399-404: ⚠️ Potential issue | 🟡 Minor

Keep the disabled-button rule scoped to the session UI.

.btn[disabled] is global, so importing this stylesheet will restyle disabled buttons across the app, not just inside the preview/session surfaces. If that change is intentional, it belongs in the shared button/base stylesheet rather than a feature-local CSS file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/sessions-preview.css` around lines 399 - 404, The
.btn[disabled] selector is global and will affect all disabled buttons; scope it
to the session preview UI by changing the selector to target the preview
container (e.g., prefix with the session preview root like .sessions-preview or
.session-preview so it becomes .sessions-preview .btn[disabled]) so the rule
only applies inside the session preview surface, or if the intention was to
change global disabled button styling, move the rule into the shared button/base
stylesheet instead.
web-ui/styles/skills-market.css-101-105 (1)

101-105: ⚠️ Potential issue | 🟡 Minor

Add a narrow-width fallback for the market layout.

This file keeps the summary strip at 4 columns, the market grid at 2 columns, the action grid at 3 columns, and the status select at a fixed 210px width at every viewport size. In a narrow window or split pane, that squeezes the market cards and filters into unreadable widths instead of stacking.

Also applies to: 220-223, 276-279, 331-333

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/skills-market.css` around lines 101 - 105, The layout keeps
fixed column counts and a fixed 210px select width which breaks in narrow
viewports; update the grid rules for .skills-summary-strip, the market grid
selector, the action grid selector, and the status select so they are
responsive: replace repeat(N, ...) usages with a fluid pattern (e.g.,
repeat(auto-fit or auto-fill, minmax(<reasonable min>, 1fr))) or add a
small-screen media query that collapses them to 1 column, and change the fixed
210px width on the status select to a responsive rule (e.g., width:100% with a
max-width:210px or a smaller min-width) so controls stack and cards don’t become
unreadably narrow. Ensure you update the rules referenced around the other
occurrences called out (lines ~220-223, ~276-279, ~331-333).
web-ui/modules/app.methods.openclaw-editing.mjs-286-288 (1)

286-288: ⚠️ Potential issue | 🟡 Minor

Wrap localStorage.setItem in try-catch to handle quota/access errors.

localStorage can throw QuotaExceededError or SecurityError (in private browsing). An unhandled exception here would interrupt the save flow silently.

🛡️ Proposed fix
         saveOpenclawConfigs() {
-            localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
+            try {
+                localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
+            } catch (e) {
+                console.warn('Failed to save OpenClaw configs to localStorage:', e);
+                this.showMessage?.('保存配置失败,请检查浏览器存储设置', 'error');
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-editing.mjs` around lines 286 - 288, The
saveOpenclawConfigs method currently calls localStorage.setItem directly; wrap
that call in a try-catch inside saveOpenclawConfigs to handle
QuotaExceededError/SecurityError (private browsing) so the app flow isn't
interrupted—catch the error, log it (using console.error or the module's logger)
with a clear message including the error, and optionally fall back to an
in-memory cache or skip persistence without rethrowing; ensure the unique symbol
saveOpenclawConfigs and the localStorage.setItem call are the changes' focal
points.
web-ui/modules/app.methods.session-actions.mjs-428-446 (1)

428-446: ⚠️ Potential issue | 🟡 Minor

Duplicate logic in else branches.

Lines 432-438 and 439-446 contain identical code. The else if and else branches perform the same operation, making the conditional redundant.

♻️ Proposed simplification
                 if (this.sessionTrashLoadedOnce) {
                     this.prependSessionTrashItem(this.buildSessionTrashItemFromSession(session, res), {
                         totalCount: res && res.totalCount !== undefined ? res.totalCount : undefined
                     });
-                } else if (this.sessionTrashCountLoadedOnce) {
-                    this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
-                        res && res.totalCount !== undefined
-                            ? res.totalCount
-                            : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
-                        this.sessionTrashItems
-                    );
                 } else {
                     this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
                         res && res.totalCount !== undefined
                             ? res.totalCount
                             : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
                         this.sessionTrashItems
                     );
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-actions.mjs` around lines 428 - 446, The
two branches after the initial if (this.sessionTrashLoadedOnce) are identical,
so remove the redundant else-if and collapse them into a single else branch that
calls this.normalizeSessionTrashTotalCount(...) and assigns to
this.sessionTrashTotalCount; locate the duplicate logic around
prependSessionTrashItem, buildSessionTrashItemFromSession,
normalizeSessionTrashTotalCount and the properties sessionTrashLoadedOnce,
sessionTrashCountLoadedOnce, sessionTrashTotalCount, sessionTrashItems and
replace the existing "else if (this.sessionTrashCountLoadedOnce) { ... } else {
... }" with a single "else { ... }" containing the shared code.
web-ui/modules/app.methods.openclaw-persist.mjs-161-179 (1)

161-179: ⚠️ Potential issue | 🟡 Minor

Missing try/catch in applyOpenclawConfig.

Unlike saveAndApplyOpenclawConfig which wraps the API call in try/catch, applyOpenclawConfig doesn't catch network/runtime errors from the api() call.

🛡️ Proposed fix to add error handling
         async applyOpenclawConfig(name) {
             this.currentOpenclawConfig = name;
             const config = this.openclawConfigs[name];
             if (!this.openclawHasContent(config)) {
                 return this.showMessage('配置为空', 'error');
             }
-            const res = await api('apply-openclaw-config', {
-                content: config.content,
-                lineEnding: this.openclawLineEnding
-            });
-            if (res.error || res.success === false) {
-                this.showMessage(res.error || '应用配置失败', 'error');
-            } else {
-                this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
-                this.openclawConfigExists = true;
-                const targetTip = res.targetPath ? `(${res.targetPath})` : '';
-                this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
+            try {
+                const res = await api('apply-openclaw-config', {
+                    content: config.content,
+                    lineEnding: this.openclawLineEnding
+                });
+                if (res.error || res.success === false) {
+                    this.showMessage(res.error || '应用配置失败', 'error');
+                } else {
+                    this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
+                    this.openclawConfigExists = true;
+                    const targetTip = res.targetPath ? `(${res.targetPath})` : '';
+                    this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
+                }
+            } catch (e) {
+                this.showMessage('应用配置失败', 'error');
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-persist.mjs` around lines 161 - 179, The
applyOpenclawConfig method lacks error handling for the async api() call; wrap
the api('apply-openclaw-config', ...) invocation in a try/catch inside
applyOpenclawConfig, mirror the pattern used in saveAndApplyOpenclawConfig:
await the api call in try, handle res.error or res.success === false as you
already do, and in catch call this.showMessage(error.message || '应用配置失败',
'error') (and optionally log the error) to avoid uncaught network/runtime
exceptions; keep existing updates to this.openclawConfigPath and
this.openclawConfigExists only on successful response.
web-ui/modules/app.methods.providers.mjs-150-175 (1)

150-175: ⚠️ Potential issue | 🟡 Minor

Modal closed before API call completes.

updateProvider calls closeEditModal() at line 163 before the async API call. If the API call fails, the user has already lost their input and would need to re-enter it to retry.

🛡️ Proposed fix to close modal after success
         async updateProvider() {
             if (this.editingProvider.readOnly || this.editingProvider.nonEditable) {
                 this.showMessage('该 provider 为保留项,不可编辑', 'error');
                 this.closeEditModal();
                 return;
             }
             if (!this.editingProvider.url) {
                 return this.showMessage('URL 必填', 'error');
             }

             const name = this.editingProvider.name;
             const url = this.editingProvider.url.trim();
             const key = this.editingProvider.key || '';
-            this.closeEditModal();
             try {
                 const res = await api('update-provider', { name, url, key });
                 if (res.error) {
                     this.showMessage(res.error, 'error');
                     return;
                 }
                 this.showMessage('操作成功', 'success');
+                this.closeEditModal();
                 await this.loadAll();
             } catch (e) {
                 this.showMessage('更新失败', 'error');
             }
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 150 - 175, The modal
is closed before the async API call in updateProvider, causing lost input on
failure; change updateProvider so closeEditModal() is called only after a
successful response (i.e., after checking !res.error and showing success), and
remove or defer the earlier closeEditModal() call; in the catch and the
res.error branch keep the modal open and show the error with this.showMessage,
so the user can correct/retry (refer to the updateProvider method and its calls
to closeEditModal(), api('update-provider', ...), and this.showMessage).
web-ui/modules/app.methods.providers.mjs-117-133 (1)

117-133: ⚠️ Potential issue | 🟡 Minor

Missing try/catch in deleteProvider.

Unlike addProvider which wraps the API call in try/catch, deleteProvider doesn't catch network/runtime errors. If the API call throws, the error will propagate unhandled.

🛡️ Proposed fix to add error handling
         async deleteProvider(name) {
             if (this.isNonDeletableProvider(name)) {
                 this.showMessage('该 provider 为保留项,不可删除', 'info');
                 return;
             }
-            const res = await api('delete-provider', { name });
-            if (res.error) {
-                this.showMessage(res.error, 'error');
-                return;
-            }
-            if (res.switched && res.provider) {
-                this.showMessage(`已删除提供商,自动切换到 ${res.provider}${res.model ? ` / ${res.model}` : ''}`, 'success');
-            } else {
-                this.showMessage('操作成功', 'success');
+            try {
+                const res = await api('delete-provider', { name });
+                if (res.error) {
+                    this.showMessage(res.error, 'error');
+                    return;
+                }
+                if (res.switched && res.provider) {
+                    this.showMessage(`已删除提供商,自动切换到 ${res.provider}${res.model ? ` / ${res.model}` : ''}`, 'success');
+                } else {
+                    this.showMessage('操作成功', 'success');
+                }
+                await this.loadAll();
+            } catch (e) {
+                this.showMessage('删除失败', 'error');
             }
-            await this.loadAll();
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 117 - 133, Wrap the
API call and subsequent response handling in deleteProvider inside a try/catch
(similar to addProvider) so network/runtime errors don't propagate: place the
await api('delete-provider', { name }) and the following res checks into a try
block, call this.showMessage(error.message || String(error), 'error') in the
catch and return, and keep the existing this.loadAll() behavior (optionally call
it in finally if you want it always executed). Target the deleteProvider method
to implement this change.
🧹 Nitpick comments (10)
tests/unit/session-tab-switch-performance.test.mjs (1)

9-11: Unused variables after refactoring.

The __filename, __dirname, and projectRoot variables are no longer used since the file now relies on bundled readers. However, per the retrieved learnings, I'll flag this as an optional cleanup rather than a required change.

🧹 Optional cleanup to remove unused variables
 import {
     readBundledWebUiHtml,
     readBundledWebUiScript
 } from './helpers/web-ui-source.mjs';

-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-const projectRoot = path.resolve(__dirname, '..', '..');
-
-const helpers = await import(pathToFileURL(path.join(__dirname, '..', '..', 'web-ui', 'session-helpers.mjs')));
+const helpers = await import(new URL('../../web-ui/session-helpers.mjs', import.meta.url));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/session-tab-switch-performance.test.mjs` around lines 9 - 11,
Remove the now-unused module-scope variables __filename, __dirname, and
projectRoot from the top of tests/unit/session-tab-switch-performance.test.mjs;
locate the const declarations for __filename = fileURLToPath(import.meta.url),
__dirname = path.dirname(__filename), and projectRoot = path.resolve(__dirname,
'..', '..') and delete them (and any related unused imports if applicable) so
the test file no longer contains unused variables after switching to bundled
readers.
web-ui/modules/app.computed.index.mjs (1)

6-13: Potential property collision when spreading multiple computed factories.

If any of the four factory functions (createSessionComputed, createDashboardComputed, createSkillsComputed, createConfigModeComputed) return objects with overlapping property names, later spreads will silently override earlier ones. This is likely intentional given the module separation, but worth noting for future maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.computed.index.mjs` around lines 6 - 13, The current
createAppComputed function spreads objects from createSessionComputed,
createDashboardComputed, createSkillsComputed, and createConfigModeComputed
which can silently override duplicate property names; update createAppComputed
to either (a) namespace each factory's return under distinct keys (e.g.,
session, dashboard, skills, configMode) so collisions cannot occur, or (b)
perform an explicit merge that detects duplicate keys (collect keys from
createSessionComputed/createDashboardComputed/createSkillsComputed/createConfigModeComputed,
and if a key is about to be overwritten, throw or log an error) and then combine
them; choose one approach and implement it by modifying createAppComputed and
adjusting any callers accordingly to use the new namespaced or validated shape.
web-ui/styles/skills-list.css (1)

88-95: Consider using CSS variables for brand colors.

Hardcoded rgba values like rgba(201, 94, 75, 0.35) appear in hover/selected states. For consistency with the theming approach used elsewhere (e.g., var(--color-brand)), consider defining these as CSS variables with alpha variants.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/skills-list.css` around lines 88 - 95, Replace the hardcoded
rgba values in .skill-item:hover and .skill-item.selected with CSS variables:
add a brand color and alpha variants (or an RGB root variable plus alpha uses)
in your theme root (e.g., --color-brand or --color-brand-rgb) and then use those
variables for border-color and the linear-gradient background; update
.skill-item:hover to use the variable with 0.35 alpha and .skill-item.selected
to use the variable with 0.55 alpha and the two gradient stops (0.10 and 0.04)
so theming is consistent with var(--color-brand) usage elsewhere.
web-ui/partials/index/layout-header.html (1)

354-364: Remove the permanently disabled config-subtabs block.

v-if="false && ..." means this markup can never render, so it only adds dead template surface that can drift out of sync with the live tab navigation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/layout-header.html` around lines 354 - 364, Remove the
dead template block gated by v-if="false && mainTab === 'config' &&
!sessionStandalone": delete the entire <div> with class "config-subtabs" and its
three <button> children (the elements using classes "config-subtab" and the
`@click` handler onConfigTabClick with configMode checks) so there’s no
permanently disabled markup drifting out of sync with live navigation; if there
are related unused styles or references (e.g., CSS for
.config-subtabs/.config-subtab or code only used by those buttons), remove or
refactor them as well.
web-ui/partials/index/panel-sessions.html (1)

59-78: Disabled filter controls should include user feedback or hide entirely.

The role and time filter selects are disabled without explanation. Consider adding a tooltip or visual hint explaining why they're unavailable, or conditionally hide them until implemented.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-sessions.html` around lines 59 - 78, The two
disabled select controls (class="session-role-select" bound to sessionRoleFilter
and class="session-time-select" bound to sessionTimePreset, both calling
onSessionFilterChange) lack user feedback; update the template to either
conditionally render them only when the feature is available (use a flag like
filtersAvailable and wrap the selects in a v-if) or keep them visible but add
explanatory UX: a title/tooltip and ARIA text explaining why they're disabled
and how to enable them; ensure the selects retain disabled attribute when
unavailable and that the message references the control (role/time) so users
understand why sessionRoleFilter and sessionTimePreset cannot be changed.
web-ui/modules/app.computed.dashboard.mjs (1)

23-25: Stub returning constant false — verify intent or add TODO.

hasLocalAndProxy() always returns false. If this is a placeholder for future implementation, consider adding a comment to clarify intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.computed.dashboard.mjs` around lines 23 - 25, The
hasLocalAndProxy() method currently returns the constant false (a stub); either
implement the real detection logic or mark it explicitly as a TODO/comment and
return a safe default. Locate hasLocalAndProxy() in
web-ui/modules/app.computed.dashboard.mjs, and if intended as a placeholder add
a clear TODO comment explaining expected behavior and when it should be
implemented (or add unit tests), otherwise replace the stub with the actual
checks for both local and proxy presence and ensure callers handle the returned
value correctly.
web-ui/partials/index/panel-config-claude.html (1)

78-84: Disabled share button has redundant loading class binding.

The disabled HTML attribute makes the :class="{ loading: claudeShareLoading[name] }" binding ineffective since the button can never enter loading state while permanently disabled.

♻️ Simplify by removing unused binding
-                                <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" `@click`="copyClaudeShareCommand(name)" disabled title="分享导入命令(暂时禁用)" aria-label="Share import command">
+                                <button class="card-action-btn" `@click`="copyClaudeShareCommand(name)" disabled title="分享导入命令(暂时禁用)" aria-label="Share import command">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-claude.html` around lines 78 - 84, The
button currently uses a useless dynamic class binding :class="{ loading:
claudeShareLoading[name] }" while also being permanently disabled, so remove the
binding from the button element and rely on the static disabled/title attributes
(leave the `@click`="copyClaudeShareCommand(name)" handler as-is or remove the
handler if the button should be inert); specifically edit the button that
references claudeShareLoading[name] and copyClaudeShareCommand to drop the
:class binding (or alternatively make the disabled state conditional if you
intend to show the loading class).
web-ui/partials/index/modal-config-template-agents.html (1)

119-121: Complex disabled condition is correct but could benefit from extracted computed.

The button's disabled logic agentsSaving || agentsLoading || agentsDiffLoading || (agentsDiffVisible && !agentsDiffHasChanges) correctly prevents apply during operations and when no changes exist. For maintainability, consider extracting to a computed property.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-config-template-agents.html` around lines 119 -
121, Extract the complex disabled expression into a computed property (e.g.,
agentsApplyDisabled) and use it on the button so the template uses
:disabled="agentsApplyDisabled"; implement the computed to return agentsSaving
|| agentsLoading || agentsDiffLoading || (agentsDiffVisible &&
!agentsDiffHasChanges) and keep the existing apply handler applyAgentsContent
unchanged, updating any template references if needed (e.g., replace the inline
expression with agentsApplyDisabled and leave the click text logic as-is).
web-ui/modules/app.methods.claude-config.mjs (1)

157-165: Hardcoded default values in modal reset.

The closeClaudeConfigModal resets newClaudeConfig with hardcoded values for baseUrl and model. If these defaults need to be configurable or align with other defaults in the codebase, consider extracting them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.claude-config.mjs` around lines 157 - 165, The
modal reset in closeClaudeConfigModal currently overwrites newClaudeConfig with
hardcoded baseUrl and model values; extract these defaults into shared constants
or a config object (e.g., DEFAULT_CLAUDE_BASE_URL and DEFAULT_CLAUDE_MODEL or a
DEFAULT_CLAUDE_CONFIG) and use those constants when resetting newClaudeConfig so
the modal aligns with project-wide defaults and is configurable; update
closeClaudeConfigModal to assign name/apiKey to empty strings and baseUrl/model
from the extracted DEFAULT_CLAUDE_CONFIG (or import the existing defaults if
present).
web-ui/modules/app.methods.session-actions.mjs (1)

231-250: Inconsistent clipboard API fallback order.

copyResumeCommand tries fallbackCopyText first then navigator.clipboard, while copyInstallCommand does the opposite. Consider standardizing the order for consistency, though this doesn't affect functionality.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-actions.mjs` around lines 231 - 250,
copyResumeCommand currently calls fallbackCopyText(command) before attempting
navigator.clipboard, which is the opposite order used in copyInstallCommand;
change copyResumeCommand to first attempt navigator.clipboard.writeText(command)
(guarded by navigator.clipboard && window.isSecureContext and wrapped in
try/catch), and only if that fails fall back to fallbackCopyText(command),
preserving the same success/error showMessage calls and early returns as in
copyInstallCommand to keep behavior consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli.js`:
- Around line 10708-10724: The current flow checks fs.existsSync(filePath) and
returns 404 before consulting PUBLIC_WEB_UI_DYNAMIC_ASSETS, making bundled
readers unreachable when the source files are absent; change the logic so you
lookup const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath)
before the file-existence check (or, alternatively, if dynamicAsset is truthy
serve it via dynamicAsset.reader(filePath) regardless of fs.existsSync),
ensuring dynamicAsset.reader is invoked for entries in
PUBLIC_WEB_UI_DYNAMIC_ASSETS (and preserve the existing behavior for
PUBLIC_WEB_UI_STATIC_ASSETS and other 404 cases).
- Around line 10016-10042: The addWatcher function creates an FSWatcher via
fs.watch but doesn't attach an 'error' listener, so update the watcher creation
in addWatcher to register watcher.on('error', ...) after fs.watch returns;
inside the handler follow the file's existing pattern (log the error and attempt
a graceful recovery such as calling syncDirectoryTree(target) or removing the
watcher and marking watcherEntries accordingly) and ensure
watcherEntries.set(...) still runs only after the error listener is attached;
reference addWatcher, fs.watch, watcher, watcherEntries, syncDirectoryTree and
trigger when locating where to add this handler.
- Around line 10016-10042: The current addWatcher uses fs.watch directly on
files which breaks when editors save via atomic replace (temp-file + rename)
because the inode changes; update addWatcher so that when target is a file
(isDirectory === false) you watch its parent directory instead of the file
itself: compute const basename = path.basename(target) and const watchTarget =
isDirectory ? target : path.dirname(target), call fs.watch(watchTarget, {
recursive }, ...) and inside the callback ignore events where filename !==
basename (for file watchers) and still apply the same extension and eventType
checks, call syncDirectoryTree when needed (for directory rename cases), and
store the original target, basename and isDirectory in watcherEntries so
trigger({ target, eventType, filename }) continues to receive the original file
target semantics; this prevents losing updates when the file inode is replaced
by atomic saves.

In `@tests/unit/helpers/web-ui-app-options.mjs`:
- Around line 15-22: HEAD_WEB_UI_FILES is missing modules imported by
web-ui/app.js, which prevents readExecutableBundledWebUiScript from traversing
the import graph; update the HEAD_WEB_UI_FILES constant to include the directly
imported files web-ui/modules/app.computed.index.mjs and
web-ui/modules/app.methods.index.mjs, plus their transitive imports such as
web-ui/modules/app.computed.dashboard.mjs,
web-ui/modules/app.computed.session.mjs and the collection of
web-ui/modules/app.methods.*.mjs files referenced by app.methods.index.mjs so
the bundler can find and include all required modules when
readExecutableBundledWebUiScript processes app.js.

In `@tests/unit/web-ui-behavior-parity.test.mjs`:
- Around line 8-9: The test fails because HEAD_WEB_UI_FILES doesn't include all
module dependencies required when captureHeadBundledAppOptions() stages the HEAD
fixture and app.js imports modules; update the test so HEAD_WEB_UI_FILES (or the
staging logic used by captureHeadBundledAppOptions) includes every file under
web-ui/modules/ that app.js and the generated index files import (e.g.,
app.computed.index.mjs, app.methods.index.mjs, app.constants.mjs,
app.computed.dashboard.mjs, app.computed.session.mjs, and all app.methods.*
files), or change the staging to recursively stage the entire web-ui/modules/
directory from HEAD so captureHeadBundledAppOptions can evaluate app.js without
missing imports.

In `@web-ui/logic.agents-diff.mjs`:
- Around line 27-35: splitDiffLines currently removes a trailing '\n' before
splitting, which collapses inputs like '\n' into an empty array; remove the
logic that slices off the final newline so that normalizeDiffText output is
split directly (i.e., keep the existing checks for falsy normalized and then
return normalized.split('\n') without trimming the trailing newline) so '\n'
becomes [''] and trailing-blank-line edits are preserved; update function
splitDiffLines (and adjust any tests for normalizeDiffText if necessary)
accordingly.

In `@web-ui/modules/app.methods.codex-config.mjs`:
- Around line 76-87: performProviderSwitch sets this.currentProvider before
calling loadModelsForProvider so a failed load can leave a stale currentModel
and cause applyCodexConfigDirect to push an invalid provider/model; capture the
previous provider (e.g., const prevProvider = this.currentProvider) and wrap
await this.loadModelsForProvider(name) in try/catch (or check its return) so
that on failure you restore this.currentProvider = prevProvider, avoid changing
this.currentModel/modelsHasCurrent, and skip calling applyCodexConfigDirect;
ensure the logic around this.modelsSource, this.models and this.currentModel
only runs when loadModelsForProvider succeeded.
- Around line 228-249: The current loop assumes each entry in this.providersList
is an object with a name property, so string entries (provider names) are
filtered out and speed tests never run; modify the handling in the block that
builds providers and tasks (around providersList, runSpeedTest,
buildSpeedTestIssue) to accept both string and object forms — coerce each
provider to an object with a name (e.g., if provider is a string use { name:
provider }) before filtering and mapping, then use provider.name when calling
this.runSpeedTest and this.buildSpeedTestIssue so string-array inputs from
listRes.providers are processed correctly.

In `@web-ui/modules/app.methods.runtime.mjs`:
- Around line 188-220: The importBackupFile method can be re-entered because
claudeImportLoading / codexImportLoading are set but not checked; add an early
guard at the top of importBackupFile that checks this[loadingKey] and returns
immediately if true to prevent overlapping restore-*-dir calls (use the same
loadingKey logic you already have), then proceed with the existing file-size
check, readFileAsBase64, api(action...), and ensure the finally block still
clears this[loadingKey] and calls resetImportInput(type); reference
importBackupFile, claudeImportLoading, codexImportLoading, action
('restore-claude-dir' / 'restore-codex-dir'), readFileAsBase64, resetImportInput
when making the change.

In `@web-ui/modules/app.methods.session-browser.mjs`:
- Around line 370-377: The early-return in loadSessionStandalonePlain fails to
increment sessionStandaloneRequestSeq so stale async responses can still apply
after this.activeSession is cleared; update loadSessionStandalonePlain (and the
later async response handling block that processes the "session-plain" response)
to increment sessionStandaloneRequestSeq when bailing out and to check the
current sequence before applying results; specifically, increment
sessionStandaloneRequestSeq at the start of the early-return branch and ensure
the async response handler (the code that sets sessionStandaloneText,
sessionStandaloneTitle, sessionStandaloneSourceLabel, sessionStandaloneError and
reads this.activeSession and sessionId) compares the response seq to
this.sessionStandaloneRequestSeq and aborts if they differ to avoid
dereferencing a null this.activeSession.
- Around line 87-119: sessionPathRequestSeq is a single shared token for all
sources causing race-invalidations across sources; change to per-source
sequencing (e.g., a map keyed by source) so each source tracks its own request
sequence and loading state. In loadSessionPathOptions, replace uses of
this.sessionPathRequestSeq with a per-source counter (e.g.,
this.sessionPathRequestSeqMap[source]) when creating requestSeq and when
comparing in the response/finally blocks, and ensure sessionPathOptionsLoading
and sessionPathOptionsLoadedMap semantics still work per-source (or keep a
separate sessionPathOptionsLoadingMap[source]) so a request for one source no
longer invalidates another.

In `@web-ui/modules/app.methods.startup-claude.mjs`:
- Around line 24-25: The startup sequence in loadAll() awaits
Promise.all([api('status'), api('list')]) but only checks statusRes.error, so a
failing api('list') (listRes.error) is ignored, leaving providersList undefined;
update loadAll() to treat listRes.error as a startup failure the same way as
statusRes.error (log/handle the error and abort initialization), and apply the
same check to the later block that handles statusRes/listRes around the
providersList assignment so both statusRes and listRes are validated before
proceeding.

In `@web-ui/styles/base-theme.css`:
- Line 1: The stylesheet contains an external Google Fonts import (`@import`
url(...) in base-theme.css) which creates a runtime dependency; remove this
external `@import` and instead reference fonts shipped with the app via local
`@font-face` declarations or fall back to system fonts (e.g., Fira Mono, JetBrains
Mono, Source Sans 3, Space Grotesk alternatives) so the UI does not perform
third-party network requests—update base-theme.css to load local font files from
the app bundle (or replace with a system-font stack) and ensure any font-family
usages across the CSS match the new local or fallback names.

---

Minor comments:
In `@web-ui/modules/app.methods.openclaw-editing.mjs`:
- Around line 286-288: The saveOpenclawConfigs method currently calls
localStorage.setItem directly; wrap that call in a try-catch inside
saveOpenclawConfigs to handle QuotaExceededError/SecurityError (private
browsing) so the app flow isn't interrupted—catch the error, log it (using
console.error or the module's logger) with a clear message including the error,
and optionally fall back to an in-memory cache or skip persistence without
rethrowing; ensure the unique symbol saveOpenclawConfigs and the
localStorage.setItem call are the changes' focal points.

In `@web-ui/modules/app.methods.openclaw-persist.mjs`:
- Around line 161-179: The applyOpenclawConfig method lacks error handling for
the async api() call; wrap the api('apply-openclaw-config', ...) invocation in a
try/catch inside applyOpenclawConfig, mirror the pattern used in
saveAndApplyOpenclawConfig: await the api call in try, handle res.error or
res.success === false as you already do, and in catch call
this.showMessage(error.message || '应用配置失败', 'error') (and optionally log the
error) to avoid uncaught network/runtime exceptions; keep existing updates to
this.openclawConfigPath and this.openclawConfigExists only on successful
response.

In `@web-ui/modules/app.methods.providers.mjs`:
- Around line 150-175: The modal is closed before the async API call in
updateProvider, causing lost input on failure; change updateProvider so
closeEditModal() is called only after a successful response (i.e., after
checking !res.error and showing success), and remove or defer the earlier
closeEditModal() call; in the catch and the res.error branch keep the modal open
and show the error with this.showMessage, so the user can correct/retry (refer
to the updateProvider method and its calls to closeEditModal(),
api('update-provider', ...), and this.showMessage).
- Around line 117-133: Wrap the API call and subsequent response handling in
deleteProvider inside a try/catch (similar to addProvider) so network/runtime
errors don't propagate: place the await api('delete-provider', { name }) and the
following res checks into a try block, call this.showMessage(error.message ||
String(error), 'error') in the catch and return, and keep the existing
this.loadAll() behavior (optionally call it in finally if you want it always
executed). Target the deleteProvider method to implement this change.

In `@web-ui/modules/app.methods.session-actions.mjs`:
- Around line 428-446: The two branches after the initial if
(this.sessionTrashLoadedOnce) are identical, so remove the redundant else-if and
collapse them into a single else branch that calls
this.normalizeSessionTrashTotalCount(...) and assigns to
this.sessionTrashTotalCount; locate the duplicate logic around
prependSessionTrashItem, buildSessionTrashItemFromSession,
normalizeSessionTrashTotalCount and the properties sessionTrashLoadedOnce,
sessionTrashCountLoadedOnce, sessionTrashTotalCount, sessionTrashItems and
replace the existing "else if (this.sessionTrashCountLoadedOnce) { ... } else {
... }" with a single "else { ... }" containing the shared code.

In `@web-ui/modules/app.methods.session-trash.mjs`:
- Around line 167-206: Before mutating state, save prior state (e.g., const
prevSessions = [...this.sessionsList]; const prevActive = this.activeSession).
After removing the session and computing nextSession, wrap the await
this.selectSession(nextSession) call in try/catch; on catch, restore
this.sessionsList = prevSessions, call
this.syncSessionPathOptionsForSource(this.sessionFilterSource,
this.extractPathOptionsFromSessions(prevSessions), false), restore
this.activeSession = prevActive (or call this.clearActiveSessionState() if
prevActive was null), and then rethrow or log the error so the UI remains
consistent; keep references to currentList, removedIndex, nextActiveKey,
activeKey, this.selectSession, this.syncSessionPathOptionsForSource,
this.clearActiveSessionState and this.sessionsList to locate the changes.

In `@web-ui/source-bundle.cjs`:
- Around line 137-164: Document that relative imports must use named-import
syntax only by adding a concise comment above buildRelativeImportAliasStatements
(and optionally referencing this rule in contributor docs) stating that default
imports (e.g., import foo from './bar') and namespace imports (import * as foo
from './bar') are not supported for relative executable-bundle imports and will
throw; mention the relevant parsing rules this function enforces
(splitCommaSeparatedSpecifiers, IDENTIFIER_RE) and the error messages thrown so
contributors understand the constraint and how to format imports like { foo, bar
}.

In `@web-ui/styles/feedback.css`:
- Line 108: The file contains a standalone comment "/* 内容区域包裹器 - 稳定高度 */" with
no accompanying CSS rule; either remove this trailing comment if it was left
accidentally, or add the intended rule (e.g., a selector like .content-wrapper
or the appropriate container selector used in your layout) and define the
stable-height styles (height/min-height, overflow rules, etc.) so the comment
has a corresponding CSS block; locate the lone comment text to apply the change.

In `@web-ui/styles/sessions-preview.css`:
- Around line 399-404: The .btn[disabled] selector is global and will affect all
disabled buttons; scope it to the session preview UI by changing the selector to
target the preview container (e.g., prefix with the session preview root like
.sessions-preview or .session-preview so it becomes .sessions-preview
.btn[disabled]) so the rule only applies inside the session preview surface, or
if the intention was to change global disabled button styling, move the rule
into the shared button/base stylesheet instead.

In `@web-ui/styles/skills-market.css`:
- Around line 101-105: The layout keeps fixed column counts and a fixed 210px
select width which breaks in narrow viewports; update the grid rules for
.skills-summary-strip, the market grid selector, the action grid selector, and
the status select so they are responsive: replace repeat(N, ...) usages with a
fluid pattern (e.g., repeat(auto-fit or auto-fill, minmax(<reasonable min>,
1fr))) or add a small-screen media query that collapses them to 1 column, and
change the fixed 210px width on the status select to a responsive rule (e.g.,
width:100% with a max-width:210px or a smaller min-width) so controls stack and
cards don’t become unreadably narrow. Ensure you update the rules referenced
around the other occurrences called out (lines ~220-223, ~276-279, ~331-333).

---

Nitpick comments:
In `@tests/unit/session-tab-switch-performance.test.mjs`:
- Around line 9-11: Remove the now-unused module-scope variables __filename,
__dirname, and projectRoot from the top of
tests/unit/session-tab-switch-performance.test.mjs; locate the const
declarations for __filename = fileURLToPath(import.meta.url), __dirname =
path.dirname(__filename), and projectRoot = path.resolve(__dirname, '..', '..')
and delete them (and any related unused imports if applicable) so the test file
no longer contains unused variables after switching to bundled readers.

In `@web-ui/modules/app.computed.dashboard.mjs`:
- Around line 23-25: The hasLocalAndProxy() method currently returns the
constant false (a stub); either implement the real detection logic or mark it
explicitly as a TODO/comment and return a safe default. Locate
hasLocalAndProxy() in web-ui/modules/app.computed.dashboard.mjs, and if intended
as a placeholder add a clear TODO comment explaining expected behavior and when
it should be implemented (or add unit tests), otherwise replace the stub with
the actual checks for both local and proxy presence and ensure callers handle
the returned value correctly.

In `@web-ui/modules/app.computed.index.mjs`:
- Around line 6-13: The current createAppComputed function spreads objects from
createSessionComputed, createDashboardComputed, createSkillsComputed, and
createConfigModeComputed which can silently override duplicate property names;
update createAppComputed to either (a) namespace each factory's return under
distinct keys (e.g., session, dashboard, skills, configMode) so collisions
cannot occur, or (b) perform an explicit merge that detects duplicate keys
(collect keys from
createSessionComputed/createDashboardComputed/createSkillsComputed/createConfigModeComputed,
and if a key is about to be overwritten, throw or log an error) and then combine
them; choose one approach and implement it by modifying createAppComputed and
adjusting any callers accordingly to use the new namespaced or validated shape.

In `@web-ui/modules/app.methods.claude-config.mjs`:
- Around line 157-165: The modal reset in closeClaudeConfigModal currently
overwrites newClaudeConfig with hardcoded baseUrl and model values; extract
these defaults into shared constants or a config object (e.g.,
DEFAULT_CLAUDE_BASE_URL and DEFAULT_CLAUDE_MODEL or a DEFAULT_CLAUDE_CONFIG) and
use those constants when resetting newClaudeConfig so the modal aligns with
project-wide defaults and is configurable; update closeClaudeConfigModal to
assign name/apiKey to empty strings and baseUrl/model from the extracted
DEFAULT_CLAUDE_CONFIG (or import the existing defaults if present).

In `@web-ui/modules/app.methods.session-actions.mjs`:
- Around line 231-250: copyResumeCommand currently calls
fallbackCopyText(command) before attempting navigator.clipboard, which is the
opposite order used in copyInstallCommand; change copyResumeCommand to first
attempt navigator.clipboard.writeText(command) (guarded by navigator.clipboard
&& window.isSecureContext and wrapped in try/catch), and only if that fails fall
back to fallbackCopyText(command), preserving the same success/error showMessage
calls and early returns as in copyInstallCommand to keep behavior consistent.

In `@web-ui/partials/index/layout-header.html`:
- Around line 354-364: Remove the dead template block gated by v-if="false &&
mainTab === 'config' && !sessionStandalone": delete the entire <div> with class
"config-subtabs" and its three <button> children (the elements using classes
"config-subtab" and the `@click` handler onConfigTabClick with configMode checks)
so there’s no permanently disabled markup drifting out of sync with live
navigation; if there are related unused styles or references (e.g., CSS for
.config-subtabs/.config-subtab or code only used by those buttons), remove or
refactor them as well.

In `@web-ui/partials/index/modal-config-template-agents.html`:
- Around line 119-121: Extract the complex disabled expression into a computed
property (e.g., agentsApplyDisabled) and use it on the button so the template
uses :disabled="agentsApplyDisabled"; implement the computed to return
agentsSaving || agentsLoading || agentsDiffLoading || (agentsDiffVisible &&
!agentsDiffHasChanges) and keep the existing apply handler applyAgentsContent
unchanged, updating any template references if needed (e.g., replace the inline
expression with agentsApplyDisabled and leave the click text logic as-is).

In `@web-ui/partials/index/panel-config-claude.html`:
- Around line 78-84: The button currently uses a useless dynamic class binding
:class="{ loading: claudeShareLoading[name] }" while also being permanently
disabled, so remove the binding from the button element and rely on the static
disabled/title attributes (leave the `@click`="copyClaudeShareCommand(name)"
handler as-is or remove the handler if the button should be inert); specifically
edit the button that references claudeShareLoading[name] and
copyClaudeShareCommand to drop the :class binding (or alternatively make the
disabled state conditional if you intend to show the loading class).

In `@web-ui/partials/index/panel-sessions.html`:
- Around line 59-78: The two disabled select controls
(class="session-role-select" bound to sessionRoleFilter and
class="session-time-select" bound to sessionTimePreset, both calling
onSessionFilterChange) lack user feedback; update the template to either
conditionally render them only when the feature is available (use a flag like
filtersAvailable and wrap the selects in a v-if) or keep them visible but add
explanatory UX: a title/tooltip and ARIA text explaining why they're disabled
and how to enable them; ensure the selects retain disabled attribute when
unavailable and that the message references the control (role/time) so users
understand why sessionRoleFilter and sessionTimePreset cannot be changed.

In `@web-ui/styles/skills-list.css`:
- Around line 88-95: Replace the hardcoded rgba values in .skill-item:hover and
.skill-item.selected with CSS variables: add a brand color and alpha variants
(or an RGB root variable plus alpha uses) in your theme root (e.g.,
--color-brand or --color-brand-rgb) and then use those variables for
border-color and the linear-gradient background; update .skill-item:hover to use
the variable with 0.35 alpha and .skill-item.selected to use the variable with
0.55 alpha and the two gradient stops (0.10 and 0.04) so theming is consistent
with var(--color-brand) usage elsewhere.
🪄 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: 92b1c011-c5f8-420d-9bc4-7d54c4084e46

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and 26d3eeb.

📒 Files selected for processing (75)
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

@github-actions[bot]: Understood. I will conduct this review strictly limited to correctness, safety, and quality within the existing design — no breaking changes, no refactoring suggestions, no API/interface/behavior alterations of any kind.


🧠 Learnings used
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
✅ Actions performed

Full review triggered.

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 19

♻️ Duplicate comments (3)
cli.js (3)

10023-10037: ⚠️ Potential issue | 🟠 Major

Add an 'error' listener for each FSWatcher.

Line 10023 creates a watcher but never subscribes to 'error'. If a watched path becomes unavailable, this can crash the process.

Proposed non-breaking fix
             const watcher = fs.watch(target, { recursive }, (eventType, filename) => {
                 if (isDirectory && !recursive && eventType === 'rename') {
                     syncDirectoryTree(target);
                 }
                 if (!filename) return;
                 const lower = filename.toLowerCase();
                 if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
                 trigger({ target, eventType, filename });
             });
+            watcher.on('error', () => {
+                closeWatcher(watchKey);
+            });
             watcherEntries.set(watchKey, {
                 watcher,
                 target,
                 recursive,
                 isDirectory
             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10023 - 10037, The FSWatcher created by fs.watch
(variable watcher) needs an 'error' listener to prevent uncaught exceptions when
a watched path becomes unavailable; add watcher.on('error', ...) after the
watcher is created (near where watcher is set and watcherEntries.set is called)
to handle the error (log it via your logger or console, close the watcher, and
remove its entry from watcherEntries) and avoid crashing—use existing symbols
watcher, watchKey, watcherEntries, syncDirectoryTree, and trigger to locate and
safely cleanup the watcher on error.

10708-10724: ⚠️ Potential issue | 🔴 Critical

Bundled dynamic asset readers are still gated behind disk existence checks.

Line 10709 returns 404 before Line 10714 checks PUBLIC_WEB_UI_DYNAMIC_ASSETS, so bundled readers can’t serve /web-ui/app.js, /web-ui/logic.mjs, /web-ui/styles.css, etc. when source files are absent.

Proposed non-breaking fix
             const relativePath = path.relative(webDir, filePath).replace(/\\/g, '/');
-            if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
-                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
-                res.end('Not Found');
-                return;
-            }
             const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath);
             if (dynamicAsset) {
                 res.writeHead(200, { 'Content-Type': dynamicAsset.mime });
                 res.end(dynamicAsset.reader(filePath), 'utf-8');
                 return;
             }
             if (!PUBLIC_WEB_UI_STATIC_ASSETS.has(relativePath)) {
                 res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
                 res.end('Not Found');
                 return;
             }
+            if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
+                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
+                res.end('Not Found');
+                return;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10708 - 10724, The request handler currently checks disk
existence for filePath before consulting PUBLIC_WEB_UI_DYNAMIC_ASSETS, causing
bundled readers to be skipped; move the dynamic asset handling so that after
computing relativePath you first check
PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath) and, if present, write the 200
response using dynamicAsset.mime and dynamicAsset.reader(filePath) without
performing the fs.existsSync/fs.statSync checks; keep the existing fallback
checks for static assets (PUBLIC_WEB_UI_STATIC_ASSETS.has(relativePath)) and the
404 responses unchanged.

10016-10031: ⚠️ Potential issue | 🟠 Major

File-level watch remains fragile under atomic-save editors.

Line 10078 still watches files directly. Atomic replace (tmp + rename) can orphan that watcher, so later edits stop triggering restarts.

Also applies to: 10078-10081

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10016 - 10031, The current addWatcher function watches
files directly which breaks when editors do atomic saves (write-to-temp +
rename) because the file watcher can become orphaned; change addWatcher (and
watcherEntries usage) so that when isDirectory is false (watching a file), you
instead watch the parent directory (use path.dirname(target)) and filter events
by the original filename before calling trigger; keep existing behavior for
directory watches (isDirectory true) and ensure the watchKey reflects the actual
watched path (parent dir for files) so duplicate watcher checks still work; also
keep the existing syncDirectoryTree and eventType/extension filtering logic when
dispatching trigger.
🧹 Nitpick comments (3)
web-ui/styles/feedback.css (1)

103-108: Trailing placeholder comment with no implementation.

Line 108 contains a comment "内容区域包裹器 - 稳定高度" (content area wrapper - stable height) but no CSS rules follow. If this is intentional scaffolding for future work, consider adding a TODO marker; otherwise, this line could be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/feedback.css` around lines 103 - 108, The trailing comment
"内容区域包裹器 - 稳定高度" in the CSS is a placeholder with no implementation; either
remove that orphan comment line or turn it into an explicit TODO marker (e.g.,
add "/* TODO: implement stable-height wrapper */") so intent is clear to future
readers; locate the comment near the .mode-content rule in feedback.css and
update it accordingly.
web-ui/partials/index/panel-sessions.html (1)

24-25: Minor: Redundant condition check.

Line 25 checks v-if="!sessionStandalone" inside the v-else block of v-if="sessionStandalone" (line 8). Since we're already in the else branch, sessionStandalone is guaranteed to be falsy, making this condition always true.

This doesn't cause incorrect behavior but adds unnecessary evaluation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-sessions.html` around lines 24 - 25, Remove the
redundant v-if check inside the else branch of the sessionStandalone
conditional: in the template where a parent element uses
v-if="sessionStandalone" and its sibling uses v-else, delete the nested
v-if="!sessionStandalone" on the <div class="selector-section"> (the element
referenced by the selector-section class) so the else branch renders
unconditionally without the unnecessary !sessionStandalone evaluation.
web-ui/styles/controls-forms.css (1)

117-122: Minor inconsistency: hardcoded color value.

Line 118 uses #fff6f5 instead of a CSS variable. Consider using a theme variable for consistency with the design system (e.g., var(--color-error-bg) or similar), though this is a minor nit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/controls-forms.css` around lines 117 - 122, Replace the
hardcoded background color in the .health-issue rule with the design-system CSS
variable (e.g., change background: `#fff6f5`; to background:
var(--color-error-bg)); update the .health-issue selector in the CSS so it uses
the theme variable (or an existing error background variable) to maintain
consistency with other components and avoid hardcoded colors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/run.mjs`:
- Line 20: The HEAD_WEB_UI_FILES fixture used by createHeadWebUiFixture() is
missing transitive modules required by web-ui/app.js; update the
HEAD_WEB_UI_FILES array in tests/unit/helpers/web-ui-app-options.mjs to include
at least "web-ui/modules/app.computed.index.mjs",
"web-ui/modules/app.methods.index.mjs", and "web-ui/modules/app.constants.mjs"
(or simply add all .mjs files from web-ui/modules/) so
captureHeadBundledAppOptions() can resolve imports when creating the HEAD
fixture.

In `@web-ui/logic.sessions.mjs`:
- Around line 27-48: buildSessionListParams currently uses normalized semantics
to decide queryValue via isSessionQueryEnabled(source) but returns the raw
source and pathFilter, allowing dirty values like ' Claude ' to be sent to the
backend; update buildSessionListParams to compute and return a normalized source
(e.g., reuse the same normalization used by isSessionQueryEnabled) and a
normalized/trimmed pathFilter before returning the params so the returned object
uses normalizedSource and trimmed pathFilter (keep queryValue logic unchanged).

In `@web-ui/modules/api.mjs`:
- Around line 1-3: The exported API_BASE uses bare location and can throw
ReferenceError in Node/test environments; change its initializer in this module
(API_BASE) to first guard that location is defined (e.g., typeof location !==
'undefined' or typeof window !== 'undefined') before accessing location.origin,
and only use location.origin when it exists and is not the string 'null';
otherwise return the existing fallback 'http://localhost:3737'. Ensure the check
is performed at module-evaluation time so importing the module in non-browser
contexts does not reference an undefined global.

In `@web-ui/modules/app.computed.session.mjs`:
- Around line 111-124: Currently sessionTrashHasMoreItems and
sessionTrashHiddenCount compare visibleSessionTrashItems.length against
sessionTrashItems.length, which ignores sessionTrashTotalCount; update both to
use sessionTrashCount (the computed total) instead of
this.sessionTrashItems.length so pagination reflects sessionTrashTotalCount;
modify sessionTrashHasMoreItems to return this.visibleSessionTrashItems.length <
this.sessionTrashCount and sessionTrashHiddenCount to return Math.max(0,
this.sessionTrashCount - this.visibleSessionTrashItems.length), keeping
sessionTrashCount implementation as-is.

In `@web-ui/modules/app.methods.install.mjs`:
- Around line 27-33: normalizeInstallRegistryUrl currently only checks scheme;
update it to reject any registry string containing whitespace or shell
metacharacters by returning '' when the value matches characters like whitespace
or [&|;`$><\\'"], e.g. enforce a stricter regex such as
^https?:\/\/[^\s"'`&|;<>]+$ so only safe URL characters are allowed;
additionally, change appendInstallRegistryOption to always emit the option with
the registry value quoted (--registry="...") and ensure any embedded quotes are
handled/escaped or the value is already rejected by the stricter validator;
apply these changes to the normalizeInstallRegistryUrl and
appendInstallRegistryOption functions.

In `@web-ui/modules/app.methods.openclaw-editing.mjs`:
- Around line 286-288: The saveOpenclawConfigs method currently calls
localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs))
without handling exceptions; wrap that call in a try-catch inside
saveOpenclawConfigs, catch QuotaExceededError/SecurityError (and general
errors), and provide a fallback: log the error (console.error or process
logger), optionally remove or trim older entries or fall back to sessionStorage,
and surface a user-facing notification (e.g., this.notify / alert) indicating
the config could not be saved; ensure you reference saveOpenclawConfigs and
this.openclawConfigs when adding the error handling and fallback flow.

In `@web-ui/modules/app.methods.openclaw-persist.mjs`:
- Around line 161-179: applyOpenclawConfig currently calls
api('apply-openclaw-config') without exception handling; wrap the API call in a
try-catch inside applyOpenclawConfig, await the api call in the try block and
keep the existing res handling there, and in the catch block call
this.showMessage with a clear error (e.g. err.message or a fallback) and the
'error' type so API exceptions surface to the user; mirror the pattern used in
saveAndApplyOpenclawConfig and ensure state updates
(openclawConfigPath/openclawConfigExists) only occur on success.

In `@web-ui/modules/app.methods.providers.mjs`:
- Around line 117-133: The deleteProvider method lacks a try/catch around the
async api('delete-provider', ...) call which can cause unhandled promise
rejections; wrap the API call (and subsequent response handling and await
this.loadAll()) in a try/catch, call this.showMessage(error.message ||
String(error), 'error') inside the catch, and return early on error. Update the
async deleteProvider(name) function to mirror error handling used in
addProvider/updateProvider/resetConfig while keeping checks for
this.isNonDeletableProvider(name) and the existing response-based success
messages.
- Around line 201-223: addModel and removeModel perform async API calls without
try/catch and can produce unhandled promise rejections; wrap the await api(...)
calls in try/catch blocks in both methods (addModel and removeModel), call
this.showMessage with the caught error message (or a generic message if none),
and ensure existing success-path behavior (closeModelModal and await
this.loadAll()) remains in the try branch while errors are handled in the catch
branch so failures don't bubble as unhandled rejections.

In `@web-ui/modules/app.methods.session-actions.mjs`:
- Around line 428-446: The two branches checking
this.sessionTrashCountLoadedOnce and the final else are identical; remove the
duplicate by consolidating them into a single branch so the total-count update
only appears once. Specifically, inside the block that runs when
this.sessionTrashLoadedOnce is false, replace the existing if/else-if/else with
a single conditional that sets this.sessionTrashTotalCount via
this.normalizeSessionTrashTotalCount(res && res.totalCount !== undefined ?
res.totalCount :
(this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount,
this.sessionTrashItems) + 1), this.sessionTrashItems), keeping the
prependSessionTrashItem(this.buildSessionTrashItemFromSession(...)) logic for
the this.sessionTrashLoadedOnce true case and removing the redundant branch that
references this.sessionTrashCountLoadedOnce.

In `@web-ui/modules/app.methods.session-timeline.mjs`:
- Around line 14-20: The ResizeObserver in observeSessionPreviewHeaderResize
currently only calls updateSessionTimelineOffset(), which leaves cached message
tops and the active anchor stale when the header wraps; modify the observer
callback to also trigger a re-measure of the timeline (recompute cached message
tops and refresh the active anchor). Concretely, inside
observeSessionPreviewHeaderResize's ResizeObserver callback call the method that
recalculates message positions/anchors (e.g., measureSessionTimeline(),
recomputeMessageTops(), or refreshActiveAnchor()) — if such a helper does not
exist, add a small method (e.g., remeasureSessionTimeline or
updateSessionMeasurements) that recalculates message top caches and selects the
active anchor, and call it along with updateSessionTimelineOffset().

In `@web-ui/partials/index/layout-header.html`:
- Around line 354-364: The block with class "config-subtabs" is effectively dead
because v-if="false && mainTab === 'config' && !sessionStandalone" will never be
true; either remove the whole div or restore the intended runtime condition
(e.g., drop the leading false) so the subtabs render when mainTab === 'config'
&& !sessionStandalone, and if the block is being intentionally kept for future
use, replace the hard-coded false with a clear TODO comment and/or a feature
flag check so readers know why the onConfigTabClick handlers and configMode
checks exist.

In `@web-ui/partials/index/modals-basic.html`:
- Around line 15-16: The API key input fields currently bind to v-models (e.g.,
newProvider.key, provider.key, newProvider.claudeKey, provider.claudeKey) and
must be changed from default text inputs to password-style secret inputs; update
each <input> for those v-models to include type="password" and add
autocomplete="off" and spellcheck="false" attributes so the keys are masked and
browser helpers are disabled while preserving the existing v-model bindings and
placeholder text.
- Around line 2-24: The modal container for the "Add provider" dialog (the
element with class "modal" shown when showAddModal is true) needs proper dialog
semantics: add role="dialog", aria-modal="true" and
aria-labelledby="<unique-id>" on that <div class="modal"> and give the title
element (the element with class "modal-title") the same id so screen readers can
announce it; update the same pattern for all other modal blocks (the ones using
showAddModal logic equivalents) and ensure handlers like closeAddModal and
addProvider remain unchanged.

In `@web-ui/partials/index/panel-config-claude.html`:
- Around line 54-56: The clickable card rendered by v-for
(claudeConfigs/currentClaudeConfig) must be made keyboard and screen-reader
accessible: add role="button" and tabindex="0" to the card element and wire
`@keydown.enter` and `@keydown.space` to call the same handler used by `@click`
(applyClaudeConfig(name)); ensure the handler signature (applyClaudeConfig) can
accept keyboard events or ignore the event and use the name param. Replace
icon-only buttons' title attributes (the edit and delete buttons) with
aria-label attributes (keep the share button's aria-label for consistency) so
screen readers get descriptive labels, then manually test Tab + Enter/Space and
a screen reader to verify behavior.

In `@web-ui/partials/index/panel-config-openclaw.html`:
- Around line 47-49: Replace the clickable <div> that iterates over
openclawConfigs with a semantic <button> so keyboard users can activate it
(preserve :key="name", :class="['card', { active: currentOpenclawConfig === name
}]" and `@click`="applyOpenclawConfig(name)"); ensure the button uses
type="button" to avoid form submission and retains any existing classes/styles.
Also add meaningful aria-label attributes to the edit and delete icon buttons
(the buttons that call your edit/delete handlers) so screen readers announce
their purpose (e.g., aria-label="Edit OpenClaw config {name}" and
aria-label="Delete OpenClaw config {name}"), and remove reliance on title
attributes for accessibility.

In `@web-ui/partials/index/panel-settings.html`:
- Around line 8-30: The two subtab buttons (ids settings-tab-backup and
settings-tab-trash) currently use a static tabindex="0"; change them to a
dynamic binding so only the active subtab (settingsTab) has tabindex="0" and
inactive ones have tabindex="-1" (e.g., bind tabindex to an expression like
settingsTab === 'backup' ? 0 : -1 and similarly for 'trash'), keeping the
existing :class and `@click` handlers and ensuring the roving-tabindex pattern
used in the main top-tabs (where active tab is focusable and others are -1) is
mirrored here and uses the settingsTab and onSettingsTabClick symbols.

In `@web-ui/source-bundle.cjs`:
- Around line 218-220: readExecutableBundledJavaScriptModule currently forwards
a relative entryPath straight to bundleExecutableJavaScriptFile causing nested
imports to resolve from repo root; update readExecutableBundledJavaScriptModule
to detect relative paths and resolve them against __dirname (e.g., use
path.resolve(__dirname, entryPath) or equivalent) before calling
bundleExecutableJavaScriptFile so module resolution occurs relative to the
web-ui bundle directory.

In `@web-ui/styles/responsive.css`:
- Around line 179-223: The .session-item-copy rule is shrinking the hit target
to 20px and overriding the 44px tap-target floor defined earlier; update the
.session-item-copy block (not the earlier shared rule) to preserve the 44px
touch target by removing or overriding the fixed width/height of 20px and
ensuring min-width/min-height remain 44px (e.g., delete width:20px and
height:20px or set them to min-width/min-height:44px), and if a smaller visual
icon is desired, keep a separate inner element (e.g., .session-item-copy >
.icon) sized to 20px so the visual shrinks but the button box stays 44px.

---

Duplicate comments:
In `@cli.js`:
- Around line 10023-10037: The FSWatcher created by fs.watch (variable watcher)
needs an 'error' listener to prevent uncaught exceptions when a watched path
becomes unavailable; add watcher.on('error', ...) after the watcher is created
(near where watcher is set and watcherEntries.set is called) to handle the error
(log it via your logger or console, close the watcher, and remove its entry from
watcherEntries) and avoid crashing—use existing symbols watcher, watchKey,
watcherEntries, syncDirectoryTree, and trigger to locate and safely cleanup the
watcher on error.
- Around line 10708-10724: The request handler currently checks disk existence
for filePath before consulting PUBLIC_WEB_UI_DYNAMIC_ASSETS, causing bundled
readers to be skipped; move the dynamic asset handling so that after computing
relativePath you first check PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath) and,
if present, write the 200 response using dynamicAsset.mime and
dynamicAsset.reader(filePath) without performing the fs.existsSync/fs.statSync
checks; keep the existing fallback checks for static assets
(PUBLIC_WEB_UI_STATIC_ASSETS.has(relativePath)) and the 404 responses unchanged.
- Around line 10016-10031: The current addWatcher function watches files
directly which breaks when editors do atomic saves (write-to-temp + rename)
because the file watcher can become orphaned; change addWatcher (and
watcherEntries usage) so that when isDirectory is false (watching a file), you
instead watch the parent directory (use path.dirname(target)) and filter events
by the original filename before calling trigger; keep existing behavior for
directory watches (isDirectory true) and ensure the watchKey reflects the actual
watched path (parent dir for files) so duplicate watcher checks still work; also
keep the existing syncDirectoryTree and eventType/extension filtering logic when
dispatching trigger.

---

Nitpick comments:
In `@web-ui/partials/index/panel-sessions.html`:
- Around line 24-25: Remove the redundant v-if check inside the else branch of
the sessionStandalone conditional: in the template where a parent element uses
v-if="sessionStandalone" and its sibling uses v-else, delete the nested
v-if="!sessionStandalone" on the <div class="selector-section"> (the element
referenced by the selector-section class) so the else branch renders
unconditionally without the unnecessary !sessionStandalone evaluation.

In `@web-ui/styles/controls-forms.css`:
- Around line 117-122: Replace the hardcoded background color in the
.health-issue rule with the design-system CSS variable (e.g., change background:
`#fff6f5`; to background: var(--color-error-bg)); update the .health-issue
selector in the CSS so it uses the theme variable (or an existing error
background variable) to maintain consistency with other components and avoid
hardcoded colors.

In `@web-ui/styles/feedback.css`:
- Around line 103-108: The trailing comment "内容区域包裹器 - 稳定高度" in the CSS is a
placeholder with no implementation; either remove that orphan comment line or
turn it into an explicit TODO marker (e.g., add "/* TODO: implement
stable-height wrapper */") so intent is clear to future readers; locate the
comment near the .mode-content rule in feedback.css and update it accordingly.
🪄 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: 6047418d-8a0b-4c63-a75c-1a91409eb443

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and 26d3eeb.

📒 Files selected for processing (75)
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
🪛 GitHub Actions: ci
tests/unit/run.mjs

[error] 1-1: Test run failed with ENOENT: no such file or directory, open '/tmp/codexmate-head-web-ui-3jYhNd/modules/app.constants.mjs' (exit code 1).

tests/unit/web-ui-behavior-parity.test.mjs

[error] 9-9: Unit test failed while capturing head bundled app options; caused by missing file '/tmp/codexmate-head-web-ui-QQiqaq/modules/app.constants.mjs' (propagated ENOENT).

web-ui/source-bundle.cjs

[error] 16-62: source-bundle.cjs failed reading missing module file 'app.constants.mjs' from /tmp/codexmate-head-web-ui-3jYhNd/modules/. Error: ENOENT (openSync/readFileSync).

🪛 HTMLHint (1.9.2)
web-ui/partials/index/layout-footer.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 10-10: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 12-12: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 69-69: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/modal-confirm-toast.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 27-27: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/modals-basic.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-openclaw.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/layout-header.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-claude.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-settings.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-codex.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-market.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-sessions.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-config-template-agents.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-skills.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-openclaw-config.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

🔇 Additional comments (84)
tests/unit/claude-settings-sync.test.mjs (1)

2-4: Bundled source helper adoption looks correct.

Switching this test to readBundledWebUiScript() keeps assertions aligned with the served asset shape and avoids path-coupled reads.

tests/unit/session-trash-state.test.mjs (2)

3-13: Shared bundled-source setup is solid.

This keeps test inputs consistent with bundled Web UI artifacts and reduces drift from runtime-delivered assets.


663-664: Updated test-time constant bindings look consistent.

The injected names are now aligned across these call sites, which helps keep the function instantiation surface coherent.

Also applies to: 763-764, 1742-1744, 1909-1910

tests/e2e/run.js (1)

26-27: Good E2E expansion point.

Adding testWebUiAssets here keeps coverage in the existing execution flow while preserving the established cleanup path.

Also applies to: 122-123

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

1-13: Computed composition module looks clean.

The consolidated factory is clear and keeps the computed sources modular while exposing a single assembly point.

tests/unit/agents-diff-ui.test.mjs (1)

2-6: Bundled artifact test sourcing is correct here.

These updates keep the assertions tied to the same UI assets that are actually served, which improves regression fidelity.

Also applies to: 9-10, 30-31, 53-54

tests/unit/session-tab-switch-performance.test.mjs (1)

4-7: Looks good: assertions now target bundled assets.

This keeps the performance/timeline checks aligned with the shipped UI bundle instead of raw source files.

Also applies to: 347-348, 353-354

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

2-5: Bundled CSS/script readers are the right source for these UI assertions.

Nice update to keep compact-layout checks tied to runtime-equivalent assets.

Also applies to: 8-9, 18-19, 41-42

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

1-69: LGTM - Vue partial template is correctly structured.

The HTMLHint errors (missing doctype, unpaired tags) are false positives since this is a partial fragment designed to be composed with other partials into a complete HTML document. The Vue directives, accessibility attributes, and dynamic bindings are properly implemented.

tests/unit/skills-modal-ui.test.mjs (1)

2-7: LGTM - Test imports correctly updated to use bundled asset readers.

The migration to readBundledWebUiHtml(), readBundledWebUiCss(), and readBundledWebUiScript() aligns with the new bundling infrastructure while preserving readProjectFile() for verifying individual module implementation details.

web-ui/partials/index/modal-confirm-toast.html (1)

1-27: LGTM - Confirmation modal and toast partial are well-structured.

The HTMLHint errors are false positives for this Vue partial. The accessibility implementation is solid with proper role="alertdialog", aria-modal, and conditional aria-labelledby/aria-label handling. The overlay click-to-dismiss pattern and dynamic button states are correctly implemented.

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

1-35: LGTM - Test helper module correctly bridges ESM tests to CJS bundled assets.

The path resolution logic is correct (tests/unit/helpers → project root via three parent traversals), and the wrapper functions properly delegate to source-bundle.cjs methods with fully resolved paths.

web-ui/styles/feedback.css (1)

1-101: LGTM - Feedback styles are well-structured.

The toast positioning, animations, state messages, and [v-cloak] rule are correctly implemented. The slideDown keyframe properly maintains the centered translateX(-50%) transform.

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

244-304: LGTM - Fallback watcher test correctly validates non-recursive watch behavior.

The test properly mocks fs.watch to throw on recursive mode, verifies that fallback watchers are registered for the root and all nested frontend directories, and asserts the expected order and flags.


306-378: LGTM - Rescan test correctly validates dynamic watcher attachment.

The test appropriately simulates a rename event creating a new nested directory and verifies that a new watcher is attached without closing existing watchers.


380-455: LGTM - Reattach test correctly validates watcher lifecycle on directory recreation.

The test properly simulates directory deletion (closes old watcher) followed by recreation (attaches new watcher), verifying both the close callback and the expected watcher registration sequence.

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

40-86: LGTM - Method composition factory is well-organized.

The createAppMethods() factory cleanly aggregates methods from all domain-specific factories with explicit dependency injection. This pattern maintains separation of concerns while providing a single entry point for the Vue app's methods.

tests/unit/provider-share-command.test.mjs (1)

526-617: Good regression coverage for preserveLoading.

These tests pin both the in-flight apply queue and the background loadAll({ preserveLoading: true }) refresh path, which matches the no-flicker UX this PR is protecting.

Also applies to: 619-758

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

2-7: Nice shift to bundled asset readers.

Pointing these assertions at bundled HTML/CSS/JS keeps the checks aligned with the artifact the embedded server actually serves.

Also applies to: 10-10, 139-140, 283-283, 328-328

cli.js (1)

10696-10700: Bundled HTML serving path looks correct.

Serving readBundledWebUiHtml(...) for /web-ui and the final fallback keeps runtime behavior consistent when source templates are unavailable.

Also applies to: 10792-10794

tests/unit/helpers/web-ui-app-options.mjs (2)

15-22: HEAD_WEB_UI_FILES is incomplete for bundling the app module graph.

This issue was previously identified. The list is missing modules that app.js imports (e.g., app.computed.index.mjs, app.methods.index.mjs, and their transitive dependencies). Without these files, createHeadWebUiFixture will produce an incomplete fixture that cannot properly bundle and execute.


40-53: LGTM - Clean global override pattern.

The withGlobalOverrides function correctly manages global state with proper cleanup in the finally block, ensuring globals are restored even if the callback throws.

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

1-298: LGTM!

The stylesheet is well-structured with consistent use of CSS custom properties, appropriate vendor prefixes for scrollbar styling, and proper cascade organization for button variants and interactive states.

web-ui/styles/sessions-preview.css (1)

1-405: LGTM!

The session preview stylesheet correctly implements responsive layouts with appropriate breakpoints, properly styled message bubbles with role variants, and includes performance optimizations via CSS containment. The backdrop-filter with webkit prefix ensures cross-browser compatibility.

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

42-106: Well-implemented accessible tab navigation.

The tab navigation correctly implements ARIA patterns with proper role="tab", dynamic aria-selected, aria-controls references, and roving tabindex management.

web-ui/styles/sessions-list.css (1)

1-407: LGTM!

The stylesheet correctly implements session list styling with appropriate performance optimizations (content-visibility: auto, contain-intrinsic-size), proper sticky positioning, and consistent hover/active/pinned state variations.

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

1-173: LGTM!

The market panel template correctly implements:

  • Proper ARIA attributes for accessibility (role="tabpanel", aria-pressed on toggle buttons)
  • Consistent disabled state management across all interactive elements
  • Appropriate loading/empty/list state handling
  • Unique :key bindings for all v-for iterations
web-ui/styles/modals-core.css (1)

1-429: LGTM!

The modal stylesheet is well-organized with proper layering (overlay → modal → variants), accessible form controls with clear focus states, and cross-browser compatible custom select styling using properly encoded SVG data URIs.

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

32-71: LGTM - Backup panel implementation.

The backup/import flow correctly:

  • Disables buttons during loading states
  • Uses hidden file inputs with proper accept=".zip" constraint
  • Shows progress percentage during downloads

73-139: LGTM - Trash panel implementation.

The trash panel correctly handles:

  • Multiple view states (loading/empty/retry/list)
  • Per-item action busy states via isSessionTrashActionBusy(item)
  • Pagination with "load more" pattern
  • Proper disabled state propagation during global operations
web-ui/modules/app.methods.session-browser.mjs (3)

87-119: Shared request sequence can cause cross-source race invalidation.

sessionPathRequestSeq is shared across all sources (codex, claude, all). If a user quickly toggles between sources, a request for one source can invalidate the in-flight request for another source, potentially leaving that source with stale or empty path options.


370-412: Stale standalone loads can apply after session is cleared.

The early return (Lines 371-377) does not increment sessionStandaloneRequestSeq, allowing an older in-flight session-plain response to still apply after the selection was cleared. Additionally, Lines 399-400 dereference this.activeSession after the async call without checking if it's still valid, which could cause issues if the session was cleared during the await.


1-419: Session browser methods are well-structured overall.

The module correctly implements session browsing functionality with proper normalization helpers, localStorage persistence with JSON parse recovery, pin management with timestamp validation, and appropriate delegation to injected helpers. The identified race conditions from previous reviews should be addressed separately.

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

1-98: LGTM!

The test suite comprehensively validates bundling behavior: BOM stripping, directive inlining, module deduplication, and executable bundle integrity. The temporary file handling with proper cleanup in finally blocks and cache-busting for dynamic imports are correctly implemented.

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

1-124: LGTM!

The E2E test suite provides solid coverage for asset serving semantics, content-type validation, bundling verification, private asset blocking, and path traversal protection. The HTTP helper correctly handles timeouts with req.destroy().

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

1-248: LGTM!

The Vue template correctly implements the provider configuration panel with appropriate conditional rendering, event handling with @click.stop for action isolation, and comprehensive disabled-state management. The static analysis warning about doctype is a false positive since this is an HTML partial that will be inlined into a complete document.

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

1-382: LGTM!

The stylesheet provides comprehensive navigation and panel styling with consistent use of CSS custom properties, appropriate responsive patterns, and correct sticky positioning for the inspector column.

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

1-285: Overall implementation is solid.

The sessions panel correctly implements complex state management with appropriate Vue patterns: v-memo for render optimization, proper key composition for v-for loops, and correct aria attributes for accessibility. The static analysis doctype warning is a false positive for this HTML partial.

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

1-113: LGTM!

The computed property factory correctly implements dashboard state derivations with appropriate defensive checks (Array.isArray, typeof, trim), proper handling of fallback values, and clear precedence logic for status indicators.

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

1-43: LGTM: Session trash item builder with robust normalization.

The function handles various edge cases including missing fields, type coercion, and fallback values appropriately.


228-266: LGTM: Request guarding pattern prevents stale responses.

The token-based invalidation combined with pending-options coalescing correctly handles concurrent requests and ensures only the latest response updates state.


384-388: No actionable issue found. sessionTrashCount at line 385 is a computed property (defined in web-ui/modules/app.computed.session.mjs) that normalizes the raw state property sessionTrashTotalCount. Using the computed property in clearSessionTrash() is correct and intentional.

			> Likely an incorrect or invalid review comment.
web-ui/modules/app.methods.providers.mjs (1)

39-73: LGTM: Provider pill state helpers are well-structured.

The helpers correctly determine provider configuration state and return appropriate labels for local vs configured providers.

web-ui/logic.runtime.mjs (2)

66-94: LGTM: Latest-only queue implementation handles edge cases correctly.

The break condition on line 84 (!queued || queued === currentTarget) prevents infinite loops while ensuring only the latest queued target is processed. Error capture and return structure are appropriate.


96-122: LGTM: Compact layout detection with reasonable heuristics.

The function properly combines viewport, screen, touch, and UA signals to determine when compact layout should be forced, with sensible fallback behavior for non-mobile devices.

tests/unit/provider-switch-regression.test.mjs (2)

75-128: LGTM: Test correctly validates "latest-only" provider switch semantics.

The test uses Promise gating effectively to verify that rapid successive switches result in only the final provider being applied, while the UI immediately reflects the latest target.


130-158: LGTM: Test validates immediate UI feedback pattern.

The test confirms that providerSwitchDisplayTarget is set immediately while currentProvider remains unchanged until the idle gate releases, ensuring responsive UI during async operations.

web-ui/styles/sessions-toolbar-trash.css (1)

1-244: LGTM: Consistent CSS styling with design system tokens.

The stylesheet properly uses CSS custom properties for theming, includes appropriate hover/focus/disabled states for interactive elements, and maintains visual consistency across components.

web-ui/partials/index/modal-openclaw-config.html (1)

1-274: LGTM: Well-structured modal with comprehensive OpenClaw configuration UI.

The template provides intuitive quick-setup and advanced structured editing modes, with appropriate loading/disabled states on buttons. The HTMLHint doctype-first warning is a false positive since this is a partial template intended for inclusion in a parent document.

web-ui/partials/index/modal-skills.html (2)

161-175: LGTM: Comprehensive busy state guards on all action buttons.

The buttons correctly combine multiple busy flags to prevent concurrent operations that could cause race conditions or data corruption during import/export/delete workflows.


1-185: LGTM: Skills modal with proper accessibility attributes.

The template includes appropriate ARIA attributes (role="group", aria-label, aria-pressed) for the target switch, enhancing keyboard navigation and screen reader support.

web-ui/partials/index/modal-config-template-agents.html (2)

57-106: LGTM: Diff preview with comprehensive state handling.

The template correctly handles all diff states (loading, error, truncated, no changes, has changes) with appropriate UI feedback and conditional rendering.


110-124: LGTM: Two-step confirmation flow with proper button state management.

The footer buttons correctly enforce the confirm-then-apply workflow: the apply button is disabled when in preview mode with no changes, and the "返回编辑" button only appears in preview mode.

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

8-9: Pipeline failure: missing module dependencies for HEAD fixture staging.

The CI failure indicates that captureHeadBundledAppOptions() cannot find /tmp/codexmate-head-web-ui-.../modules/app.constants.mjs. This confirms the fixture staging logic doesn't include all required module files from web-ui/modules/.


318-335: Well-structured parity test for Vue app skeleton.

The test properly validates that data(), methods, computed, mounted, and beforeUnmount structures match between current and HEAD versions, with allowance for the new providerSwitchDisplayTarget key.

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

276-283: Solid fallback pattern for scheduleAfterFrame.

The implementation correctly uses requestAnimationFrame when available and falls back to setTimeout with 16ms (approximately one frame at 60fps). This ensures consistent behavior across environments.


150-169: Correct touch vs mouse/pen pointer handling.

The early return for touch pointers (lines 164-166) defers navigation to the click handler, avoiding double-switching issues on touch devices while allowing immediate feedback for mouse/pen interactions.

web-ui/styles/controls-forms.css (1)

1-14: LGTM: Selector section styling.

The gradient background, border radius, and shadow create a clean card-like appearance with consistent use of design tokens.

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

3-103: Well-structured config application flow.

The applyOpenclawStructuredToText method properly validates each field incrementally, returning early on validation failures with user-friendly error messages. The nested object traversal with null checks is defensive and correct.

web-ui/logic.claude.mjs (2)

44-71: Correct credential matching precedence in matchClaudeConfigFromSettings.

The logic properly prioritizes exact apiKey match (lines 60-62), then falls back to externalCredentialType match only when settings lack an apiKey and config also lacks one (lines 63-68). This prevents credential type confusion.


34-38: URL normalization handles trailing slashes.

The function correctly trims and removes trailing slashes for consistent URL comparison. Note that it doesn't normalize protocol (http vs https) or ports, which appears intentional for exact matching.

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

87-105: LGTM: Card component base styling.

The card styling provides a solid foundation with gradient backgrounds, subtle shadows, and smooth transitions. The will-change: transform hint is appropriate for hover animations.


380-382: The @keyframes spin animation is properly defined. It's located in web-ui/styles/feedback.css at line 94, confirming the .card-action-btn.loading svg animation reference is valid.

web-ui/modules/app.methods.session-actions.mjs (2)

143-153: Correct shell argument quoting.

The quoteShellArg helper properly handles empty strings, safe characters (alphanumeric plus ._-), and escapes single quotes using the '\'' pattern for POSIX shells.


155-176: Appropriate fallback for clipboard operations.

Using document.execCommand('copy') as a fallback for older browsers is the correct pattern. The textarea is properly cleaned up in the finally block, and errors are caught gracefully.

web-ui/modules/app.methods.openclaw-core.mjs (3)

3-14: Runtime JSON5 detection with safe fallback.

The parser selection correctly checks for both parse and stringify functions on window.JSON5 before using it, falling back to native JSON when unavailable. This allows JSON5 to be an optional enhancement.


332-337: Good defensive masking for sensitive values.

The maskProviderValue helper provides consistent masking that shows first/last 3 characters for longer values while fully masking short values. This balances debuggability with security.


477-487: Correct optional number validation.

parseOptionalNumber properly handles empty strings (returning null), validates for finite non-negative numbers, and returns descriptive error messages with the field label.

web-ui/modules/app.methods.codex-config.mjs (3)

76-88: Skip config re-apply when model loading fails.

After loadModelsForProvider(name) fails (setting modelsSource to 'error'), currentProvider is already updated but currentModel may be stale from the previous provider. Lines 84-87 then call applyCodexConfigDirect with this potentially invalid provider/model combination.


228-249: Handle providersList as string array.

If providersList contains provider names as strings (as suggested by other parts of the PR), the filter provider && provider.name will drop all entries since strings don't have a .name property. The speed-test loop would never execute.


12-21: LGTM: Download helper with proper cleanup.

The downloadTextFile method correctly creates a Blob with BOM prefix for UTF-8 encoding, triggers the download via a temporary anchor element, and immediately revokes the object URL to prevent memory leaks.

web-ui/logic.agents-diff.mjs (2)

27-35: Trailing newline-only edits collapsed to "no change."

splitDiffLines() strips the trailing newline before splitting, so both '' and '\n' become []. The agents editor then treats edits that only add/remove a trailing blank line as unchanged and refuses to save them.


297-327: LGTM: Efficient diff strategy selection.

The buildLineDiff function appropriately switches between exact LCS-based diff for smaller inputs and heuristic sync-point matching for large files (>3000 lines), with proper stats aggregation and context compaction.

web-ui/modules/app.methods.runtime.mjs (2)

188-221: Guard importBackupFile against re-entry.

claudeImportLoading / codexImportLoading is set but never checked before proceeding. Re-selecting a file while a restore is in progress can fire overlapping restore-*-dir writes against the same config directory.


74-114: LGTM: Download methods with proper concurrency guards.

Both downloadClaudeDirectory and downloadCodexDirectory correctly check their loading flags at the start and return early if already in progress. The progress ramp via setInterval with cleanup in finally is properly managed.

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

1-1: External Google Fonts import creates runtime dependency.

The @import from Google Fonts adds a hard runtime dependency on an external service. Offline installs, restrictive CSPs, or privacy-sensitive environments will generate a failing third-party request on every load. The fallback font stacks on lines 33-35 will apply, but the request is still made.


368-373: Good: Reduced motion preference respected.

The @media (prefers-reduced-motion: reduce) query properly disables the FAB pulse animation and transitions for users who have indicated a preference for reduced motion.

web-ui/modules/app.methods.startup-claude.mjs (3)

24-75: Treat api('list') failures as startup failures.

loadAll() only checks statusRes.error. If api('list') returns { error: ... }, the code still enters the success path and sets providersList = listRes.providers, which would be undefined. Downstream code expecting an array would fail.


185-204: LGTM: Config import with duplicate detection.

ensureClaudeConfigFromSettings properly validates required fields, checks for existing duplicates, and generates unique sequential names when the preferred name is taken. The persistence via saveClaudeConfigs() is correctly triggered after mutation.


274-330: LGTM: Claude model loading with external credential awareness.

loadClaudeModels correctly handles the edge case where externalCredentialType is set but apiKey is missing—treating it as an "unlimited" source rather than an error. This preserves the display-only behavior for externally authenticated configs.

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

1-167: LGTM!

The Claude config methods factory is well-structured with proper validation, duplicate detection, and graceful handling of missing API keys (including externalCredentialType awareness). The confirmation dialog pattern for deletion and the "at least one config" guard are good defensive measures.

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

1-160: LGTM: Well-structured persistence methods.

The OpenClaw persistence factory properly handles modal state, content loading with fallback templates, validation before persistence, and confirmation dialogs for destructive operations.

web-ui/modules/app.methods.agents.mjs (2)

243-338: LGTM: Robust diff preview with staleness protection.

The prepareAgentsDiff implementation properly guards against stale responses using request tokens and fingerprints. The fallback to local diff generation when payload exceeds limits is a good resilience pattern.


371-424: LGTM: Safe apply flow with fingerprint validation.

The applyAgentsContent method correctly validates the diff state is current before applying, re-triggering preview if fingerprints don't match. The context-aware action selection (apply-agents-file, apply-openclaw-agents-file, apply-openclaw-workspace-file) is well-structured.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 4, 2026

@coderabbitai re-review
Stop making breaking changes, do a proper review!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

@github-actions[bot]: Understood. I sincerely apologize for continuing to introduce breaking changes — this is unacceptable. I am triggering a new full review now with a strict constraint:

  • Correctness issues only
  • Safety issues only
  • Quality issues only
  • ❌ No refactoring suggestions
  • ❌ No API/interface changes
  • ❌ No behavior alterations
  • ❌ No breaking changes of any kind

Review strictly limited to correctness, safety, and quality within the existing design. No breaking changes, no refactoring, no API/interface/behavior alterations. This constraint has been reinforced across PRs #55, #59, #67, and #68.


🧠 Learnings used
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
✅ Actions performed

Full review triggered.

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 13

♻️ Duplicate comments (20)
web-ui/logic.sessions.mjs (1)

27-49: ⚠️ Potential issue | 🟡 Minor

Return normalized filters from buildSessionListParams().

This issue was flagged in a previous review. Lines 38-39 return the raw source and pathFilter values, but Line 36 already uses isSessionQueryEnabled(source) which relies on normalized semantics internally. This inconsistency means values like ' Claude ' (with whitespace) will pass the query check but be sent to the backend unnormalized.

🛠️ Suggested fix
 export function buildSessionListParams(options = {}) {
     const {
         source = 'all',
         pathFilter = '',
         query = '',
         roleFilter = 'all',
         timeRangePreset = 'all',
         limit = 200
     } = options;
-    const queryValue = isSessionQueryEnabled(source) ? query : '';
+    const normalizedSource = normalizeSessionSource(source, 'all');
+    const normalizedPathFilter = normalizeSessionPathFilter(pathFilter);
+    const queryValue = isSessionQueryEnabled(normalizedSource) ? query : '';
     return {
-        source,
-        pathFilter,
+        source: normalizedSource,
+        pathFilter: normalizedPathFilter,
         query: queryValue,
         queryMode: 'and',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.sessions.mjs` around lines 27 - 49, Normalize the inputs before
using or returning them: in buildSessionListParams create a normalizedSource
(e.g., const normalizedSource = source.trim()) and a normalizedPathFilter (e.g.,
pathFilter.trim()), call isSessionQueryEnabled(normalizedSource) instead of
isSessionQueryEnabled(source), use normalizedSource and normalizedPathFilter in
the returned object (and for computing queryValue), so inputs like " Claude "
are trimmed/normalized consistently across checks and the payload sent to the
backend.
web-ui/styles/base-theme.css (1)

1-1: ⚠️ Potential issue | 🟠 Major

External font import still introduces a runtime third-party dependency.

Line 1 performs a Google Fonts fetch at runtime, which can fail under offline installs, strict CSPs, or privacy-restricted environments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/base-theme.css` at line 1, The external Google Fonts import at
the top of base-theme.css (`@import` url('https://fonts.googleapis.com/...'))
introduces a runtime third-party dependency; replace it by bundling the font
files locally and updating base-theme.css to reference local font-face
declarations (or add robust font-family fallbacks) so the app no longer fetches
from Google at runtime and works offline/under strict CSPs—locate the `@import`
line in base-theme.css and swap it for local `@font-face` references and fallback
families.
web-ui/modules/app.methods.install.mjs (1)

27-34: ⚠️ Potential issue | 🔴 Critical

Registry value is still shell-injectable in generated install commands.

Line 61 appends registry unquoted, while Line 27-33 accepts arbitrary characters after http(s)://. A crafted custom registry can inject extra shell tokens when users paste the command.

🔒 Minimal hardening patch
         normalizeInstallRegistryUrl(value) {
             const normalized = typeof value === 'string' ? value.trim() : '';
             if (!normalized) return '';
-            if (!/^https?:\/\//i.test(normalized)) {
+            if (!/^https?:\/\/[^\s"'`&|;<>\\$]+$/i.test(normalized)) {
                 return '';
             }
             return normalized.replace(/\/+$/, '');
         },
@@
             const registry = this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
             if (!registry) {
                 return base;
             }
-            return `${base} --registry=${registry}`;
+            return `${base} --registry="${registry}"`;
         },

Also applies to: 50-62

cli.js (3)

10708-10724: ⚠️ Potential issue | 🟠 Major

Bundled dynamic assets are still blocked when source files are absent.

Line 10709 returns 404 before checking PUBLIC_WEB_UI_DYNAMIC_ASSETS. This prevents bundled readers from serving /web-ui/app.js, /web-ui/index.html, etc. in source-less runs.

Proposed non-breaking fix
             const relativePath = path.relative(webDir, filePath).replace(/\\/g, '/');
-            if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
-                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
-                res.end('Not Found');
-                return;
-            }
             const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath);
             if (dynamicAsset) {
                 res.writeHead(200, { 'Content-Type': dynamicAsset.mime });
                 res.end(dynamicAsset.reader(filePath), 'utf-8');
                 return;
             }
             if (!PUBLIC_WEB_UI_STATIC_ASSETS.has(relativePath)) {
                 res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
                 res.end('Not Found');
                 return;
             }
+            if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
+                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
+                res.end('Not Found');
+                return;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10708 - 10724, The code checks the filesystem existence
of filePath before consulting PUBLIC_WEB_UI_DYNAMIC_ASSETS, which causes
requests for bundled dynamic assets to 404 when source files are missing; update
the handler to first compute relativePath and then check
PUBLIC_WEB_UI_DYNAMIC_ASSETS (using
PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath) and
dynamicAsset.reader(filePath)) before performing fs.existsSync/fs.statSync
checks, so that if a dynamicAsset is present you serve it (res.writeHead +
res.end with dynamicAsset.mime and dynamicAsset.reader) without hitting the 404
branch, and only fall back to filesystem existence and
PUBLIC_WEB_UI_STATIC_ASSETS afterward.

10016-10031: ⚠️ Potential issue | 🟠 Major

File-level watchers can miss edits after atomic-save rename patterns.

addWatcher watches file targets directly. Editors that save via temp-file + rename can replace the inode, after which subsequent edits may no longer trigger restart events.

Proposed non-breaking fix
-    const addWatcher = (target, recursive, isDirectory = false) => {
+    const addWatcher = (target, recursive, isDirectory = false) => {
         if (!fs.existsSync(target)) return;
-        const watchKey = `${recursive ? 'recursive' : 'plain'}:${target}`;
+        const watchTarget = isDirectory ? target : path.dirname(target);
+        const watchedBaseName = isDirectory ? '' : path.basename(target);
+        const watchKey = `${recursive ? 'recursive' : 'plain'}:${watchTarget}:${watchedBaseName}`;
         if (watcherEntries.has(watchKey)) {
             return true;
         }
         try {
-            const watcher = fs.watch(target, { recursive }, (eventType, filename) => {
+            const watcher = fs.watch(watchTarget, { recursive }, (eventType, filename) => {
+                const normalizedFilename = typeof filename === 'string'
+                    ? filename
+                    : (filename ? String(filename) : '');
+                if (!isDirectory) {
+                    if (!normalizedFilename) return;
+                    if (path.basename(normalizedFilename) !== watchedBaseName) return;
+                }
                 if (isDirectory && !recursive && eventType === 'rename') {
                     syncDirectoryTree(target);
                 }
-                if (!filename) return;
-                const lower = filename.toLowerCase();
+                if (!normalizedFilename) return;
+                const lower = normalizedFilename.toLowerCase();
                 if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
-                trigger({ target, eventType, filename });
+                trigger({ target, eventType, filename: normalizedFilename });
             });
             watcherEntries.set(watchKey, {
                 watcher,
                 target,
                 recursive,
                 isDirectory
             });

Also applies to: 10078-10081

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10016 - 10031, addWatcher currently attaches fs.watch
directly to file targets which can miss edits when editors save by creating a
temp file and renaming (inode replacement); update addWatcher to handle
rename/inode-replacement by re-establishing watchers for file targets: detect
when the fs.watch callback receives eventType 'rename' for a non-directory
target (or when the watcher emits 'error'/'close'), then check
fs.existsSync(target) after a short debounce and call addWatcher(target,
recursive, isDirectory) to recreate the watcher (or alternatively watch the
parent directory for rename events), and ensure watcherEntries is updated
accordingly; use the existing addWatcher, watcherEntries, syncDirectoryTree and
trigger symbols to locate where to add this logic.

10023-10037: ⚠️ Potential issue | 🟠 Major

Handle FSWatcher errors to avoid unexpected process termination.

fs.watch() at Line 10023 has no local 'error' listener. When watched paths become unavailable, unhandled watcher errors can crash the dev process.

Proposed non-breaking fix
         try {
             const watcher = fs.watch(target, { recursive }, (eventType, filename) => {
                 if (isDirectory && !recursive && eventType === 'rename') {
                     syncDirectoryTree(target);
                 }
                 if (!filename) return;
                 const lower = filename.toLowerCase();
                 if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
                 trigger({ target, eventType, filename });
             });
+            watcher.on('error', () => {
+                try { watcher.close(); } catch (_) {}
+                watcherEntries.delete(watchKey);
+                if (isDirectory) {
+                    syncDirectoryTree(target);
+                }
+            });
             watcherEntries.set(watchKey, {
                 watcher,
                 target,
                 recursive,
                 isDirectory
             });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10023 - 10037, The fs.watch() watcher created in this
block (variable watcher) lacks an 'error' event handler; add a listener on
watcher.on('error', ...) that gracefully handles errors by logging the error (or
forwarding to the existing logger), cleaning up state (close the watcher and
remove the entry from watcherEntries by key watchKey), and avoiding process.exit
so the dev process won't crash; ensure you still call any necessary cleanup
functions like syncDirectoryTree or trigger if appropriate and reference
watcher, watcherEntries, watchKey, syncDirectoryTree and trigger when making the
change.
web-ui/partials/index/panel-settings.html (1)

9-29: ⚠️ Potential issue | 🟡 Minor

Keep only the active settings subtab in the tab order.

Both buttons still use tabindex="0", so keyboard users can tab into inactive subtabs as well. Bind tabindex to settingsTab so only the selected tab is focusable.

Minimal fix
                         <button
                             id="settings-tab-backup"
                             role="tab"
                             aria-controls="settings-panel-backup"
                             :aria-selected="settingsTab === 'backup'"
-                            tabindex="0"
+                            :tabindex="settingsTab === 'backup' ? 0 : -1"
                             :class="['config-subtab', { active: settingsTab === 'backup' }]"
                             `@click`="onSettingsTabClick('backup')">
                             备份与导入
                         </button>
                         <button
                             id="settings-tab-trash"
                             role="tab"
                             aria-controls="settings-panel-trash"
                             :aria-selected="settingsTab === 'trash'"
-                            tabindex="0"
+                            :tabindex="settingsTab === 'trash' ? 0 : -1"
                             :class="['config-subtab', { active: settingsTab === 'trash' }]"
                             `@click`="onSettingsTabClick('trash')">
                             回收站
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-settings.html` around lines 9 - 29, Update the
two tab buttons so only the active subtab is focusable by binding tabindex to
settingsTab: replace the static tabindex="0" on the elements with a bound
expression that returns 0 when settingsTab matches the button's key and -1
otherwise (e.g., for the backup button use settingsTab === 'backup' ? 0 : -1 and
similarly for the trash button use settingsTab === 'trash' ? 0 : -1); locate the
buttons by their ids (settings-tab-backup, settings-tab-trash) and ensure all
other attributes (role, aria-controls, :aria-selected, `@click` handler
onSettingsTabClick, and the settings-tab-badge using sessionTrashCount) remain
unchanged.
web-ui/modules/app.computed.session.mjs (1)

111-124: ⚠️ Potential issue | 🟠 Major

Trash pagination still stops at the loaded page boundary.

These computed values still compare against sessionTrashItems.length, so once the loaded slice is fully visible the UI hides “加载更多” even when sessionTrashCount says more items exist on the server.

Minimal fix
         sessionTrashHasMoreItems() {
-            const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
-            return this.visibleSessionTrashItems.length < totalItems;
+            return this.visibleSessionTrashItems.length < this.sessionTrashCount;
         },
         sessionTrashHiddenCount() {
-            const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
-            return Math.max(0, totalItems - this.visibleSessionTrashItems.length);
+            return Math.max(0, this.sessionTrashCount - this.visibleSessionTrashItems.length);
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.computed.session.mjs` around lines 111 - 124,
sessionTrashHasMoreItems and sessionTrashHiddenCount are still using
this.sessionTrashItems.length which makes pagination stop at the loaded slice;
change both to derive totalItems from the same parsed totalCount logic used in
sessionTrashCount (parse Number(this.sessionTrashTotalCount), ensure
Number.isFinite and >=0, use Math.floor(totalCount) as totalItems) and only fall
back to this.sessionTrashItems.length when totalCount is invalid so that hasMore
and hiddenCount reflect the server-reported total; update the functions
sessionTrashHasMoreItems, sessionTrashHiddenCount to use that totalItems logic
consistently with sessionTrashCount.
web-ui/modules/app.methods.providers.mjs (2)

117-133: ⚠️ Potential issue | 🟡 Minor

Handle rejected deletes the same way as the other provider mutations.

A transport failure from api('delete-provider') or loadAll() escapes this handler today, so the user gets an unhandled rejection instead of an error message.

Minimal fix
         async deleteProvider(name) {
             if (this.isNonDeletableProvider(name)) {
                 this.showMessage('该 provider 为保留项,不可删除', 'info');
                 return;
             }
-            const res = await api('delete-provider', { name });
-            if (res.error) {
-                this.showMessage(res.error, 'error');
-                return;
-            }
-            if (res.switched && res.provider) {
-                this.showMessage(`已删除提供商,自动切换到 ${res.provider}${res.model ? ` / ${res.model}` : ''}`, 'success');
-            } else {
-                this.showMessage('操作成功', 'success');
-            }
-            await this.loadAll();
+            try {
+                const res = await api('delete-provider', { name });
+                if (res.error) {
+                    this.showMessage(res.error, 'error');
+                    return;
+                }
+                if (res.switched && res.provider) {
+                    this.showMessage(`已删除提供商,自动切换到 ${res.provider}${res.model ? ` / ${res.model}` : ''}`, 'success');
+                } else {
+                    this.showMessage('操作成功', 'success');
+                }
+                await this.loadAll();
+            } catch (e) {
+                this.showMessage('删除失败', 'error');
+            }
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 117 - 133, The
deleteProvider method currently lets transport failures from
api('delete-provider') or this.loadAll() bubble up; wrap the async body of
deleteProvider in a try/catch (or add try/catch around the await api(...) and
await this.loadAll()) so any thrown errors are caught, call
this.showMessage(error.message || String(error), 'error') in the catch and
return; ensure existing non-deletable check and normal success flow remain
unchanged and reference the deleteProvider function, the api('delete-provider')
call, and this.loadAll() when applying the fix.

201-223: ⚠️ Potential issue | 🟡 Minor

Catch API failures in the model add/remove actions.

Both handlers still let rejected api(...) or loadAll() promises bubble out, which turns routine network failures into unhandled rejections.

Minimal fix
         async addModel() {
             if (!this.newModelName || !this.newModelName.trim()) {
                 return this.showMessage('请输入模型', 'error');
             }
-            const res = await api('add-model', { model: this.newModelName.trim() });
-            if (res.error) {
-                this.showMessage(res.error, 'error');
-            } else {
-                this.showMessage('操作成功', 'success');
-                this.closeModelModal();
-                await this.loadAll();
+            try {
+                const res = await api('add-model', { model: this.newModelName.trim() });
+                if (res.error) {
+                    this.showMessage(res.error, 'error');
+                } else {
+                    this.showMessage('操作成功', 'success');
+                    this.closeModelModal();
+                    await this.loadAll();
+                }
+            } catch (e) {
+                this.showMessage('添加模型失败', 'error');
             }
         },
 
         async removeModel(model) {
-            const res = await api('delete-model', { model });
-            if (res.error) {
-                this.showMessage(res.error, 'error');
-            } else {
-                this.showMessage('操作成功', 'success');
-                await this.loadAll();
+            try {
+                const res = await api('delete-model', { model });
+                if (res.error) {
+                    this.showMessage(res.error, 'error');
+                } else {
+                    this.showMessage('操作成功', 'success');
+                    await this.loadAll();
+                }
+            } catch (e) {
+                this.showMessage('删除模型失败', 'error');
             }
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 201 - 223, Wrap the
async bodies of addModel and removeModel in try/catch blocks to catch rejected
promises from api(...) and await this.loadAll(), and on error call
this.showMessage with a descriptive message (e.g., error.message or a fallback
string) and type 'error'; ensure successful paths still close the modal/refresh
state (addModel should still call this.closeModelModal() and both should await
this.loadAll() inside the try), so unhandled rejections are prevented and errors
are surfaced to the user.
tests/unit/web-ui-behavior-parity.test.mjs (1)

8-9: ⚠️ Potential issue | 🔴 Critical

HEAD parity bootstrap is still staging an incomplete Web UI fixture.

captureHeadBundledAppOptions() still aborts this file before any test runs because the staged HEAD copy is missing modules/app.constants.mjs. Please make that helper copy the full transitive web-ui/modules/ dependency set, or just stage the whole web-ui/modules/ directory, before evaluating app.js.

#!/bin/bash
set -euo pipefail

helper="$(fd -t f 'web-ui-app-options\\.mjs$' tests/unit/helpers | head -n1)"
source_helper="$(fd -t f 'web-ui-source\\.mjs$' tests/unit/helpers | head -n1)"

echo "== ${helper} =="
sed -n '1,260p' "$helper"

echo
echo "== ${source_helper} =="
sed -n '1,260p' "$source_helper"

echo
echo "== HEAD staging references =="
rg -n -C2 'HEAD_WEB_UI_FILES|captureHeadBundledAppOptions|modules/' tests/unit/helpers web-ui
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/web-ui-behavior-parity.test.mjs` around lines 8 - 9,
captureHeadBundledAppOptions currently stages an incomplete Web UI fixture
(missing modules/app.constants.mjs) and aborts before tests run; update the
helper so it copies the full transitive web-ui/modules dependency set (or the
entire web-ui/modules/ directory) into the staged HEAD copy before evaluating
app.js, mirroring what captureCurrentBundledAppOptions does; modify the
helper(s) referenced by web-ui-app-options.mjs / web-ui-source.mjs to perform a
recursive copy of web-ui/modules or resolve and include all imported module
files so that captureHeadBundledAppOptions can successfully evaluate app.js
without missing imports.
web-ui/modules/app.methods.codex-config.mjs (1)

76-87: ⚠️ Potential issue | 🟠 Major

Don't silently re-apply the previous model after a provider load error.

currentProvider is updated before loadModelsForProvider(name) finishes. If that loader returns with this.modelsSource === 'error', Line 86 still pushes the old currentModel back through applyCodexConfigDirect({ silent: true }).

Minimal guard
         async performProviderSwitch(name) {
             await this.waitForCodexApplyIdle();
             this.currentProvider = name;
             await this.loadModelsForProvider(name);
+            if (this.modelsSource === 'error') {
+                return;
+            }
             if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
                 this.currentModel = this.models[0];
                 this.modelsHasCurrent = true;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.codex-config.mjs` around lines 76 - 87,
performProviderSwitch currently sets this.currentProvider before awaiting
loadModelsForProvider(name), which lets a provider-load error leave modelsSource
=== 'error' but still trigger applyCodexConfigDirect and reapply the old
currentModel; to fix, either move the assignment of this.currentProvider until
after await this.loadModelsForProvider(name) succeeds, or add a guard before
changing this.currentModel/this.modelsHasCurrent and before calling
applyCodexConfigDirect: check that this.modelsSource !== 'error' (and that
models contains a valid model) before mutating currentModel or calling
applyCodexConfigDirect from performProviderSwitch; keep references to
performProviderSwitch, loadModelsForProvider, currentProvider, modelsSource,
models, currentModel, modelsHasCurrent, waitForCodexApplyIdle, and
applyCodexConfigDirect to locate the code.
web-ui/modules/app.methods.openclaw-editing.mjs (1)

286-288: ⚠️ Potential issue | 🟡 Minor

Handle storage write failures when saving OpenClaw configs.

Line 287 lets a localStorage.setItem() exception abort the save path. In full or disabled-storage scenarios that turns a normal save into an uncaught UI failure with no user-facing error.

🐛 Minimal fix
         saveOpenclawConfigs() {
-            localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
+            try {
+                localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
+            } catch (e) {
+                this.showMessage('保存配置失败: ' + (e && e.message ? e.message : '存储不可用'), 'error');
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-editing.mjs` around lines 286 - 288,
saveOpenclawConfigs currently calls localStorage.setItem('openclawConfigs',
JSON.stringify(this.openclawConfigs)) without handling storage errors; wrap that
call in a try/catch inside the saveOpenclawConfigs method, catch DOMException or
generic exceptions from localStorage.setItem, log the error via
console/processLogger and surface a user-facing notification (e.g., show a
toast/alert or set an error state) indicating the save failed and optionally
suggest freeing storage or enabling storage; ensure the catch does not rethrow
so the UI flow continues gracefully.
web-ui/modules/app.methods.session-browser.mjs (2)

370-410: ⚠️ Potential issue | 🟠 Major

Invalidate stale standalone loads when the active session is cleared.

The early return on Lines 371-376 does not advance sessionStandaloneRequestSeq, so an older session-plain response can still win after the selection was cleared. The success path on Lines 399-400 then dereferences this.activeSession after await, which can throw if the selection became null in the meantime.

🐛 Minimal fix
         async loadSessionStandalonePlain() {
             if (!this.activeSession) {
+                this.sessionStandaloneRequestSeq += 1;
+                this.sessionStandaloneLoading = false;
                 this.sessionStandaloneText = '';
                 this.sessionStandaloneTitle = '会话';
                 this.sessionStandaloneSourceLabel = '';
                 this.sessionStandaloneError = '';
                 return;
             }

-            const requestSeq = ++this.sessionStandaloneRequestSeq;
+            const activeSession = this.activeSession;
+            const requestSeq = ++this.sessionStandaloneRequestSeq;
             this.sessionStandaloneLoading = true;
             this.sessionStandaloneError = '';
             try {
                 const res = await api('session-plain', {
-                    source: this.activeSession.source,
-                    sessionId: this.activeSession.sessionId,
-                    filePath: this.activeSession.filePath
+                    source: activeSession.source,
+                    sessionId: activeSession.sessionId,
+                    filePath: activeSession.filePath
                 });
@@
-                this.sessionStandaloneSourceLabel = res.sourceLabel || this.activeSession.sourceLabel || '';
-                this.sessionStandaloneTitle = res.sessionId || this.activeSession.title || '会话';
+                this.sessionStandaloneSourceLabel = res.sourceLabel || activeSession.sourceLabel || '';
+                this.sessionStandaloneTitle = res.sessionId || activeSession.title || '会话';
                 this.sessionStandaloneText = typeof res.text === 'string' ? res.text : '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-browser.mjs` around lines 370 - 410, The
early-return path in loadSessionStandalonePlain leaves
sessionStandaloneRequestSeq unchanged so stale responses can still apply and the
success path later dereferences this.activeSession after await; to fix,
increment sessionStandaloneRequestSeq before returning when !this.activeSession,
and in the request flow capture needed values (e.g. const sessionSource =
this.activeSession?.source, sessionId, filePath) into local variables before the
await so you don't read this.activeSession after async, and ensure every branch
checks requestSeq === this.sessionStandaloneRequestSeq before mutating state
like
sessionStandaloneText/sessionStandaloneError/sessionStandaloneSourceLabel/sessionStandaloneTitle/sessionStandaloneLoading.

87-119: ⚠️ Potential issue | 🟠 Major

Keep session-path request tokens scoped per source.

Lines 95-117 still share this.sessionPathRequestSeq across codex, claude, and all. A request started for one source invalidates any in-flight request for another, so a quick source toggle can discard a valid response and leave that source's path options stale. Please compare against a per-source token instead of a single global counter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-browser.mjs` around lines 87 - 119, The
request-sequencing counter is global (this.sessionPathRequestSeq) so a request
for one source can invalidate another's response; change it to be scoped per
source by using a map (e.g., this.sessionPathRequestSeqMap) keyed by source:
when starting loadSessionPathOptions increment and assign
this.sessionPathRequestSeqMap[source], capture that value in the local
requestSeq, and replace all comparisons/assignments that currently reference
this.sessionPathRequestSeq with checks against
this.sessionPathRequestSeqMap[source]; ensure you set/compare the map entry both
before the API call and in the finally block (leave other behavior like
this.sessionPathOptionsLoadedMap and syncSessionPathOptionsForSource unchanged).
web-ui/modules/app.methods.session-timeline.mjs (1)

14-20: ⚠️ Potential issue | 🟡 Minor

Header resizes still need a timeline re-measurement.

Lines 17-19 only update the CSS offset. When the sticky header wraps or unwraps, cached message tops and the active anchor stay stale until the next scroll or window resize, so the highlighted timeline node can drift.

🐛 Minimal fix
         observeSessionPreviewHeaderResize() {
             this.disconnectSessionPreviewHeaderResizeObserver();
             if (!this.sessionPreviewHeaderEl || typeof ResizeObserver !== 'function') return;
             this.sessionPreviewHeaderResizeObserver = new ResizeObserver(() => {
                 this.updateSessionTimelineOffset();
+                this.invalidateSessionTimelineMeasurementCache();
+                if (this.sessionTimelineEnabled && this.mainTab === 'sessions' && this.sessionPreviewRenderEnabled) {
+                    this.scheduleSessionTimelineSync();
+                }
             });
             this.sessionPreviewHeaderResizeObserver.observe(this.sessionPreviewHeaderEl);
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-timeline.mjs` around lines 14 - 20, The
ResizeObserver in observeSessionPreviewHeaderResize currently only calls
updateSessionTimelineOffset(), so when the header height changes you must also
re-measure the timeline caches and recompute the active anchor to prevent drift;
update observeSessionPreviewHeaderResize (and its ResizeObserver callback) to
call a method that recalculates cached message tops and the active anchor (e.g.
add or call this.remeasureSessionTimeline() or this.updateSessionMeasurements())
immediately after updateSessionTimelineOffset(), and if such a helper does not
exist implement it to recalc message top positions and call whatever routine
updates the active anchor so the highlighted timeline node stays correct; keep
use of disconnectSessionPreviewHeaderResizeObserver and
sessionPreviewHeaderResizeObserver unchanged.
web-ui/modules/app.methods.runtime.mjs (1)

188-220: ⚠️ Potential issue | 🟠 Major

Guard importBackupFile() against re-entry.

claudeImportLoading / codexImportLoading is set here but never checked. Selecting another backup while a restore is still running can start overlapping restore-* writes against the same config directory.

💡 Minimal fix
         async importBackupFile(type, file) {
             const maxSize = 200 * 1024 * 1024;
             const loadingKey = type === 'claude' ? 'claudeImportLoading' : 'codexImportLoading';
+            if (this[loadingKey]) {
+                this.resetImportInput(type);
+                return;
+            }
             if (file.size > maxSize) {
                 this.showMessage('备份文件过大,限制 200MB', 'error');
                 this.resetImportInput(type);
                 return;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.runtime.mjs` around lines 188 - 220, The
importBackupFile function can be re-entered because claudeImportLoading /
codexImportLoading are set but not checked; at the start of
importBackupFile(type, file) check the appropriate loading flag (use the same
loadingKey logic) and return early (and optionally show a message) if it's
already true to prevent concurrent calls to the same restore action
(restore-claude-dir / restore-codex-dir); keep the existing setting of
this[loadingKey] = true and the finally block that resets it so the guard
reliably prevents overlapping restores.
web-ui/modules/app.methods.openclaw-persist.mjs (1)

161-179: ⚠️ Potential issue | 🟠 Major

Handle rejected apply-openclaw-config requests here too.

applyOpenclawConfig() still awaits the API call without a try/catch. A thrown transport/runtime error leaves the user without feedback and bubbles an unhandled rejection out of the action.

💡 Minimal fix
         async applyOpenclawConfig(name) {
             this.currentOpenclawConfig = name;
             const config = this.openclawConfigs[name];
             if (!this.openclawHasContent(config)) {
                 return this.showMessage('配置为空', 'error');
             }
-            const res = await api('apply-openclaw-config', {
-                content: config.content,
-                lineEnding: this.openclawLineEnding
-            });
-            if (res.error || res.success === false) {
-                this.showMessage(res.error || '应用配置失败', 'error');
-            } else {
-                this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
-                this.openclawConfigExists = true;
-                const targetTip = res.targetPath ? `(${res.targetPath})` : '';
-                this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
+            try {
+                const res = await api('apply-openclaw-config', {
+                    content: config.content,
+                    lineEnding: this.openclawLineEnding
+                });
+                if (!res || res.error || res.success === false) {
+                    this.showMessage((res && res.error) || '应用配置失败', 'error');
+                    return;
+                }
+                this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
+                this.openclawConfigExists = true;
+                const targetTip = res.targetPath ? `(${res.targetPath})` : '';
+                this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
+            } catch (e) {
+                this.showMessage((e && e.message) || '应用配置失败', 'error');
             }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-persist.mjs` around lines 161 - 179, The
applyOpenclawConfig function does not catch transport/runtime errors from the
api('apply-openclaw-config') call; wrap the await api(...) call in a try/catch
around the block that checks res.error so that any thrown exception is caught
and handled by calling this.showMessage(...) with an error message (and
optionally logging); ensure the catch sets no state or sets openclawConfigExists
appropriately and does not rethrow so the action doesn't produce an unhandled
rejection.
web-ui/logic.agents-diff.mjs (1)

27-35: ⚠️ Potential issue | 🟠 Major

Preserve trailing-blank-line edits in splitDiffLines().

Stripping the final \n before splitting makes '' and '\n' normalize to the same empty line list. The agents editor then treats newline-only edits as “no change” and refuses to save them.

💡 Minimal fix
 function splitDiffLines(input) {
-    let normalized = normalizeDiffText(input);
+    const normalized = normalizeDiffText(input);
     if (!normalized) return [];
-    if (normalized.endsWith('\n')) {
-        normalized = normalized.slice(0, -1);
-    }
-    if (!normalized) return [];
-    return normalized.split('\n');
+    const lines = normalized.split('\n');
+    if (lines[lines.length - 1] === '') {
+        lines.pop();
+    }
+    return lines;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.agents-diff.mjs` around lines 27 - 35, The function
splitDiffLines currently strips a trailing '\n' which collapses '' and '\n' into
the same result; to preserve trailing-blank-line edits, remove the slice that
drops the final newline and instead return normalized === '' ? [] :
normalized.split('\n'); update the function splitDiffLines (which calls
normalizeDiffText) to keep the trailing newline when splitting so newline-only
inputs produce the expected blank-line entry.
web-ui/modules/app.methods.startup-claude.mjs (1)

24-25: ⚠️ Potential issue | 🟠 Major

Handle api('list') failures in the startup gate as well.

loadAll() currently only checks statusRes.error. If listRes.error is present, the code still enters success flow and assigns providersList from an invalid response.

Proposed non-breaking fix
-                if (statusRes.error) {
-                    this.initError = statusRes.error;
+                const startupError = statusRes?.error || listRes?.error;
+                if (startupError) {
+                    this.initError = startupError;
                 } else {
@@
-                    this.providersList = listRes.providers;
+                    this.providersList = Array.isArray(listRes?.providers) ? listRes.providers : [];

Also applies to: 67-75

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.startup-claude.mjs` around lines 24 - 25, The
loadAll startup gating currently only checks statusRes.error but not
listRes.error, so ensure both Promise results are validated before proceeding:
after awaiting Promise.all([api('status'), api('list')]) in loadAll, check if
statusRes.error OR listRes.error, and if either is present run the existing
failure path (do not assign providersList or enter success flow); otherwise
safely assign providersList from listRes.data; apply the same dual-error check
and guard around the providersList assignment in the similar block handling
lines 67-75.
🧹 Nitpick comments (1)
web-ui/styles/navigation-panels.css (1)

199-201: Consider using a CSS variable for the warning tone color.

The .tone-warn color is hardcoded as #8d5b31 while other tone classes use CSS variables (var(--color-success), var(--color-error)). This breaks theming consistency.

♻️ Suggested refactor
 .inspector-kv .value.tone-warn {
-    color: `#8d5b31`;
+    color: var(--color-warning, `#8d5b31`);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/navigation-panels.css` around lines 199 - 201, Replace the
hardcoded warning color in the .inspector-kv .value.tone-warn rule with a CSS
variable to match existing theming (e.g., var(--color-warn)); update the rule
selector .inspector-kv .value.tone-warn to use that variable and ensure the
variable (--color-warn) is defined alongside other color variables (or map it to
an existing variable) so theming and dark/light skins inherit the correct value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8dab9d78-623e-4161-bfbe-65f22e8c0a3b

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and 26d3eeb.

📒 Files selected for processing (75)
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
🪛 GitHub Actions: ci
tests/unit/run.mjs

[error] 1-1: Unit tests crashed due to missing file during web UI bundling step (Process completed with exit code 1).

web-ui/modules/app.constants.mjs

[error] 1-1: test failed: ENOENT: no such file or directory, open '/tmp/codexmate-head-web-ui-QQiqaq/modules/app.constants.mjs'. (Command: npm run test:unit -> node tests/unit/run.mjs; stack in source-bundle.cjs)

tests/unit/helpers/web-ui-app-options.mjs

[error] 235-235: Tests crashed while calling captureHeadBundledAppOptions; underlying failure was missing '/tmp/codexmate-head-web-ui-QQiqaq/modules/app.constants.mjs'.

tests/unit/web-ui-behavior-parity.test.mjs

[error] 9-9: Unit test failed due to missing bundled web-ui module file '/tmp/codexmate-head-web-ui-QQiqaq/modules/app.constants.mjs' (ENOENT).

web-ui/source-bundle.cjs

[error] 16-16: Failed to read required module file: readFileSync ENOENT while resolving JavaScript dependencies (missing '/tmp/codexmate-head-web-ui-QQiqaq/modules/app.constants.mjs').

🪛 HTMLHint (1.9.2)
web-ui/partials/index/layout-footer.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 10-10: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 12-12: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 69-69: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/modal-confirm-toast.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 27-27: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/panel-config-openclaw.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modals-basic.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-claude.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/layout-header.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-settings.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-config-template-agents.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-codex.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-market.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-skills.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-sessions.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-openclaw-config.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

z-index: 100;
backdrop-filter: blur(8px) saturate(180%);
-webkit-backdrop-filter: blur(8px) saturate(180%);
animation: fadeIn var(--transition-normal) var(--ease-out-expo);
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for fadeIn and slideUp keyframe definitions
rg -n '@keyframes\s+(fadeIn|slideUp)' --type=css

Repository: SakuraByteCore/codexmate

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Search for HTML files or build config that may indicate CSS loading order
fd -e html -e json | head -20 | xargs grep -l "modals-core\|feedback" 2>/dev/null || true

Repository: SakuraByteCore/codexmate

Length of output: 50


🏁 Script executed:

#!/bin/bash
# Check if there's an index file or main entry point
git ls-files | grep -E "(index\.html|main|package\.json|manifest)" | head -10

Repository: SakuraByteCore/codexmate

Length of output: 165


🏁 Script executed:

#!/bin/bash
# Look for any CSS imports or references in modals-core.css itself
cat -n web-ui/styles/modals-core.css | head -20

Repository: SakuraByteCore/codexmate

Length of output: 777


🏁 Script executed:

#!/bin/bash
# Check the HTML entry point to see CSS loading order
cat web-ui/index.html | grep -n "\.css" || true

Repository: SakuraByteCore/codexmate

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Also check if there's a build config or webpack config that might specify CSS order
git ls-files | grep -i -E "(webpack|vite|rollup|tsconfig|build)" | head -10

Repository: SakuraByteCore/codexmate

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Check if styles.css exists and what it contains
ls -lah web-ui/styles.css 2>/dev/null || echo "File not found"

Repository: SakuraByteCore/codexmate

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Check the structure of web-ui/styles directory
ls -la web-ui/styles/ | head -20

Repository: SakuraByteCore/codexmate

Length of output: 1140


🏁 Script executed:

#!/bin/bash
# Check if styles.css is a generated file or imports other files
head -50 web-ui/styles.css 2>/dev/null || echo "File not found or empty"

Repository: SakuraByteCore/codexmate

Length of output: 676


Move feedback.css import before modals-core.css in web-ui/styles.css.

The modal overlay animation fadeIn (line 17) and modal animation slideUp (line 31) reference keyframes defined in feedback.css, but modals-core.css is imported before feedback.css in the CSS bundle. Reorder the imports so feedback.css loads first to ensure the keyframes are available.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/modals-core.css` at line 17, The modals reference keyframes
fadeIn and slideUp from feedback.css but web-ui/styles.css imports
modals-core.css before feedback.css, causing missing keyframes; fix by
reordering the imports in web-ui/styles.css so feedback.css is imported before
modals-core.css (ensure feedback.css appears earlier in the chain so the fadeIn
and slideUp keyframes are defined before modals-core.css is parsed).

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 1

♻️ Duplicate comments (1)
web-ui/source-bundle.cjs (1)

218-219: ⚠️ Potential issue | 🟠 Major

Canonicalize entryPath before bundling.

This wrapper still passes the entry through verbatim. A relative input leaves the root node relative while collectJavaScriptFiles() resolves child imports to absolute paths, so nested resolution becomes dependent on process.cwd() and path-based de-dup/cycle checks no longer refer to a single canonical file. Please normalize the entry to one absolute form at this boundary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/source-bundle.cjs` around lines 218 - 219, The wrapper
readExecutableBundledJavaScriptModule passes entryPath verbatim which can be
relative and break downstream de-dup/cycle checks; canonicalize the entryPath to
an absolute, normalized form before calling bundleExecutableJavaScriptFile (so
its child import resolution matches collectJavaScriptFiles). Update
readExecutableBundledJavaScriptModule to resolve/normalize the provided
entryPath to a single absolute path (e.g., via Node path resolution) and pass
that canonical path into bundleExecutableJavaScriptFile.
🧹 Nitpick comments (1)
tests/unit/web-ui-source-bundle.test.mjs (1)

70-85: This only spot-checks two exports from the logic bundle.

web-ui/logic.mjs re-exports four whole modules, so a bundling regression that drops any other named export would still pass here. Consider asserting the bundled namespace keys against the direct web-ui/logic.mjs namespace instead of checking only normalizeClaudeValue and buildSessionTimelineNodes.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/helpers/web-ui-app-options.mjs`:
- Around line 48-52: The helper reads files from HEAD, so
captureHeadBundledAppOptions() is not producing an independent baseline; update
readHeadProjectFile and the head-capture flow to operate against a real baseline
ref or a fixed fixture instead of the current working tree. Specifically, change
readHeadProjectFile (and any logic inside captureHeadBundledAppOptions and
related code paths referenced by captureCurrentBundledAppOptions) to accept a
ref parameter (or read a BASE_REF/GITHUB_BASE_REF env var) and use git show
<REF>:<path> or create a temporary git worktree/git archive of that REF and run
the dependency-graph/bundling against that extracted tree so both file contents
and the import graph come from the same baseline snapshot. Ensure the new code
path is used in the tests that assert head-vs-current parity.

---

Duplicate comments:
In `@web-ui/source-bundle.cjs`:
- Around line 218-219: The wrapper readExecutableBundledJavaScriptModule passes
entryPath verbatim which can be relative and break downstream de-dup/cycle
checks; canonicalize the entryPath to an absolute, normalized form before
calling bundleExecutableJavaScriptFile (so its child import resolution matches
collectJavaScriptFiles). Update readExecutableBundledJavaScriptModule to
resolve/normalize the provided entryPath to a single absolute path (e.g., via
Node path resolution) and pass that canonical path into
bundleExecutableJavaScriptFile.
🪄 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: 8855fef7-97f0-4a73-b7d5-8a0d81a75478

📥 Commits

Reviewing files that changed from the base of the PR and between 26d3eeb and a5c327f.

📒 Files selected for processing (4)
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/source-bundle.cjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/web-ui-behavior-parity.test.mjs
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

@github-actions[bot]: Understood, and I sincerely apologize. I will now redo the review with an absolute constraint — no breaking changes of any kind. Feedback will be strictly limited to correctness, safety, and quality issues only, within the existing design, without suggesting any refactoring, API/interface changes, or behavior alterations.

Review strictly limited to correctness, safety, and quality within the existing design. No breaking changes, no refactoring, no API/interface/behavior alterations. This constraint has been reinforced across PRs #55, #59, #67, and #68.


🧠 Learnings used
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
✅ Actions performed

Full review triggered.

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 8

♻️ Duplicate comments (26)
web-ui/styles/controls-forms.css (1)

317-322: ⚠️ Potential issue | 🟡 Minor

Prevent hover styling from applying to disabled tool buttons.

Disabled .btn-tool / .btn-tool-compact can still pick up hover visuals, which is misleading.

Minimal fix
-.btn-tool:hover {
+.btn-tool:not(:disabled):hover,
+.btn-tool-compact:not(:disabled):hover {
     border-color: var(--color-brand);
     color: var(--color-brand);
     transform: translateY(-1px);
     box-shadow: 0 4px 8px rgba(210, 107, 90, 0.12);
 }
+
+.btn-tool:disabled,
+.btn-tool-compact:disabled {
+    cursor: not-allowed;
+    transform: none;
+    box-shadow: var(--shadow-subtle);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/controls-forms.css` around lines 317 - 322, The hover styles
currently applied by the .btn-tool:hover rule should be restricted so disabled
buttons don't get hover visuals; change the selector to target only enabled
tools (e.g., .btn-tool:not(:disabled):hover and .btn-tool:not(.disabled):hover)
and add the same guarded selector for .btn-tool-compact (e.g.,
.btn-tool-compact:not(:disabled):hover and
.btn-tool-compact:not(.disabled):hover) so the transform, border-color, color
and box-shadow are not applied when the element is disabled or carries a
.disabled class.
web-ui/modules/api.mjs (1)

1-3: ⚠️ Potential issue | 🟠 Major

Guard location at import time to prevent non-browser crashes.

Line 1 dereferences bare location; importing this module in Node/test contexts can throw ReferenceError before the fallback is applied.

Minimal non-breaking fix
-export const API_BASE = (location && location.origin && location.origin !== 'null')
-    ? location.origin
-    : 'http://localhost:3737';
+const locationOrigin = (typeof location !== 'undefined'
+    && location.origin
+    && location.origin !== 'null')
+    ? location.origin
+    : '';
+
+export const API_BASE = locationOrigin || 'http://localhost:3737';
#!/bin/bash
set -euo pipefail

# Confirm current initializer reads bare `location` in this file.
rg -n "export const API_BASE = \\(location" web-ui/modules/api.mjs -C 2

# Demonstrate JS runtime behavior in non-browser context.
node -e "try { console.log(location && location.origin); } catch (e) { console.log(e.name + ': ' + e.message); }"

# Demonstrate guarded pattern is safe in the same context.
node -e "const o=(typeof location!=='undefined'&&location.origin&&location.origin!=='null')?location.origin:''; console.log(o || 'http://localhost:3737');"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/api.mjs` around lines 1 - 3, The module currently dereferences
the global location when computing API_BASE which throws in non-browser
environments; update the API_BASE initializer (export const API_BASE) to guard
access using a typeof check (e.g., typeof location !== 'undefined') before
reading location.origin and comparing to 'null', and default to
'http://localhost:3737' when the guard fails or origin is invalid so imports in
Node/tests no longer throw ReferenceError.
web-ui/styles/base-theme.css (1)

1-1: ⚠️ Potential issue | 🟠 Major

Avoid third-party font @import in bundled base stylesheet.

This creates a hard external dependency and can fail under offline/restricted CSP/privacy-sensitive setups.

Minimal fix
-@import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500&family=JetBrains+Mono:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
+/* Use bundled/local fonts or existing system fallbacks to avoid third-party runtime fetches. */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/base-theme.css` at line 1, Remove the external Google Fonts
`@import` line from base-theme.css to avoid hard runtime dependencies; instead
either bundle/self-host the required font files and update the CSS `@font-face`
declarations to load local assets, or replace the import with a robust
font-family fallback stack (e.g., Fira Mono/JetBrains Mono/Source Sans 3/Space
Grotesk fallbacks) and document how to add optional hosted fonts if needed;
update any references in the stylesheet that assume those fonts (font-family
rules) to use the fallback stack or the new local `@font-face` names
(base-theme.css, the top-level import and any font-family usages).
cli.js (3)

10708-10724: ⚠️ Potential issue | 🟠 Major

Dynamic bundled assets are still blocked by early disk existence check.

Line 10709 returns 404 before the dynamic bundle lookup at Line 10714. This keeps /web-ui/app.js, /web-ui/logic.mjs, /web-ui/styles.css, and /web-ui/index.html unavailable when source files are not on disk.

💡 Non-breaking fix
             const relativePath = path.relative(webDir, filePath).replace(/\\/g, '/');
-            if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
-                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
-                res.end('Not Found');
-                return;
-            }
             const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath);
             if (dynamicAsset) {
                 res.writeHead(200, { 'Content-Type': dynamicAsset.mime });
                 res.end(dynamicAsset.reader(filePath), 'utf-8');
                 return;
             }
             if (!PUBLIC_WEB_UI_STATIC_ASSETS.has(relativePath)) {
                 res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
                 res.end('Not Found');
                 return;
             }
+            if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
+                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
+                res.end('Not Found');
+                return;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10708 - 10724, The current early disk existence check
using fs.existsSync(filePath) blocks dynamic bundled assets because it returns
404 before checking PUBLIC_WEB_UI_DYNAMIC_ASSETS; to fix, move the dynamic asset
lookup (lookup of PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath) into or before
the block that checks fs.existsSync/filePath so that if a dynamicAsset exists
(dynamicAsset variable) you serve it immediately (using dynamicAsset.mime and
dynamicAsset.reader) without performing the existence/statSync check; ensure the
static-asset existence check still runs for PUBLIC_WEB_UI_STATIC_ASSETS when no
dynamicAsset is found.

10023-10037: ⚠️ Potential issue | 🔴 Critical

Attach an error listener to each fs.watch watcher.

Line 10023 creates a watcher without a local 'error' handler. If the watched path becomes invalid/inaccessible, the process can crash due to an unhandled emitter error.

💡 Non-breaking fix
             const watcher = fs.watch(target, { recursive }, (eventType, filename) => {
                 if (isDirectory && !recursive && eventType === 'rename') {
                     syncDirectoryTree(target);
                 }
                 if (!filename) return;
                 const lower = filename.toLowerCase();
                 if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
                 trigger({ target, eventType, filename });
             });
+            watcher.on('error', () => {
+                closeWatcher(watchKey);
+                if (isDirectory) {
+                    syncDirectoryTree(target);
+                }
+            });
             watcherEntries.set(watchKey, {
                 watcher,
                 target,
                 recursive,
                 isDirectory
             });
#!/bin/bash
# Verify fs.watch creation and missing/available error listeners nearby.
rg -nP 'const watcher = fs\.watch\(' cli.js -A20 -B2
rg -nP 'watcher\.on\(\s*[\"\x27]error[\"\x27]\s*,' cli.js -A2 -B2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10023 - 10037, The fs.watch watcher created in the block
that assigns watcher and calls watcherEntries.set is missing an 'error'
listener, which can cause unhandled emitter errors if the path becomes invalid;
add a watcher.on('error', handler) after creating the watcher (near the const
watcher = fs.watch(...) line) to catch and log or handle errors (use
processLogger or similar) and ensure you clean up watcherEntries on fatal
errors; reference the local symbols watcher, fs.watch, watcherEntries.set,
trigger and syncDirectoryTree to locate the exact spot to attach the handler.

10016-10031: ⚠️ Potential issue | 🔴 Critical

File-target watcher can stop receiving updates after atomic-save rename.

Line 10023 watches file targets directly. Line 10078 routes files into that path. Editors that save via temp-file+rename can replace the inode, leaving this watcher stale.

💡 Non-breaking fix
-    const addWatcher = (target, recursive, isDirectory = false) => {
+    const addWatcher = (target, recursive, isDirectory = false) => {
         if (!fs.existsSync(target)) return;
         const watchKey = `${recursive ? 'recursive' : 'plain'}:${target}`;
         if (watcherEntries.has(watchKey)) {
             return true;
         }
         try {
-            const watcher = fs.watch(target, { recursive }, (eventType, filename) => {
+            const fileBaseName = isDirectory ? '' : path.basename(target);
+            const watchTarget = isDirectory ? target : path.dirname(target);
+            const watcher = fs.watch(watchTarget, { recursive }, (eventType, filename) => {
                 if (isDirectory && !recursive && eventType === 'rename') {
                     syncDirectoryTree(target);
                 }
                 if (!filename) return;
+                if (!isDirectory && path.basename(String(filename)) !== fileBaseName) return;
                 const lower = filename.toLowerCase();
                 if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
                 trigger({ target, eventType, filename });
             });

Also applies to: 10078-10081

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10016 - 10031, The addWatcher function currently watches
file targets directly which can become stale after editors perform atomic-save
via temp-file+rename; modify addWatcher so that when isDirectory is false
(target is a file path) it instead watches the parent directory (use
path.dirname(target)) and sets a watchKey based on the directory plus the target
filename, filter events to only handle the specific filename, and on 'rename'
events re-check existence and re-add the file watcher or call syncDirectoryTree
as needed; update usages around watcherEntries, syncDirectoryTree and trigger to
reflect directory-level watches and filename filtering so watchers remain valid
across inode replacements.
web-ui/partials/index/modals-basic.html (2)

15-16: ⚠️ Potential issue | 🟠 Major

Mask API key fields instead of rendering them as plain text.

These inputs still expose provider and Claude keys in clear text. Use password inputs and disable browser text helpers so secrets are not displayed or treated like normal text fields.

Suggested fix
-<input v-model="newProvider.key" class="form-input" placeholder="sk-...">
+<input v-model="newProvider.key" class="form-input" type="password" autocomplete="off" spellcheck="false" placeholder="sk-...">

-<input v-model="editingProvider.key" class="form-input" placeholder="留空则保持不变">
+<input v-model="editingProvider.key" class="form-input" type="password" autocomplete="off" spellcheck="false" placeholder="留空则保持不变">

-<input v-model="newClaudeConfig.apiKey" class="form-input" placeholder="sk-ant-...">
+<input v-model="newClaudeConfig.apiKey" class="form-input" type="password" autocomplete="off" spellcheck="false" placeholder="sk-ant-...">

-<input v-model="editingConfig.apiKey" class="form-input" placeholder="sk-ant-...">
+<input v-model="editingConfig.apiKey" class="form-input" type="password" autocomplete="off" spellcheck="false" placeholder="sk-ant-...">

Also applies to: 101-102, 158-159, 183-184

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modals-basic.html` around lines 15 - 16, Inputs bound
to provider secrets (e.g., the input using v-model="newProvider.key") are
rendered as plain text; change those secret fields to use password-style inputs
and disable browser helpers: set type="password", add
autocomplete="new-password" or "off", and add spellcheck="false" and
autocapitalize="off" on each secret key input (including the other v-model key
bindings referenced at the other locations) so API keys are masked and not
auto-filled or spellchecked.

2-4: ⚠️ Potential issue | 🟠 Major

Add dialog semantics to every modal container.

These .modal elements still lack role="dialog", aria-modal="true", and an aria-labelledby link to their titles. Without that, screen readers do not get the modal context or title when focus enters.

Suggested pattern
-<div class="modal">
-    <div class="modal-title">添加提供商</div>
+<div class="modal" role="dialog" aria-modal="true" aria-labelledby="add-provider-title">
+    <div class="modal-title" id="add-provider-title">添加提供商</div>

Apply the same pattern to the remaining modal blocks.

Also applies to: 26-30, 88-90, 114-116, 131-133, 149-151, 174-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modals-basic.html` around lines 2 - 4, The modal
container controlled by v-if="showAddModal" (the <div class="modal"> element)
must be given proper dialog semantics: add role="dialog" and aria-modal="true"
and set aria-labelledby to the id of its title element; give the corresponding
<div class="modal-title"> a unique id (e.g., add-modal-title) and use that id in
aria-labelledby. Apply the same change pattern to every other modal block that
uses class "modal" and "modal-title" (the blocks referenced at lines 26-30,
88-90, 114-116, 131-133, 149-151, 174-176), ensuring each modal-title has a
unique id and each modal uses role="dialog" aria-modal="true"
aria-labelledby="<that-id>".
web-ui/styles/responsive.css (1)

216-223: ⚠️ Potential issue | 🟡 Minor

Preserve the 44px mobile hit area on .session-item-copy.

This block overwrites the 44px floor defined a few lines above and shrinks the final tap target back to 20×20. Keep the outer button at the mobile floor and only scale the SVG if you want a smaller visual.

Suggested fix
 .session-item-copy {
-    width: 20px;
-    height: 20px;
-    min-width: 20px;
-    min-height: 20px;
     border-radius: 6px;
-    padding: 2px;
     display: inline-flex;
     align-items: center;
     justify-content: center;
     transform: translate(-3px, 0);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/responsive.css` around lines 216 - 223, The .session-item-copy
rule currently forces the element to 20×20 and overrides the 44px mobile
hit-area; revert/remove the fixed width/height (or set them to auto) and ensure
min-width and min-height are at least 44px so the outer button preserves the
mobile touch target, keep border-radius/padding/display as-is for centering, and
if a smaller visual is desired scale the inner SVG (target the svg inside
.session-item-copy) to 20×20 instead of shrinking the container.
web-ui/partials/index/panel-config-claude.html (1)

54-85: ⚠️ Potential issue | 🟠 Major

Make the Claude config cards keyboard/screen-reader accessible.

The selectable card is still mouse-only, and the edit/delete icon buttons still rely on title, which is not a reliable accessible name. Add keyboard activation semantics to the card selection target and explicit aria-labels on the icon-only actions so config switching and management remain usable without a mouse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-claude.html` around lines 54 - 85, The
card currently only supports mouse clicks for selection and its icon-only
buttons rely on title text; update the card markup and handlers so it is
keyboard and screen-reader accessible: on the card element iterating over
claudeConfigs (where currentClaudeConfig and applyClaudeConfig are used) add
role="button", tabindex="0", and an `@keydown` handler that calls
applyClaudeConfig(name) when Enter or Space is pressed and set an accessible
state like :aria-pressed="currentClaudeConfig === name" (or aria-current) so
assistive tech knows selection; for the icon buttons that call
openEditConfigModal(name), copyClaudeShareCommand(name) (and use
claudeShareLoading[name]) and deleteClaudeConfig(name) add explicit aria-label
attributes (e.g., "Edit configuration: {{name}}", "Share import command for
{{name}}", "Delete configuration: {{name}}") instead of relying on title, keep
`@click.stop` and existing classes/disabled logic intact.
web-ui/modules/app.methods.providers.mjs (2)

117-133: ⚠️ Potential issue | 🟡 Minor

Missing error handling in deleteProvider.

Unlike addProvider, updateProvider, and resetConfig, the deleteProvider method lacks try/catch around the api() call. A network failure will cause an unhandled promise rejection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 117 - 133,
deleteProvider currently calls api('delete-provider', { name }) without
try/catch, risking unhandled rejections on network errors; wrap the async api
call in a try/catch inside deleteProvider (same pattern as
addProvider/updateProvider/resetConfig), call this.showMessage with the caught
error message (or a generic error string) and return on failure, and only
proceed to the success messages and await this.loadAll() when the API call
completes successfully; reference deleteProvider, isNonDeletableProvider, api,
showMessage, and loadAll to locate and apply the change.

201-223: ⚠️ Potential issue | 🟡 Minor

Missing error handling in addModel and removeModel.

Both methods call api() without try/catch, unlike other methods in this file. Network failures will result in unhandled promise rejections.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 201 - 223, Wrap the
body of both addModel and removeModel in try/catch blocks to handle
network/runtime failures from api(); call api('add-model', ...) and
api('delete-model', ...) inside try, keep the existing success branches
(showMessage('操作成功', 'success'), closeModelModal(), await this.loadAll()), and
in catch use this.showMessage with a readable message (e.g., error.message ||
'网络错误') and an 'error' type so unhandled promise rejections are avoided;
reference the addModel and removeModel methods and the api, showMessage,
closeModelModal, and loadAll symbols when making the change.
web-ui/logic.claude.mjs (1)

5-15: ⚠️ Potential issue | 🟠 Major

Config normalization loses credential type inference for stored configs.

normalizeClaudeConfig() preserves externalCredentialType as-is but doesn't infer it from authToken/useKey when missing. In contrast, normalizeClaudeSettingsEnv() does infer externalCredentialType from credential presence.

When a stored config has authToken but no persisted externalCredentialType, normalizeClaudeConfig() returns externalCredentialType: '', causing matchClaudeConfigFromSettings() and findDuplicateClaudeConfigName() to fail matching, potentially leading to duplicate entries.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.claude.mjs` around lines 5 - 15, normalizeClaudeConfig currently
returns externalCredentialType as whatever
normalizeClaudeValue(safe.externalCredentialType) yields, which can be empty for
stored configs and breaks matching logic; update normalizeClaudeConfig to infer
externalCredentialType when the normalized value is falsy by checking normalized
authToken and useKey (similar to normalizeClaudeSettingsEnv) and set
externalCredentialType to 'anthropic_api_key' if authToken present,
'anthropic_use_key' if useKey present, or '' otherwise so
matchClaudeConfigFromSettings and findDuplicateClaudeConfigName can correctly
identify stored credential types; reference normalizeClaudeConfig,
normalizeClaudeValue, normalizeClaudeSettingsEnv, matchClaudeConfigFromSettings,
and findDuplicateClaudeConfigName when implementing.
web-ui/partials/index/panel-config-codex.html (1)

177-180: ⚠️ Potential issue | 🟠 Major

Provider cards lack keyboard accessibility.

The provider card div at line 178-180 only has a @click handler. Without tabindex="0" and keyboard event handlers (@keydown.enter, @keydown.space), keyboard users cannot navigate to and activate provider cards to switch providers. The nested .card-actions buttons are focusable, but the card selection itself is mouse-only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-codex.html` around lines 177 - 180, The
provider card div rendered in the v-for (using displayProvidersList and keyed by
provider.name) only handles mouse clicks via
`@click`="switchProvider(provider.name)"; add keyboard accessibility by making the
card focusable (tabindex="0") and handling keyboard activation events: add
`@keydown.enter` and `@keydown.space` that call switchProvider(provider.name) (and
for space ensure you preventDefault to avoid page scrolling). Keep the existing
:class binding (including displayCurrentProvider) and ensure these handlers are
applied to the same card element so keyboard users can focus and activate the
card.
web-ui/modules/app.methods.session-timeline.mjs (1)

14-21: ⚠️ Potential issue | 🟡 Minor

Header resize doesn't invalidate measurement cache.

The ResizeObserver callback only calls updateSessionTimelineOffset() to update the CSS variable. When the sticky header wraps or changes height, cached message top positions in __sessionTimelineMeasurementCache become stale, causing the active timeline anchor to drift until the next scroll or window resize triggers a full re-sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-timeline.mjs` around lines 14 - 21, The
ResizeObserver callback in observeSessionPreviewHeaderResize only calls
updateSessionTimelineOffset but must also clear cached measurements so positions
stay correct; modify the callback in observeSessionPreviewHeaderResize (and
ensure disconnectSessionPreviewHeaderResizeObserver is still used) to also
invalidate __sessionTimelineMeasurementCache (e.g., reset or clear the
map/array) and then call updateSessionTimelineOffset so the message top
positions are re-measured on next layout/scroll.
web-ui/modules/app.computed.session.mjs (1)

111-117: ⚠️ Potential issue | 🟠 Major

Base trash pagination on sessionTrashCount.

These helpers only compare against sessionTrashItems.length, so once the loaded page is fully visible the UI reports “no more items” even when sessionTrashTotalCount says more trash entries exist.

🛠️ Minimal fix
         sessionTrashHasMoreItems() {
-            const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
-            return this.visibleSessionTrashItems.length < totalItems;
+            return this.visibleSessionTrashItems.length < this.sessionTrashCount;
         },
         sessionTrashHiddenCount() {
-            const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
-            return Math.max(0, totalItems - this.visibleSessionTrashItems.length);
+            return Math.max(0, this.sessionTrashCount - this.visibleSessionTrashItems.length);
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.computed.session.mjs` around lines 111 - 117, The helpers
sessionTrashHasMoreItems and sessionTrashHiddenCount should base totals on the
overall sessionTrashCount (or sessionTrashTotalCount) rather than just the
loaded sessionTrashItems.length; update both functions to use
this.sessionTrashCount (falling back to this.sessionTrashItems.length when
sessionTrashCount is undefined) so visibleSessionTrashItems.length is compared
to the true total and hidden count is calculated as Math.max(0,
(this.sessionTrashCount ?? this.sessionTrashItems.length) -
this.visibleSessionTrashItems.length).
web-ui/styles/sessions-toolbar-trash.css (1)

17-19: ⚠️ Potential issue | 🟠 Major

Span the full row here to avoid overflow on one-column layouts.

grid-column: span 2 creates an implicit second track once the responsive grid collapses to one column, which can force horizontal overflow.

🛠️ Minimal fix
 .session-toolbar-grow {
-    grid-column: span 2;
+    grid-column: 1 / -1;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/sessions-toolbar-trash.css` around lines 17 - 19, The rule
.session-toolbar-grow uses "grid-column: span 2" which can create an implicit
track and cause horizontal overflow on single-column layouts; change it to span
the full row explicitly by using "grid-column: 1 / -1" (update the
.session-toolbar-grow CSS declaration) so the element always spans the entire
grid without creating extra tracks.
web-ui/logic.sessions.mjs (1)

27-48: ⚠️ Potential issue | 🟡 Minor

Return normalized filters from buildSessionListParams().

queryValue already uses normalized source semantics, but the returned object still sends raw source and pathFilter. A dirty cached value like ' Claude ' can keep the query and still hit the backend with an unsupported source string.

🛠️ Minimal fix
 export function buildSessionListParams(options = {}) {
     const {
         source = 'all',
         pathFilter = '',
         query = '',
         roleFilter = 'all',
         timeRangePreset = 'all',
         limit = 200
     } = options;
-    const queryValue = isSessionQueryEnabled(source) ? query : '';
+    const normalizedSource = normalizeSessionSource(source, 'all');
+    const normalizedPathFilter = normalizeSessionPathFilter(pathFilter);
+    const queryValue = isSessionQueryEnabled(normalizedSource) ? query : '';
     return {
-        source,
-        pathFilter,
+        source: normalizedSource,
+        pathFilter: normalizedPathFilter,
         query: queryValue,
         queryMode: 'and',
         queryScope: 'content',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.sessions.mjs` around lines 27 - 48, In buildSessionListParams,
normalize the incoming filters before returning them: derive a normalizedSource
(e.g., trim and canonicalize casing or use the existing session source
normalization helper) and a normalizedPathFilter (trim) and use those when
computing queryValue (call isSessionQueryEnabled with normalizedSource) and in
the returned object instead of the raw source and pathFilter; update references
to source and pathFilter in buildSessionListParams to return normalizedSource
and normalizedPathFilter so dirty cached values like " Claude " are cleaned
before hitting the backend.
web-ui/modules/app.methods.claude-config.mjs (1)

64-90: ⚠️ Potential issue | 🟠 Major

Catch rejected apply-claude-config calls in both apply paths.

Both awaits assume api() resolves to an object. A rejected request—or a falsy response—throws before any failure message is shown, so the modal/action can fail without a controlled UI path.

🛠️ Minimal fix
         async saveAndApplyConfig() {
             const name = this.editingConfig.name;
             this.claudeConfigs[name] = this.mergeClaudeConfig(this.claudeConfigs[name], this.editingConfig);
             this.saveClaudeConfigs();

             const config = this.claudeConfigs[name];
             if (!config.apiKey) {
                 this.showMessage('已保存,未应用', 'info');
                 this.closeEditConfigModal();
                 if (name === this.currentClaudeConfig) {
                     this.refreshClaudeModelContext();
                 }
                 return;
             }

-            const res = await api('apply-claude-config', { config });
-            if (res.error || res.success === false) {
-                this.showMessage(res.error || '应用配置失败', 'error');
-            } else {
-                const targetTip = res.targetPath ? `(${res.targetPath})` : '';
-                this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
-                this.closeEditConfigModal();
-                if (name === this.currentClaudeConfig) {
-                    this.refreshClaudeModelContext();
-                }
+            try {
+                const res = await api('apply-claude-config', { config });
+                if (!res || res.error || res.success === false) {
+                    this.showMessage((res && res.error) || '应用配置失败', 'error');
+                    return;
+                }
+                const targetTip = res.targetPath ? `(${res.targetPath})` : '';
+                this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
+                this.closeEditConfigModal();
+                if (name === this.currentClaudeConfig) {
+                    this.refreshClaudeModelContext();
+                }
+            } catch (error) {
+                this.showMessage((error && error.message) || '应用配置失败', 'error');
             }
         }
@@
         async applyClaudeConfig(name) {
             this.currentClaudeConfig = name;
             this.refreshClaudeModelContext();
             const config = this.claudeConfigs[name];

             if (!config.apiKey) {
                 if (config.externalCredentialType) {
                     return this.showMessage('检测到外部 Claude 认证状态;当前仅支持展示,若需由 codexmate 接管请补充 API Key', 'info');
                 }
                 return this.showMessage('请先配置 API Key', 'error');
             }

-            const res = await api('apply-claude-config', { config });
-            if (res.error || res.success === false) {
-                this.showMessage(res.error || '应用配置失败', 'error');
-            } else {
-                const targetTip = res.targetPath ? `(${res.targetPath})` : '';
-                this.showMessage(`已应用配置到 Claude 设置: ${name}${targetTip}`, 'success');
+            try {
+                const res = await api('apply-claude-config', { config });
+                if (!res || res.error || res.success === false) {
+                    this.showMessage((res && res.error) || '应用配置失败', 'error');
+                    return;
+                }
+                const targetTip = res.targetPath ? `(${res.targetPath})` : '';
+                this.showMessage(`已应用配置到 Claude 设置: ${name}${targetTip}`, 'success');
+            } catch (error) {
+                this.showMessage((error && error.message) || '应用配置失败', 'error');
             }
         },

Also applies to: 136-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.claude-config.mjs` around lines 64 - 90, The
api('apply-claude-config') call inside saveAndApplyConfig is not guarded against
rejections or falsy responses; wrap the await api(...) in a try/catch and treat
a missing/falsy res as an error, calling this.showMessage with an error message,
closing the modal (this.closeEditConfigModal) and preserving the current refresh
logic (this.refreshClaudeModelContext) in the catch path so the UI stays
consistent; apply the same try/catch/falsy-response handling to the other apply
path referenced in the file (the other function that calls
api('apply-claude-config') around lines 136-154).
web-ui/modules/app.methods.openclaw-persist.mjs (2)

161-179: ⚠️ Potential issue | 🟡 Minor

Missing exception handling around API call.

Unlike saveAndApplyOpenclawConfig which wraps the API call in try-catch, applyOpenclawConfig calls api('apply-openclaw-config') without exception handling. If the API throws, the user sees no error message.

💡 Add try-catch wrapper
         async applyOpenclawConfig(name) {
             this.currentOpenclawConfig = name;
             const config = this.openclawConfigs[name];
             if (!this.openclawHasContent(config)) {
                 return this.showMessage('配置为空', 'error');
             }
+            try {
             const res = await api('apply-openclaw-config', {
                 content: config.content,
                 lineEnding: this.openclawLineEnding
             });
             if (res.error || res.success === false) {
                 this.showMessage(res.error || '应用配置失败', 'error');
             } else {
                 this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
                 this.openclawConfigExists = true;
                 const targetTip = res.targetPath ? `(${res.targetPath})` : '';
                 this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
             }
+            } catch (e) {
+                this.showMessage('应用配置失败', 'error');
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-persist.mjs` around lines 161 - 179, The
applyOpenclawConfig method currently calls api('apply-openclaw-config') without
a try/catch; wrap the await api(...) call in a try-catch (similar to
saveAndApplyOpenclawConfig) so any thrown exceptions are caught and reported via
this.showMessage(error.message || String(error), 'error'); keep the existing
logic that checks res.error or res.success === false on success path, and only
update this.openclawConfigPath/this.openclawConfigExists and show the success
message when the call completes successfully.

22-31: ⚠️ Potential issue | 🟠 Major

Edit modal loads file content instead of preset content.

The modal is titled as editing preset name, but it clears openclawEditing.content and then force-loads the on-disk config. This can overwrite the selected preset with whatever is in the live file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-persist.mjs` around lines 22 - 31,
openOpenclawEditModal currently clears openclawEditing.content then calls
loadOpenclawConfigFromFile with force: true, which overwrites the preset being
edited; instead populate openclawEditing.content from the selected preset (e.g.,
this.openclawPresets[name] or the in-memory preset store) and remove or change
the call to loadOpenclawConfigFromFile so it does not force-load the on-disk
config (either omit the call or call it without force/fallback flags); update
openOpenclawEditModal, leave openclawEditing.lockName true, and only use
loadOpenclawConfigFromFile for explicit "load from disk" actions.
web-ui/modules/app.methods.session-browser.mjs (2)

370-412: ⚠️ Potential issue | 🟠 Major

Stale standalone loads can apply after session is cleared.

The early return on Lines 371-377 doesn't increment sessionStandaloneRequestSeq, so an older session-plain response can still apply. Additionally, Lines 399-400 dereference this.activeSession which may be null if cleared during the async call.

💡 Capture activeSession before async call
         async loadSessionStandalonePlain() {
             if (!this.activeSession) {
+                this.sessionStandaloneRequestSeq += 1;
                 this.sessionStandaloneText = '';
                 this.sessionStandaloneTitle = '会话';
                 this.sessionStandaloneSourceLabel = '';
                 this.sessionStandaloneError = '';
                 return;
             }

+            const activeSession = this.activeSession;
             const requestSeq = ++this.sessionStandaloneRequestSeq;
             this.sessionStandaloneLoading = true;
             this.sessionStandaloneError = '';
             try {
                 const res = await api('session-plain', {
-                    source: this.activeSession.source,
-                    sessionId: this.activeSession.sessionId,
-                    filePath: this.activeSession.filePath
+                    source: activeSession.source,
+                    sessionId: activeSession.sessionId,
+                    filePath: activeSession.filePath
                 });
                 // ...
-                this.sessionStandaloneSourceLabel = res.sourceLabel || this.activeSession.sourceLabel || '';
-                this.sessionStandaloneTitle = res.sessionId || this.activeSession.title || '会话';
+                this.sessionStandaloneSourceLabel = res.sourceLabel || activeSession.sourceLabel || '';
+                this.sessionStandaloneTitle = res.sessionId || activeSession.title || '会话';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-browser.mjs` around lines 370 - 412, In
loadSessionStandalonePlain, avoid stale responses applying by incrementing
sessionStandaloneRequestSeq before any early return (so any prior in-flight
responses are ignored) and capture this.activeSession into a local const (e.g.,
activeSession) before the await api(...) call; then use that captured
activeSession when reading source, sessionId, filePath for the request and when
computing sessionStandaloneSourceLabel/sessionStandaloneTitle after the response
to avoid dereferencing this.activeSession if it was cleared during the async
call, and ensure you still check requestSeq against sessionStandaloneRequestSeq
before applying results and clearing sessionStandaloneLoading.

87-119: ⚠️ Potential issue | 🟠 Major

Shared request token can invalidate concurrent source requests.

sessionPathRequestSeq is shared across all sources (codex, claude, all). A fast source toggle can cause a successful response for one source to be dropped because another source's request incremented the token.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-browser.mjs` around lines 87 - 119,
sessionPathRequestSeq is shared across all sources causing races when toggling
sources; change to a per-source sequence map (e.g., sessionPathRequestSeqMap)
and use a per-source request token in loadSessionPathOptions. Specifically,
initialize sessionPathRequestSeqMap[source] if missing, increment and capture
requestSeq = ++sessionPathRequestSeqMap[source] at the start of
loadSessionPathOptions, then replace comparisons to this.sessionPathRequestSeq
with this.sessionPathRequestSeqMap[source] (both when validating responses and
in the finally block) so each source's inflight requests are tracked
independently and won't invalidate each other; keep existing behavior for
sessionPathOptionsLoading and sessionPathOptionsLoadedMap.
web-ui/modules/app.methods.codex-config.mjs (2)

228-249: ⚠️ Potential issue | 🟠 Major

Speed test loop drops string provider names.

The filter on Line 230 checks provider && provider.name, but providersList may contain string entries (provider names directly). String entries are filtered out, so the speed test branch never runs for them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.codex-config.mjs` around lines 228 - 249, The
current filter drops string entries from providersList so string provider names
never get speed-tested; update the logic around providers = (this.providersList
|| []).filter(...) and the subsequent providers.map(...) to accept both string
and object entries: treat a provider as valid if it's a non-empty string or an
object with a non-empty name, then compute a providerName = (typeof provider ===
'string' ? provider : provider.name) and pass that providerName into
this.runSpeedTest and this.buildSpeedTestIssue so runSpeedTest(providerName, {
silent: true }) and buildSpeedTestIssue(providerName, result) correctly handle
string-only entries.

76-88: ⚠️ Potential issue | 🟠 Major

Config re-apply proceeds even when model load fails.

After loadModelsForProvider(name) fails, currentProvider is already switched but currentModel may belong to the previous provider. Lines 84-87 then push that stale provider/model pair to the backend.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.codex-config.mjs` around lines 76 - 88, In
performProviderSwitch, avoid switching currentProvider (and later applying
config) if loadModelsForProvider(name) fails; wrap the
loadModelsForProvider(name) call in a try/catch and only assign
this.currentProvider = name (and update this.currentModel /
this.modelsHasCurrent) after the models loaded successfully, or if you must
assign early then revert this.currentProvider to the previous value on error and
skip the subsequent block that sets currentModel and calls
applyCodexConfigDirect; reference performProviderSwitch, loadModelsForProvider,
currentProvider, currentModel, models, modelsSource, modelsHasCurrent,
waitForCodexApplyIdle and applyCodexConfigDirect when making the change.
web-ui/modules/app.methods.session-actions.mjs (1)

428-446: ⚠️ Potential issue | 🟡 Minor

Identical branches for trash count update.

Lines 432-438 (sessionTrashCountLoadedOnce true) and lines 439-446 (else) contain identical logic. This appears to be unintended duplication.

🔧 Consolidate duplicate branches
                 if (this.sessionTrashLoadedOnce) {
                     this.prependSessionTrashItem(this.buildSessionTrashItemFromSession(session, res), {
                         totalCount: res && res.totalCount !== undefined ? res.totalCount : undefined
                     });
-                } else if (this.sessionTrashCountLoadedOnce) {
-                    this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
-                        res && res.totalCount !== undefined
-                            ? res.totalCount
-                            : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
-                        this.sessionTrashItems
-                    );
                 } else {
                     this.sessionTrashTotalCount = this.normalizeSessionTrashTotalCount(
                         res && res.totalCount !== undefined
                             ? res.totalCount
                             : (this.normalizeSessionTrashTotalCount(this.sessionTrashTotalCount, this.sessionTrashItems) + 1),
                         this.sessionTrashItems
                     );
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-actions.mjs` around lines 428 - 446, The
code has duplicate logic in the branches guarded by sessionTrashCountLoadedOnce
and the final else; consolidate them by removing the redundant branch and using
a single fallback that updates sessionTrashTotalCount via
normalizeSessionTrashTotalCount when sessionTrashLoadedOnce is false; keep the
separate branch that calls
prependSessionTrashItem(buildSessionTrashItemFromSession(...)) when
sessionTrashLoadedOnce is true, and in the fallback compute the new count with
res?.totalCount !== undefined ? res.totalCount :
(normalizeSessionTrashTotalCount(this.sessionTrashTotalCount,
this.sessionTrashItems) + 1) and assign to this.sessionTrashTotalCount using
normalizeSessionTrashTotalCount(this.sessionTrashTotalCount,
this.sessionTrashItems).
🧹 Nitpick comments (1)
web-ui/styles/feedback.css (1)

108-108: Trailing comment appears incomplete.

Line 108 has a comment /* 内容区域包裹器 - 稳定高度 */ with no subsequent CSS rule. This may be intentional (placeholder) or truncated. Consider removing if unused or completing if needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/feedback.css` at line 108, The trailing comment "/* 内容区域包裹器 -
稳定高度 */" in feedback.css stands alone with no associated CSS rule; either remove
this orphan comment or add the intended rule for the content-area wrapper (e.g.,
the selector you expect such as .content-wrapper or .feedback-content)
specifying the stable height properties. Update the file by locating the comment
and either delete it if unused, or replace it with the complete CSS block for
the content-area wrapper (including height/min-height and any overflow rules) so
the comment is not left dangling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1251eaab-d297-47c4-a4da-6473d06306aa

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and a5c327f.

📒 Files selected for processing (75)
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
🪛 HTMLHint (1.9.2)
web-ui/partials/index/modal-confirm-toast.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 27-27: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/panel-config-openclaw.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/layout-footer.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 10-10: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 12-12: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 69-69: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/modals-basic.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-claude.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-market.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/layout-header.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-settings.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-config-template-agents.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-codex.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-sessions.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-skills.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-openclaw-config.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 11

♻️ Duplicate comments (36)
cli.js (3)

10023-10037: ⚠️ Potential issue | 🟠 Major

Handle FSWatcher errors locally.

fs.watch() can emit 'error' when a watched path disappears or becomes inaccessible. With no listener on watcher, a directory rename/delete can terminate codexmate run instead of letting this helper resync or drop the stale entry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10023 - 10037, fs.watch() watcher instances currently
lack an 'error' listener so an emitted error can crash the process; add a
watcher.on('error', ...) handler on the created watcher (the local variable
watcher) that handles failures by logging the error, closing the watcher,
removing its entry from watcherEntries keyed by watchKey, and invoking
syncDirectoryTree(target) or otherwise re-scheduling/resyncing the path when
appropriate to recover from deleted/renamed directories; ensure the handler
references watcher, watchKey, watcherEntries, target, and syncDirectoryTree so
stale watchers are cleaned up instead of letting the error bubble and terminate
the process.

10708-10724: ⚠️ Potential issue | 🟠 Major

Consult bundled readers before returning 404.

Lines 10709-10713 return 404 before PUBLIC_WEB_UI_DYNAMIC_ASSETS is checked. In source-less runs that makes /web-ui/index.html, /web-ui/app.js, /web-ui/logic.mjs, and /web-ui/styles.css unreachable even though bundled readers exist.

Non-breaking fix
             const relativePath = path.relative(webDir, filePath).replace(/\\/g, '/');
-            if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
-                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
-                res.end('Not Found');
-                return;
-            }
             const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath);
             if (dynamicAsset) {
                 res.writeHead(200, { 'Content-Type': dynamicAsset.mime });
                 res.end(dynamicAsset.reader(filePath), 'utf-8');
                 return;
             }
             if (!PUBLIC_WEB_UI_STATIC_ASSETS.has(relativePath)) {
                 res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
                 res.end('Not Found');
                 return;
             }
+            if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
+                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
+                res.end('Not Found');
+                return;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10708 - 10724, The handler currently checks
fs.existsSync/filePath and returns 404 before consulting bundled readers, making
bundled dynamic assets unreachable; change the order so you look up
PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath) first and, if found, return its
MIME and reader output (res.writeHead + res.end) without requiring the on-disk
file, otherwise proceed to check fs.existsSync(filePath) and
fs.statSync(...).isFile() and then fall back to
PUBLIC_WEB_UI_STATIC_ASSETS.has(relativePath) as the final static check; refer
to the variables relativePath, filePath, PUBLIC_WEB_UI_DYNAMIC_ASSETS, and
PUBLIC_WEB_UI_STATIC_ASSETS to locate and reorder the logic.

10016-10031: ⚠️ Potential issue | 🔴 Critical

Direct file watches still break after atomic-save replaces the file.

When target is a file, addWatcher() still uses fs.watch(target, ...). Editors that save via temp-file + rename replace that inode, so the watcher on legacyHtmlPath can stop seeing subsequent edits until the process restarts. Watching the parent directory and filtering on path.basename(target) avoids that without changing external behavior.

Also applies to: 10078-10080

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10016 - 10031, In addWatcher(), when the watched target
is a file (isDirectory === false), stop calling fs.watch(target, ...) directly
and instead watch the parent directory: compute const parent =
path.dirname(target) and const base = path.basename(target), use
fs.watch(parent, { recursive }, ...) and update the watchKey to use the parent
(e.g. `${recursive ? 'recursive' : 'plain'}:${parent}`) to avoid duplicate
registrations, and inside the callback filter events so you only call trigger({
target, eventType, filename }) when filename exists and path.basename(filename)
=== base (and still enforce the extension regex); apply the same
parent-directory-watch pattern for the other similar places that watch files.
web-ui/modules/app.computed.dashboard.mjs (1)

12-21: ⚠️ Potential issue | 🟡 Minor

Let claudeModelHasList() mirror the fallback option logic.

Line 13 only checks claudeModels.length, but Lines 16-20 already prepend currentClaudeModel when the fetched list is empty or missing it. That can still route the UI through the “no list” path even though there is a valid option to display.

🛠️ Minimal fix
         claudeModelHasList() {
-            return Array.isArray(this.claudeModels) && this.claudeModels.length > 0;
+            return (Array.isArray(this.claudeModels) && this.claudeModels.length > 0)
+                || !!String(this.currentClaudeModel || '').trim();
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.computed.dashboard.mjs` around lines 12 - 21, Update
claudeModelHasList to mirror the fallback logic in claudeModelOptions: compute
the effective list the same way (use Array.isArray(this.claudeModels) ?
[...this.claudeModels] : [], then trim this.currentClaudeModel and unshift it if
non-empty and not already present) and return whether that effective list has
length > 0; reference claudeModelHasList, claudeModelOptions, claudeModels, and
currentClaudeModel to locate and implement the change.
web-ui/source-bundle.cjs (1)

218-220: ⚠️ Potential issue | 🟡 Minor

Resolve relative module entry paths before bundling.

Line 219 forwards entryPath unchanged. If a caller passes a relative module path here, nested imports resolve from process.cwd() instead of web-ui/, which is the same failure mode that produced /modules/app.constants.mjs ENOENT earlier.

🛠️ Minimal fix
 function readExecutableBundledJavaScriptModule(entryPath) {
-    return bundleExecutableJavaScriptFile(entryPath, { preserveExports: true });
+    const resolvedEntryPath = path.isAbsolute(entryPath)
+        ? entryPath
+        : path.resolve(__dirname, entryPath);
+    return bundleExecutableJavaScriptFile(resolvedEntryPath, { preserveExports: true });
 }

Run the following script to inspect the helper and its current callers; if the implementation stays as-is, all callers need to pass absolute paths:

#!/bin/bash
sed -n '214,220p' web-ui/source-bundle.cjs
echo
rg -n -C2 'readExecutableBundledJavaScriptModule|readExecutableBundledWebUiModule' .
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/source-bundle.cjs` around lines 218 - 220,
readExecutableBundledJavaScriptModule forwards a potentially relative entryPath
to bundleExecutableJavaScriptFile causing nested imports to resolve from the
cwd; before calling bundleExecutableJavaScriptFile in
readExecutableBundledJavaScriptModule, resolve the entryPath to an absolute path
(e.g. via path.resolve(__dirname, entryPath) or otherwise resolving relative to
the web-ui module root) so nested imports resolve from the web-ui directory;
update the call in readExecutableBundledJavaScriptModule to pass the resolved
absolute path to bundleExecutableJavaScriptFile.
web-ui/modules/app.methods.session-timeline.mjs (1)

14-20: ⚠️ Potential issue | 🟡 Minor

Re-measure the timeline when the preview header changes height.

Line 18 only updates the CSS offset. A header wrap/unwrap also shifts message tops, so the cached offsets and active anchor stay stale until the next scroll or window resize.

🛠️ Minimal fix
         observeSessionPreviewHeaderResize() {
             this.disconnectSessionPreviewHeaderResizeObserver();
             if (!this.sessionPreviewHeaderEl || typeof ResizeObserver !== 'function') return;
             this.sessionPreviewHeaderResizeObserver = new ResizeObserver(() => {
                 this.updateSessionTimelineOffset();
+                this.invalidateSessionTimelineMeasurementCache();
+                if (
+                    this.sessionTimelineEnabled
+                    && this.mainTab === 'sessions'
+                    && this.getMainTabForNav() === 'sessions'
+                    && this.sessionPreviewRenderEnabled
+                ) {
+                    this.scheduleSessionTimelineSync();
+                }
             });
             this.sessionPreviewHeaderResizeObserver.observe(this.sessionPreviewHeaderEl);
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-timeline.mjs` around lines 14 - 20, The
ResizeObserver in observeSessionPreviewHeaderResize currently only calls
this.updateSessionTimelineOffset(), which updates CSS offset but doesn't
recompute cached message top offsets or the active anchor; update the observer
callback to also re-measure message positions and refresh the active anchor by
calling the existing method that recomputes message offsets (or add a new method
like remeasureSessionMessageOffsets/recalculateSessionMessageOffsets if none
exists), ensuring you use the same instance
(this.sessionPreviewHeaderResizeObserver) and updateSessionTimelineOffset
together so header wrap/unwrap triggers a full re-measure and anchor refresh.
tests/unit/helpers/web-ui-app-options.mjs (1)

48-68: ⚠️ Potential issue | 🟠 Major

captureHeadBundledAppOptions() still uses the current snapshot as its baseline.

Line 49 hard-codes HEAD, and Line 58 still walks the working-tree import graph. On a clean PR checkout, the head fixture and the current fixture can come from the same tree, so the parity assertions can pass without comparing against an independent baseline.

Also applies to: 232-236

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/helpers/web-ui-app-options.mjs` around lines 48 - 68, The head
fixture is being built from the working tree because createHeadWebUiFixture
calls sourceBundle.collectJavaScriptFiles on the live entryPath; instead,
populate a temporary fixture from the HEAD commit first (use readHeadProjectFile
to write all HEAD web-ui files into fixtureRoot, e.g. enumerate HEAD files under
web-ui via git ls-tree -r --name-only HEAD web-ui and write each with
readHeadProjectFile), then call sourceBundle.collectJavaScriptFiles against the
fixture's entry (path.join(fixtureRoot, HEAD_WEB_UI_ENTRY basename)) so
dependencyPaths reflect the HEAD snapshot; update createHeadWebUiFixture and any
related captureHeadBundledAppOptions usage to use this fixture-based collection
rather than the working-tree entryPath.
web-ui/modules/api.mjs (1)

1-3: ⚠️ Potential issue | 🟠 Major

Guard location before reading API_BASE to avoid import-time crashes outside browsers.

Line 1 dereferences bare location; importing this module in non-browser contexts can throw before fallback is applied.

Minimal non-breaking fix
-export const API_BASE = (location && location.origin && location.origin !== 'null')
-    ? location.origin
-    : 'http://localhost:3737';
+const safeOrigin = (typeof location !== 'undefined' && location && location.origin && location.origin !== 'null')
+    ? location.origin
+    : '';
+
+export const API_BASE = safeOrigin || 'http://localhost:3737';
In JavaScript ES modules, does evaluating `location && location.origin` throw a `ReferenceError` when `location` is undefined (e.g., Node.js)? Is `typeof location !== 'undefined'` the correct guard pattern?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/api.mjs` around lines 1 - 3, The API_BASE export currently
dereferences location at import time which can throw in non-browser runtimes;
change the guard to check typeof location !== 'undefined' before accessing
location.origin (e.g., use typeof location !== 'undefined' && location.origin &&
location.origin !== 'null') so API_BASE only reads location.origin in browser
contexts and falls back to 'http://localhost:3737'; update the expression that
defines API_BASE to use this typeof guard around location and location.origin
references.
web-ui/styles/titles-cards.css (1)

263-274: ⚠️ Potential issue | 🟠 Major

Hidden card actions are still keyboard-focusable while invisible.

Line 263 hides actions only visually; Line 271 reveals only on hover. Keyboard focus can land on invisible controls.

Minimal non-breaking fix
 .card-actions {
     display: flex;
     gap: 8px;
     opacity: 0;
+    visibility: hidden;
+    pointer-events: none;
     transform: translateX(4px);
     transition: all var(--transition-normal) var(--ease-spring);
 }
 
-.card:hover .card-actions {
+.card:hover .card-actions,
+.card:focus-within .card-actions {
     opacity: 1;
+    visibility: visible;
+    pointer-events: auto;
     transform: translateX(0);
 }
 
 .mode-cards .card-actions {
     opacity: 1;
+    visibility: visible;
+    pointer-events: auto;
     transform: translateX(0);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/titles-cards.css` around lines 263 - 274, .card-actions is only
visually hidden which leaves its controls focusable; update the CSS so hidden
actions cannot receive keyboard focus by using visibility and pointer-events
instead of only opacity/transform: e.g., change .card-actions to include
visibility: hidden; pointer-events: none; and remove them from interaction, and
change .card:hover .card-actions to set visibility: visible; pointer-events:
auto; (keep your transition rules intact). This ensures the .card-actions
elements are both visually hidden and non-focusable until .card:hover triggers
them.
web-ui/partials/index/panel-config-claude.html (1)

54-56: ⚠️ Potential issue | 🟠 Major

Card selection and icon actions are still not keyboard/screen-reader accessible.

Line 54 creates a clickable div without keyboard semantics. Lines 72 and 85 rely on title for icon-only buttons instead of accessible labels.

Minimal non-breaking fix
-<div v-for="(config, name) in claudeConfigs" :key="name"
-     :class="['card', { active: currentClaudeConfig === name }]"
-     `@click`="applyClaudeConfig(name)">
+<div v-for="(config, name) in claudeConfigs" :key="name"
+     :class="['card', { active: currentClaudeConfig === name }]"
+     role="button"
+     tabindex="0"
+     `@click`="applyClaudeConfig(name)"
+     `@keydown.enter.prevent`="applyClaudeConfig(name)"
+     `@keydown.space.prevent`="applyClaudeConfig(name)">
@@
-<button class="card-action-btn" `@click`="openEditConfigModal(name)" title="编辑">
+<button class="card-action-btn" `@click`="openEditConfigModal(name)" title="编辑" aria-label="编辑配置">
@@
-<button class="card-action-btn delete" `@click`="deleteClaudeConfig(name)" title="删除">
+<button class="card-action-btn delete" `@click`="deleteClaudeConfig(name)" title="删除" aria-label="删除配置">

Also applies to: 72-73, 85-86

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-claude.html` around lines 54 - 56, Wrap
the clickable card div and icon-only controls with proper interactive semantics:
make the card element used with claudeConfigs/currentClaudeConfig that invokes
applyClaudeConfig(name) keyboard-accessible by adding role="button",
tabindex="0", and a keydown handler that triggers applyClaudeConfig(name) for
Enter/Space (and set aria-pressed or aria-selected to reflect
currentClaudeConfig === name); replace icon-only elements that rely on title
(lines around the icons) with either a native <button> or add an explicit
aria-label describing the action and ensure they are focusable (tabindex/role)
so screen readers and keyboard users can activate them.
web-ui/partials/index/modals-basic.html (3)

135-138: ⚠️ Potential issue | 🟠 Major

Use a real button for model removal.

span + @click is not keyboard-focusable, so keyboard users cannot trigger this action.

Suggested fix
-                        <span class="btn-remove-model" `@click`="removeModel(model)">删除</span>
+                        <button type="button" class="btn-remove-model" `@click`="removeModel(model)">删除</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modals-basic.html` around lines 135 - 138, Replace the
non-focusable span used for the remove action with a real button element so
keyboard users can activate it; update the element that currently has class
"btn-remove-model" and the `@click`="removeModel(model)" listener to be a <button
type="button">, ensure it retains the same class for styling, and add an
accessible label (e.g., aria-label or include visually hidden text) so the
removeModel(model) handler and visuals remain unchanged while improving keyboard
and screen-reader accessibility.

15-16: ⚠️ Potential issue | 🟠 Major

Mask the provider and Claude API key inputs.

These fields are still plain-text inputs. Switch them to type="password" and add autocomplete="off" / spellcheck="false" so keys are not exposed during entry or edit.

Also applies to: 100-103, 157-159, 182-184

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modals-basic.html` around lines 15 - 16, The API key
inputs are plain-text; change the input elements bound to
v-model="newProvider.key" and the other provider/API key fields (the inputs at
the other occurrences referenced) to use type="password", and add attributes
autocomplete="off" and spellcheck="false" so keys are masked and not
auto-completed or spellchecked; update the input elements in the modal template
where v-model binds provider/newProvider API key fields accordingly (the inputs
with v-model="newProvider.key" and the other provider API key v-models
mentioned).

2-4: ⚠️ Potential issue | 🟠 Major

Add dialog semantics to every modal container.

Each .modal block still lacks role="dialog", aria-modal="true", and a title reference via aria-labelledby, so assistive tech will not announce modal context.

Also applies to: 26-29, 88-90, 114-116, 131-133, 149-151, 174-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modals-basic.html` around lines 2 - 4, The modal
containers are missing accessibility attributes; for each .modal element (e.g.,
the add-provider modal controlled by showAddModal and the other modals
referenced in the comment) add role="dialog" and aria-modal="true" and set
aria-labelledby to the id of the modal title; give each modal title element (the
.modal-title inside each modal) a unique id (e.g., addModalTitle) and use that
id in the containing .modal's aria-labelledby so screen readers announce the
title.
web-ui/partials/index/modal-config-template-agents.html (2)

7-11: ⚠️ Potential issue | 🟠 Major

Lock the template editor while apply is running.

configTemplateApplying disables only the confirm button; the textarea stays editable, so the modal can drift from the content being written.

Suggested fix
                     <textarea
                         v-model="configTemplateContent"
                         class="form-input template-editor"
                         spellcheck="false"
+                        :readonly="configTemplateApplying"
                         placeholder="在这里编辑 config.toml 模板内容"></textarea>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-config-template-agents.html` around lines 7 - 11,
The textarea bound to configTemplateContent (class "template-editor") remains
editable while applying; update the modal to make the textarea
read-only/disabled when configTemplateApplying is true so users cannot edit
while apply is running. Locate the template-editor textarea
(v-model="configTemplateContent") and add a reactive attribute (e.g.,
:readonly="configTemplateApplying" or :disabled="configTemplateApplying") and
optionally a visual disabled class when configTemplateApplying is true to match
the confirm button behavior.

1-3: ⚠️ Potential issue | 🟠 Major

Add dialog semantics to these modal containers.

Both .modal blocks lack role="dialog", aria-modal="true", and a title reference via aria-labelledby, so screen readers will not announce modal context.

Also applies to: 26-29

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-config-template-agents.html` around lines 1 - 3,
The modal containers rendered when showConfigTemplateModal is true (and the
other identical modal block) need proper dialog semantics; add role="dialog" and
aria-modal="true" to the element with classes "modal modal-wide", and add
aria-labelledby that references a unique id placed on the "modal-title" element
(e.g., give the element with text "Config 模板编辑器(手动确认应用)" an id like
"modal-config-title" and use aria-labelledby="modal-config-title"); ensure the
same pattern is applied to the other modal instance and that the id values are
unique per modal.
web-ui/styles/responsive.css (1)

216-223: ⚠️ Potential issue | 🟡 Minor

Keep .session-item-copy at the 44px mobile tap-target floor.

This block overrides the 44px minimum set a few lines above and shrinks the final hit area back to 20px on small screens.

Suggested fix
     .session-item-copy {
-        width: 20px;
-        height: 20px;
-        min-width: 20px;
-        min-height: 20px;
         border-radius: 6px;
         padding: 2px;
         display: inline-flex;
         align-items: center;
         justify-content: center;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/responsive.css` around lines 216 - 223, The .session-item-copy
rule reduces the touch target to 20px, violating the 44px mobile tap-target
minimum; update the .session-item-copy declaration (remove or override the
explicit width/height: 20px and min-width/min-height: 20px) so that its minimum
touch area is at least 44px (e.g., set min-width and min-height to 44px or rely
on existing 44px rules) while preserving border-radius, padding and display;
edit the .session-item-copy block to ensure it does not shrink below 44px on
small screens.
web-ui/partials/index/panel-settings.html (1)

9-25: ⚠️ Potential issue | 🟡 Minor

Implement roving tabindex for the settings subtabs.

Both buttons stay in the tab order because tabindex is hard-coded to 0. Only the active subtab should be focusable; the inactive one should be -1.

Suggested fix
                         <button
                             id="settings-tab-backup"
                             role="tab"
                             aria-controls="settings-panel-backup"
                             :aria-selected="settingsTab === 'backup'"
-                            tabindex="0"
+                            :tabindex="settingsTab === 'backup' ? 0 : -1"
                             :class="['config-subtab', { active: settingsTab === 'backup' }]"
                             `@click`="onSettingsTabClick('backup')">
                             备份与导入
                         </button>
                         <button
                             id="settings-tab-trash"
                             role="tab"
                             aria-controls="settings-panel-trash"
                             :aria-selected="settingsTab === 'trash'"
-                            tabindex="0"
+                            :tabindex="settingsTab === 'trash' ? 0 : -1"
                             :class="['config-subtab', { active: settingsTab === 'trash' }]"
                             `@click`="onSettingsTabClick('trash')">
                             回收站
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-settings.html` around lines 9 - 25, The two
subtab buttons (ids settings-tab-backup and settings-tab-trash) currently have a
hard-coded tabindex="0" so both are in the tab order; change each to use a
dynamic binding so only the active subtab is focusable (e.g.,
:tabindex="settingsTab === 'backup' ? 0 : -1" for the backup button and
:tabindex="settingsTab === 'trash' ? 0 : -1" for the trash button), keeping the
existing aria-selected binding and onSettingsTabClick handler intact so keyboard
users can rove focus between subtabs via the active state.
web-ui/partials/index/modal-skills.html (1)

169-170: ⚠️ Potential issue | 🟠 Major

Keep “导入选中” disabled while the import list is still scanning.

This footer action still ignores skillsScanningImports, so importSelectedSkills can run while the selectable key set is being recomputed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-skills.html` around lines 169 - 170, The import
button's disabled condition is missing the scanning flag; update the binding for
the button that triggers importSelectedSkills to also disable when
skillsScanningImports is true (i.e., include "|| skillsScanningImports"
alongside skillsImporting, skillsImportSelectedCount === 0, skillsZipImporting,
skillsExporting, skillsDeleting) so the import action cannot run while the
import list is still scanning/recomputing.
web-ui/styles/base-theme.css (1)

1-1: ⚠️ Potential issue | 🟠 Major

Bundled UI still hard-depends on Google Fonts.

This @import keeps /web-ui from being fully self-contained: every load now depends on a third-party request. Offline installs and restrictive CSP/privacy setups will fail before the fallback stack takes over.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/base-theme.css` at line 1, The CSS currently hard-depends on an
external Google Fonts import in web-ui/styles/base-theme.css (the `@import`
url('https://fonts.googleapis.com/...') line), which breaks offline/strict-CSP
setups; remove that external `@import` and instead load fonts locally or fall back
to system fonts: either replace the import with local `@font-face` declarations
that reference bundled font files (and update build to include those assets) or
delete the `@import` and ensure font-family stacks in base-theme.css use safe
system fallbacks (e.g., fall back to monospace/sans-serif) so the UI is
self-contained and works without third-party network requests.
web-ui/styles/sessions-toolbar-trash.css (1)

17-19: ⚠️ Potential issue | 🟠 Major

grid-column: span 2 can overflow the single-column layout.

When the toolbar collapses to one column, this can create an implicit second track and push the grid wider than the viewport. Use a full-row span here instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/sessions-toolbar-trash.css` around lines 17 - 19,
.session-toolbar-grow currently uses grid-column: span 2 which can create an
implicit second track and overflow a single-column layout; change the rule for
.session-toolbar-grow to span the full row (e.g., set grid-column to cover from
the first to the last grid line such as "grid-column: 1 / -1") so it occupies
the entire row without creating extra tracks.
web-ui/partials/index/layout-header.html (1)

215-251: ⚠️ Potential issue | 🟡 Minor

Don't expose the side-rail entries as a second ARIA tab widget.

These buttons target the same panels as the top tablist, but the panels are still labelled by the top tabs. Leaving role="tab" and aria-controls here creates duplicate tab relationships for assistive tech.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/layout-header.html` around lines 215 - 251, These
side-rail buttons are incorrectly exposed as a second ARIA tablist; change both
the market and settings side entries (elements with id="side-tab-market" and
id="side-tab-settings") to non-tab controls by replacing role="tab" with
role="button" and removing aria-controls and :aria-selected attributes (keep
data-main-tab, the :tabindex binding, and the event handlers
onMainTabPointerDown/onMainTabClick so they still activate the top tabs); update
any code that relied on :aria-selected to instead rely on
mainTab/isMainTabNavActive for visual state only.
web-ui/modules/app.methods.providers.mjs (1)

117-133: ⚠️ Potential issue | 🟡 Minor

Rejected provider/model requests still escape these handlers.

deleteProvider, addModel, and removeModel await api() without a local try/catch. A network or transport failure will still reject out of the UI handler instead of surfacing a message like the other CRUD paths in this module.

Also applies to: 201-223

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 117 - 133, The async
API calls in deleteProvider, addModel, and removeModel currently await api(...)
without local try/catch, so network/transport failures will reject the handler;
wrap each api(...) call in a try/catch that catches thrown errors and calls
this.showMessage(error.message || String(error), 'error') (or similar) and
returns early, preserving the existing success/error handling for API responses;
make sure to apply the same pattern in deleteProvider (around the
delete-provider call) and in the addModel and removeModel methods so all CRUD
paths surface network/transport failures to the UI instead of letting the
promise escape.
web-ui/partials/index/panel-config-codex.html (1)

177-180: ⚠️ Potential issue | 🟠 Major

Provider switching is still mouse-only.

The active-provider card is a plain div with only @click, so keyboard users still cannot focus the switch control itself; focus jumps straight to the nested action buttons. Please make the card focusable and wire Enter/Space to the same handler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-codex.html` around lines 177 - 180, The
provider cards are not keyboard-accessible: make the card element focusable and
behave like a button by adding tabindex="0" and role="button" (or aria-pressed
if it represents toggle state) and wire a keydown handler that calls the same
switchProvider(provider.name) when Enter (key === 'Enter') or Space (key === '
') is pressed; update the template where displayProvidersList,
displayCurrentProvider and switchProvider are used to add these attributes and
the keydown method so keyboard users can focus and activate the card just like a
click.
web-ui/logic.agents-diff.mjs (1)

27-34: ⚠️ Potential issue | 🟠 Major

Preserve trailing blank-line edits in splitDiffLines().

Dropping the final \n makes newline-only edits collapse to “no diff” in cases like '' vs '\n'.

🛠️ Minimal fix
 function splitDiffLines(input) {
-    let normalized = normalizeDiffText(input);
+    const normalized = normalizeDiffText(input);
     if (!normalized) return [];
-    if (normalized.endsWith('\n')) {
-        normalized = normalized.slice(0, -1);
-    }
-    if (!normalized) return [];
-    return normalized.split('\n');
+    const lines = normalized.split('\n');
+    if (lines[lines.length - 1] === '') {
+        lines.pop();
+    }
+    return lines;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.agents-diff.mjs` around lines 27 - 34, splitDiffLines currently
trims a trailing '\n', which loses newline-only edits; remove the logic that
slices off the final newline so that normalizeDiffText's output can be split
into lines including a trailing empty string (preserving blank-line edits). In
other words, inside splitDiffLines (and keeping the early empty-string guard),
stop removing the final '\n' and return normalized.split('\n') directly;
reference splitDiffLines and normalizeDiffText when making this change.
web-ui/modules/app.methods.codex-config.mjs (2)

229-238: ⚠️ Potential issue | 🟠 Major

Handle string entries in providersList here.

This branch only keeps items with provider.name, so a string array of provider names is filtered out and no remote speed tests run.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.codex-config.mjs` around lines 229 - 238, The
current filter on providersList only keeps objects with a .name, so string
entries are dropped; update the logic that builds providers and tasks to accept
string entries by treating an element that is a string as a provider name (e.g.
providerName = typeof provider === 'string' ? provider : provider.name) and use
that providerName when calling runSpeedTest and when constructing the result
object; adjust the initial filter/mapping around providersList, providers, and
the tasks creation (referencing providersList, providers and runSpeedTest) so
strings and objects with name are both handled consistently and errors still map
to the same result shape.

76-87: ⚠️ Potential issue | 🟠 Major

Do not re-apply Codex config after a model-load failure.

If loadModelsForProvider() leaves modelsSource === 'error', the silent re-apply still uses the previous provider’s currentModel.

🛠️ Minimal fix
         async performProviderSwitch(name) {
             await this.waitForCodexApplyIdle();
             this.currentProvider = name;
             await this.loadModelsForProvider(name);
+            if (this.modelsSource === 'error') {
+                return;
+            }
             if (this.modelsSource === 'remote' && this.models.length > 0 && !this.models.includes(this.currentModel)) {
                 this.currentModel = this.models[0];
                 this.modelsHasCurrent = true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.codex-config.mjs` around lines 76 - 87, In
performProviderSwitch, avoid re-applying Codex config when loadModelsForProvider
failed: after await this.loadModelsForProvider(name) check this.modelsSource !==
'error' before mutating this.currentModel/this.modelsHasCurrent and before
calling applyCodexConfigDirect; only set this.currentModel = this.models[0] and
this.modelsHasCurrent = true when this.modelsSource is not 'error' and
this.models.length > 0, and only call await this.applyCodexConfigDirect({
silent: true }) when getProviderConfigModeMeta(this.configMode) is true AND
this.modelsSource !== 'error'.
web-ui/modules/app.methods.claude-config.mjs (1)

64-90: ⚠️ Potential issue | 🟠 Major

Catch rejected apply-claude-config calls in both apply paths.

Both code paths assume the API resolves to a response object. A rejection or falsy return becomes an unhandled error and the user never gets the failure message.

Also applies to: 136-155

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.claude-config.mjs` around lines 64 - 90, The
apply-claude-config API call can reject or return a falsy value, causing
unhandled exceptions; update saveAndApplyConfig (and the other similar handler
around lines 136-155) to await the api('apply-claude-config', { config }) inside
a try/catch (or append .catch) and treat undefined/null responses as failures,
then call this.showMessage with an error message and ensure
this.closeEditConfigModal()/this.refreshClaudeModelContext() run only on
success; reference the saveAndApplyConfig function and the other apply handler
so you add identical rejection handling and falsy-response checks in both
places.
web-ui/modules/app.methods.openclaw-persist.mjs (2)

22-30: ⚠️ Potential issue | 🟠 Major

Open the saved preset when editing an OpenClaw config.

This modal starts with empty content and force-loads the live file, so saving can overwrite the selected preset with on-disk content instead of the preset the user chose.

🛠️ Minimal fix
         openOpenclawEditModal(name) {
+            const config = this.openclawConfigs[name] || {};
             this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
             this.openclawEditing = {
                 name,
-                content: '',
+                content: typeof config.content === 'string' ? config.content : '',
                 lockName: true
             };
-            void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
+            void this.loadOpenclawConfigFromFile({ silent: true, force: false, fallbackToTemplate: false });
             this.showOpenclawConfigModal = true;
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-persist.mjs` around lines 22 - 30, The
modal is initialized with an empty content and then force-loads the on-disk
file, which can overwrite the chosen preset; update openOpenclawEditModal to
prefill this.openclawEditing.content with the selected preset's content (use the
passed-in name to look up the preset) and call loadOpenclawConfigFromFile
without force:true (or remove the force flag) so it won't override the preset
content; touch the openOpenclawEditModal function and the
loadOpenclawConfigFromFile invocation and ensure openclawEditing.name and
lockName behavior remain unchanged.

161-179: ⚠️ Potential issue | 🟠 Major

Catch apply-openclaw-config failures in applyOpenclawConfig().

Unlike saveAndApplyOpenclawConfig(), this path lets API rejections escape and shows no failure feedback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-persist.mjs` around lines 161 - 179, The
applyOpenclawConfig function currently awaits api('apply-openclaw-config')
without catching rejections so failures can escape; wrap the api call in a
try/catch around the await api(...) inside applyOpenclawConfig, and on catch
call this.showMessage with an error message (use the caught error.message or a
fallback like '应用配置失败') and return early; only set this.openclawConfigPath,
this.openclawConfigExists and show the success message inside the successful
branch after the awaited api resolves. Ensure you reference applyOpenclawConfig,
api, this.showMessage, this.openclawConfigPath and this.openclawConfigExists
when applying the fix.
web-ui/modules/app.methods.session-browser.mjs (2)

87-119: ⚠️ Potential issue | 🟠 Major

Scope path-option request tokens per source.

A request for one source invalidates any in-flight request for another, so quick source toggles can drop a valid response and leave that source stale.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-browser.mjs` around lines 87 - 119, The
current global sessionPathRequestSeq in loadSessionPathOptions causes requests
for one source to invalidate in-flight requests for another; change to a
per-source sequence/map (e.g. sessionPathRequestSeqMap keyed by source) and use
sessionRequestSeq = ++this.sessionPathRequestSeqMap[source] when issuing the API
call and compare that same per-source value before applying results; also
consider switching sessionPathOptionsLoading to a per-source flag
(sessionPathOptionsLoadingMap[source]) and keep updating
sessionPathOptionsLoadedMap[source] and calling
syncSessionPathOptionsForSource(source, res.paths, true) only when the
per-source sequence matches to avoid dropping valid responses.

370-410: ⚠️ Potential issue | 🟠 Major

Invalidate stale standalone loads when activeSession is cleared.

Clearing the selection does not advance sessionStandaloneRequestSeq, so an older session-plain response can still apply. The later fallback to this.activeSession also dereferences null in that case.

🛠️ Minimal fix
         async loadSessionStandalonePlain() {
             if (!this.activeSession) {
+                this.sessionStandaloneRequestSeq += 1;
+                this.sessionStandaloneLoading = false;
                 this.sessionStandaloneText = '';
                 this.sessionStandaloneTitle = '会话';
                 this.sessionStandaloneSourceLabel = '';
                 this.sessionStandaloneError = '';
                 return;
             }
 
+            const activeSession = this.activeSession;
             const requestSeq = ++this.sessionStandaloneRequestSeq;
             this.sessionStandaloneLoading = true;
             this.sessionStandaloneError = '';
             try {
                 const res = await api('session-plain', {
-                    source: this.activeSession.source,
-                    sessionId: this.activeSession.sessionId,
-                    filePath: this.activeSession.filePath
+                    source: activeSession.source,
+                    sessionId: activeSession.sessionId,
+                    filePath: activeSession.filePath
                 });
@@
-                this.sessionStandaloneSourceLabel = res.sourceLabel || this.activeSession.sourceLabel || '';
-                this.sessionStandaloneTitle = res.sessionId || this.activeSession.title || '会话';
+                this.sessionStandaloneSourceLabel = res.sourceLabel || activeSession.sourceLabel || '';
+                this.sessionStandaloneTitle = res.sessionId || activeSession.title || '会话';
                 this.sessionStandaloneText = typeof res.text === 'string' ? res.text : '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-browser.mjs` around lines 370 - 410, In
loadSessionStandalonePlain, increment sessionStandaloneRequestSeq when
activeSession is falsy to invalidate any in-flight responses (i.e., do
++this.sessionStandaloneRequestSeq before the early return), and avoid
dereferencing this.activeSession after awaiting by capturing the current session
into a local const (e.g., const current = this.activeSession) at the top and
using current when reading source, sessionId, title, and filePath in the
response handling and when computing fallbacks
(sessionStandaloneSourceLabel/sessionStandaloneTitle); also ensure you still
check requestSeq !== this.sessionStandaloneRequestSeq before applying results
and set sessionStandaloneLoading appropriately.
web-ui/modules/app.computed.session.mjs (1)

111-117: ⚠️ Potential issue | 🟠 Major

Use sessionTrashCount for trash pagination.

These getters stop at the number of loaded entries, so the UI can show “no more” while sessionTrashTotalCount still reports additional trash items.

🛠️ Minimal fix
         sessionTrashHasMoreItems() {
-            const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
-            return this.visibleSessionTrashItems.length < totalItems;
+            return this.visibleSessionTrashItems.length < this.sessionTrashCount;
         },
         sessionTrashHiddenCount() {
-            const totalItems = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
-            return Math.max(0, totalItems - this.visibleSessionTrashItems.length);
+            return Math.max(0, this.sessionTrashCount - this.visibleSessionTrashItems.length);
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.computed.session.mjs` around lines 111 - 117, The getters
sessionTrashHasMoreItems and sessionTrashHiddenCount currently derive totalItems
from this.sessionTrashItems.length which only reflects loaded entries; update
both to use this.sessionTrashCount (the pagination-aware total) instead: in
sessionTrashHasMoreItems return this.visibleSessionTrashItems.length <
this.sessionTrashCount, and in sessionTrashHiddenCount return Math.max(0,
this.sessionTrashCount - this.visibleSessionTrashItems.length); keep the
existing visibleSessionTrashItems references and bounds check.
web-ui/logic.sessions.mjs (1)

27-48: ⚠️ Potential issue | 🟡 Minor

Return normalized source and pathFilter from buildSessionListParams().

The query gating already uses normalized source semantics, but the returned payload still forwards the raw values.

🛠️ Minimal fix
 export function buildSessionListParams(options = {}) {
     const {
         source = 'all',
         pathFilter = '',
         query = '',
         roleFilter = 'all',
         timeRangePreset = 'all',
         limit = 200
     } = options;
-    const queryValue = isSessionQueryEnabled(source) ? query : '';
+    const normalizedSource = normalizeSessionSource(source, 'all');
+    const normalizedPathFilter = normalizeSessionPathFilter(pathFilter);
+    const queryValue = isSessionQueryEnabled(normalizedSource) ? query : '';
     return {
-        source,
-        pathFilter,
+        source: normalizedSource,
+        pathFilter: normalizedPathFilter,
         query: queryValue,
         queryMode: 'and',
         queryScope: 'content',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.sessions.mjs` around lines 27 - 48, buildSessionListParams
currently returns the raw source and pathFilter even though query gating uses
normalized source semantics; modify buildSessionListParams to normalize both
source and pathFilter before including them in the returned object by reusing
the same normalization logic used for query gating (i.e., call the existing
normalizer(s) that isSessionQueryEnabled relies on or invoke the same
normalization path) so the returned payload forwards normalized values for
source and pathFilter rather than the raw inputs.
web-ui/modules/app.methods.startup-claude.mjs (2)

24-90: ⚠️ Potential issue | 🟠 Major

Abort loadAll() when either startup call fails.

Only statusRes.error is handled, and that failure path still falls through to the later model/profile loads. If api('list') fails, providersList becomes undefined; if api('status') fails, the method still continues with stale state after finally.

Minimal fix
                 const [statusRes, listRes] = await Promise.all([api('status'), api('list')]);
 
-                if (statusRes.error) {
-                    this.initError = statusRes.error;
+                if (statusRes.error || listRes.error) {
+                    this.initError = statusRes.error || listRes.error;
+                    return;
                 } else {
                     this.currentProvider = statusRes.provider;
                     this.currentModel = statusRes.model;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.startup-claude.mjs` around lines 24 - 90, The
startup sequence in loadAll() must abort when either api('status') or
api('list') fails: detect listRes.error (and statusRes.error) and set
this.initError accordingly, then return early (or rethrow) before the finally
block's preserveLoading handling so we don't proceed to
loadModelsForProvider(this.currentProvider) or loadCodexAuthProfiles() with
invalid/stale state; update the try/catch around Promise.all([api('status'),
api('list')]) (and the catch that sets this.initError = '连接失败: ' + e.message) to
ensure a failing statusRes or listRes stops execution (e.g., set initError and
return) rather than falling through.

336-342: ⚠️ Potential issue | 🟠 Major

maybeShowStarPrompt() can surface a storage failure as a fake connection error.

This runs inside loadAll()'s startup try. If localStorage is unavailable or throws, startup jumps to the catch block and sets initError to a connection failure even though the backend calls succeeded.

Minimal fix
         maybeShowStarPrompt() {
             const storageKey = 'codexmateStarPrompted';
-            if (localStorage.getItem(storageKey)) {
-                return;
-            }
-            this.showMessage('欢迎到 GitHub 点 Star', 'info');
-            localStorage.setItem(storageKey, '1');
+            try {
+                if (localStorage.getItem(storageKey)) {
+                    return;
+                }
+                this.showMessage('欢迎到 GitHub 点 Star', 'info');
+                localStorage.setItem(storageKey, '1');
+            } catch (_) {}
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.startup-claude.mjs` around lines 336 - 342,
maybeShowStarPrompt() can throw if localStorage is unavailable, causing
loadAll()'s startup try to jump to the catch and incorrectly set initError; wrap
the localStorage access in a small try/catch (or check for window/localStorage
existence) inside maybeShowStarPrompt so any storage errors are caught and
ignored (log optionally) and do not propagate, keeping the rest of
loadAll()/startup flow intact and preventing a fake connection error.
web-ui/modules/app.methods.runtime.mjs (1)

188-196: ⚠️ Potential issue | 🟠 Major

Prevent overlapping restore requests for the same target directory.

claudeImportLoading / codexImportLoading are set here but never checked before a new import starts. Re-selecting a file while a restore is still in flight can issue concurrent restore-* writes against the same config directory.

Minimal fix
         async importBackupFile(type, file) {
             const maxSize = 200 * 1024 * 1024;
             const loadingKey = type === 'claude' ? 'claudeImportLoading' : 'codexImportLoading';
+            if (this[loadingKey]) {
+                this.resetImportInput(type);
+                return;
+            }
             if (file.size > maxSize) {
                 this.showMessage('备份文件过大,限制 200MB', 'error');
                 this.resetImportInput(type);
                 return;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.runtime.mjs` around lines 188 - 196, Add an early
guard in importBackupFile to prevent starting a new import when the same target
is already running: after computing loadingKey in importBackupFile, check if
this[loadingKey] is true and if so call this.showMessage('备份正在恢复中,请稍后',
'warning') (or similar) and this.resetImportInput(type) then return; also ensure
that the existing code sets this[loadingKey] = false in all error/exit paths
(use a finally block or explicit resets) so the flag is cleared after the
restore completes; reference importBackupFile and the claudeImportLoading /
codexImportLoading keys.
🧹 Nitpick comments (1)
web-ui/styles/feedback.css (1)

108-108: Trailing comment suggests incomplete section.

The comment /* 内容区域包裹器 - 稳定高度 */ at the end of the file suggests a section that may have been intended but not completed. If this is intentional placeholder for future work, consider adding a TODO or removing the orphaned comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/feedback.css` at line 108, The trailing orphaned CSS comment
"/* 内容区域包裹器 - 稳定高度 */" indicates an incomplete section; either remove this
unused comment or replace it with a clear TODO comment (e.g., "/* TODO:
implement stable-height content wrapper */") so intent is explicit; update the
comment near the end of web-ui/styles/feedback.css (the trailing "内容区域包裹器 -
稳定高度" comment) accordingly to avoid misleading readers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fcb5d30d-4d1a-4ec1-adeb-cfedea349300

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and a5c327f.

📒 Files selected for processing (75)
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 4, 2026

@coderabbitai re-review
Stop making breaking changes, do a proper review!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

@github-actions[bot]: Understood. I sincerely apologize for the repeated breaking change suggestions — this is unacceptable and I take full responsibility. Triggering a new full review now with an absolute hard constraint:

  • ✅ Correctness issues only
  • ✅ Safety issues only
  • ✅ Quality issues only
  • No breaking changes — period
  • ❌ No refactoring suggestions
  • ❌ No API/interface changes
  • ❌ No behavior alterations

This constraint has been reinforced across PRs #55, #59, #67, and #68 and will be strictly honored.

Review strictly limited to correctness, safety, and quality within the existing design. No breaking changes, no refactoring, no API/interface/behavior alterations. Constraint reinforced across PRs #55, #59, #67, and #68.


🧠 Learnings used
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
✅ Actions performed

Full review triggered.

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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

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)

10738-10754: ⚠️ Potential issue | 🟠 Major

Guard allowlisted static assets against edit-time TOCTOU races.

Line 10738 performs fs.existsSync() followed by fs.statSync(), and line 10754 creates a read stream without an error listener. If a frontend file is renamed or deleted between the stat check and stream creation, fs.statSync() can throw synchronously, or fs.createReadStream() can emit an unhandled error event, causing the server to crash instead of returning a 404/500 response. This is problematic during development when frontend files are actively being edited.

Minimal hardening
-            if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
+            let stat = null;
+            try {
+                stat = fs.statSync(filePath);
+            } catch (_) {
+                res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
+                res.end('Not Found');
+                return;
+            }
+            if (!stat.isFile()) {
                 res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
                 res.end('Not Found');
                 return;
             }
@@
-            res.writeHead(200, { 'Content-Type': mime });
-            fs.createReadStream(filePath).pipe(res);
+            res.writeHead(200, { 'Content-Type': mime });
+            const stream = fs.createReadStream(filePath);
+            stream.on('error', (error) => {
+                if (!res.headersSent) {
+                    const status = error && error.code === 'ENOENT' ? 404 : 500;
+                    res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
+                    res.end(status === 404 ? 'Not Found' : 'Internal Server Error');
+                } else {
+                    try { res.destroy(error); } catch (_) {}
+                }
+            });
+            stream.pipe(res);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10738 - 10754, Replace the fragile existsSync/statSync +
direct createReadStream pattern with a single guarded flow: call
fs.statSync(filePath) inside a try/catch (drop the separate fs.existsSync
check), verify stat.isFile() and on failure send 404, then create the read
stream and attach an 'error' listener to the stream to handle ENOENT/other fs
errors by sending a 404/500 response and ensuring the response is ended; ensure
you also wrap the stat call to catch synchronous throws and respond
appropriately. Use the existing symbols filePath, fs.statSync,
res.writeHead/res.end and fs.createReadStream to locate and implement the
changes.
♻️ Duplicate comments (3)
web-ui/modules/app.methods.startup-claude.mjs (2)

295-323: ⚠️ Potential issue | 🟠 Major

Clear claudeModelsLoading on the unlimited early-return path.

Line 315 returns after advancing claudeModelsRequestSeq, but before Line 322 ever reinitializes the loading lifecycle. If a previous models-by-url request was still in flight, its finally will no longer clear the flag, so the Claude model spinner can stay stuck after switching to an external-credential config.

Minimal non-breaking patch
             if (!apiKey && externalCredentialType) {
                 this.claudeModels = [];
                 this.claudeModelsSource = 'unlimited';
                 this.claudeModelsHasCurrent = true;
+                this.claudeModelsLoading = false;
                 return;
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.startup-claude.mjs` around lines 295 - 323, The
early-return branch in loadClaudeModels that handles the "unlimited" external
credential path sets this.claudeModels and this.claudeModelsSource then returns
without clearing the loading flag; ensure you clear the loading lifecycle by
setting this.claudeModelsLoading = false before returning in that branch
(method: loadClaudeModels; fields: claudeModelsLoading, claudeModelsRequestSeq,
claudeModels, claudeModelsSource, claudeModelsHasCurrent) so the spinner won't
remain stuck when switching to an external-credential config.

139-149: ⚠️ Potential issue | 🟠 Major

Ignore stale failures in both model loaders.

Line 139 and Line 363 only guard the post-await success path. If an older request rejects after the provider/config has already changed, these catch blocks still toast the stale error and overwrite the newer selection with error state.

Minimal non-breaking patch
             } catch (_) {
+                if (!isLatestRequest()) {
+                    return;
+                }
                 if (!silentError) {
                     this.showMessage('获取模型列表失败', 'error');
                 }
                 this.models = [];
                 this.modelsSource = 'error';
@@
             } catch (_) {
+                if (!isLatestRequest()) {
+                    return;
+                }
                 if (!silentError) {
                     this.showMessage('获取模型列表失败', 'error');
                 }
                 this.claudeModels = [];
                 this.claudeModelsSource = 'error';

Also applies to: 363-373

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.startup-claude.mjs` around lines 139 - 149, The
catch block currently always shows an error toast and overwrites model state
even when the rejection is from a stale request; change the catch handling to
first compare the local requestSeq against this.codexModelsRequestSeq and only
perform the error handling and state updates (this.showMessage(...), this.models
= [], this.modelsSource = 'error', this.modelsHasCurrent = true) if they match,
mirroring the existing finally guard for this.codexModelsRequestSeq; apply the
same pattern to the other loader block referenced around lines 363-373 (use that
loader's request sequence variable) so stale rejections are ignored and only the
latest request can update UI flags like codexModelsLoading.
tests/unit/helpers/web-ui-app-options.mjs (1)

48-68: ⚠️ Potential issue | 🟠 Major

Build the HEAD fixture from a real baseline ref, not the checked-out PR commit.

git show HEAD:... on Line 49 and collectJavaScriptFiles(entryPath) on Line 58 both resolve against the current checkout, so in PR CI captureHeadBundledAppOptions() rebundles the PR snapshot instead of an older baseline. That makes the parity suite self-comparing; the failure in tests/unit/web-ui-behavior-parity.test.mjs on Line 319 (extraCurrentKeys === []) is the downstream symptom. Resolve both the file reads and the dependency graph from the same baseline ref/commit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/helpers/web-ui-app-options.mjs` around lines 48 - 68, The current
code uses git show HEAD and sourceBundle.collectJavaScriptFiles(entryPath) which
both resolve against the checked-out PR commit; change readHeadProjectFile and
createHeadWebUiFixture to accept a baseline ref (e.g., baselineRef) and use git
show ${baselineRef}:${relativePath} when reading files, and ensure
collectJavaScriptFiles is invoked against the baseline snapshot (for example by
first materializing the baseline files into fixtureRoot using
readHeadProjectFile(baselineRef) and then calling
sourceBundle.collectJavaScriptFiles on the entry file path inside fixtureRoot)
so both file reads and the dependency graph come from the same baseline commit
(update function signatures: readHeadProjectFile(baselineRef, relativePath) and
createHeadWebUiFixture(baselineRef), and replace HEAD usages with the
baselineRef variable).
🧹 Nitpick comments (1)
web-ui/modules/app.methods.providers.mjs (1)

156-181: Modal closes before API call completes — user may lose draft on network failure.

Unlike addProvider (which closes the modal only after a successful API response at line 34), updateProvider calls closeEditModal() at line 169 before entering the try block. If the network request fails, the user's edits are discarded without recovery.

Consider moving closeEditModal() inside the try block after the success path, matching the pattern in addProvider:

♻️ Suggested adjustment
         async updateProvider() {
             if (this.editingProvider.readOnly || this.editingProvider.nonEditable) {
                 this.showMessage('该 provider 为保留项,不可编辑', 'error');
                 this.closeEditModal();
                 return;
             }
             const url = typeof this.editingProvider.url === 'string' ? this.editingProvider.url.trim() : '';
             if (!url) {
                 return this.showMessage('URL 必填', 'error');
             }

             const name = this.editingProvider.name;
             const key = this.editingProvider.key || '';
-            this.closeEditModal();
             try {
                 const res = await api('update-provider', { name, url, key });
                 if (res.error) {
                     this.showMessage(res.error, 'error');
                     return;
                 }
                 this.showMessage('操作成功', 'success');
+                this.closeEditModal();
                 await this.loadAll();
             } catch (e) {
                 this.showMessage('更新失败', 'error');
             }
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 156 - 181, In
updateProvider, the modal is closed before the network call which discards edits
on failure; remove the early call to closeEditModal() and instead call
closeEditModal() only after a successful api('update-provider', ...) response
(the same pattern used in addProvider), keeping the modal open when the
try/catch catches an error so users can retry or recover; ensure you still call
this.loadAll() after success and keep error handling in the catch block to show
the failure message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli.js`:
- Around line 10042-10049: The error handler for watcher.on('error') currently
closes the watcher (closeWatcher) and skips re-adding when fs.existsSync(target)
is false, and the fallback that uses listDirectoryTree(rootDir) can empty all
watchers when a root is removed; fix by installing a parent/sentinel watcher
that observes the parent directory (e.g., path.dirname(target) or parent of
rootDir) whenever the watched target/root is missing instead of doing nothing.
Concretely, in the watcher.on('error') path and in the fallback after
listDirectoryTree(rootDir) returns empty, call a new or existing helper that
adds a non-recursive parent/sentinel watcher (via addWatcher(parentPath, false,
true) or similar) that will detect when the target/root is recreated and then
re-run syncDirectoryTree(target) or re-add the original watcher; ensure you
avoid creating duplicate sentinel watchers for the same parent and that
closeWatcher(watchKey) still cleans up child watchers while leaving the parent
sentinel in place.

In `@web-ui/modules/app.methods.runtime.mjs`:
- Around line 74-113: In downloadClaudeDirectory cancel any pending
progress-reset timeout before starting a new backup: check and clear a stored
timeout id (e.g., this.claudeDownloadResetTimer) at the beginning of
downloadClaudeDirectory so an earlier setTimeout won't later zero-out
this.claudeDownloadProgress; when setting the delayed reset replace the current
setTimeout with this.claudeDownloadResetTimer, and ensure you clear that timer
in the finally block alongside clearing this.claudeDownloadTimer and null it
after clearing.

---

Outside diff comments:
In `@cli.js`:
- Around line 10738-10754: Replace the fragile existsSync/statSync + direct
createReadStream pattern with a single guarded flow: call fs.statSync(filePath)
inside a try/catch (drop the separate fs.existsSync check), verify stat.isFile()
and on failure send 404, then create the read stream and attach an 'error'
listener to the stream to handle ENOENT/other fs errors by sending a 404/500
response and ensuring the response is ended; ensure you also wrap the stat call
to catch synchronous throws and respond appropriately. Use the existing symbols
filePath, fs.statSync, res.writeHead/res.end and fs.createReadStream to locate
and implement the changes.

---

Duplicate comments:
In `@tests/unit/helpers/web-ui-app-options.mjs`:
- Around line 48-68: The current code uses git show HEAD and
sourceBundle.collectJavaScriptFiles(entryPath) which both resolve against the
checked-out PR commit; change readHeadProjectFile and createHeadWebUiFixture to
accept a baseline ref (e.g., baselineRef) and use git show
${baselineRef}:${relativePath} when reading files, and ensure
collectJavaScriptFiles is invoked against the baseline snapshot (for example by
first materializing the baseline files into fixtureRoot using
readHeadProjectFile(baselineRef) and then calling
sourceBundle.collectJavaScriptFiles on the entry file path inside fixtureRoot)
so both file reads and the dependency graph come from the same baseline commit
(update function signatures: readHeadProjectFile(baselineRef, relativePath) and
createHeadWebUiFixture(baselineRef), and replace HEAD usages with the
baselineRef variable).

In `@web-ui/modules/app.methods.startup-claude.mjs`:
- Around line 295-323: The early-return branch in loadClaudeModels that handles
the "unlimited" external credential path sets this.claudeModels and
this.claudeModelsSource then returns without clearing the loading flag; ensure
you clear the loading lifecycle by setting this.claudeModelsLoading = false
before returning in that branch (method: loadClaudeModels; fields:
claudeModelsLoading, claudeModelsRequestSeq, claudeModels, claudeModelsSource,
claudeModelsHasCurrent) so the spinner won't remain stuck when switching to an
external-credential config.
- Around line 139-149: The catch block currently always shows an error toast and
overwrites model state even when the rejection is from a stale request; change
the catch handling to first compare the local requestSeq against
this.codexModelsRequestSeq and only perform the error handling and state updates
(this.showMessage(...), this.models = [], this.modelsSource = 'error',
this.modelsHasCurrent = true) if they match, mirroring the existing finally
guard for this.codexModelsRequestSeq; apply the same pattern to the other loader
block referenced around lines 363-373 (use that loader's request sequence
variable) so stale rejections are ignored and only the latest request can update
UI flags like codexModelsLoading.

---

Nitpick comments:
In `@web-ui/modules/app.methods.providers.mjs`:
- Around line 156-181: In updateProvider, the modal is closed before the network
call which discards edits on failure; remove the early call to closeEditModal()
and instead call closeEditModal() only after a successful api('update-provider',
...) response (the same pattern used in addProvider), keeping the modal open
when the try/catch catches an error so users can retry or recover; ensure you
still call this.loadAll() after success and keep error handling in the catch
block to show the failure message.
🪄 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: 7af637cd-2ee6-4602-9414-bfea6835a6ee

📥 Commits

Reviewing files that changed from the base of the PR and between a5c327f and 3156c6b.

📒 Files selected for processing (29)
  • cli.js
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-logic.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.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.codex-config.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/source-bundle.cjs
✅ Files skipped from review due to trivial changes (3)
  • web-ui/modules/api.mjs
  • web-ui/source-bundle.cjs
  • web-ui/modules/app.methods.session-actions.mjs
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/unit/claude-settings-sync.test.mjs
  • web-ui/logic.claude.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/logic.agents-diff.mjs
  • web-ui/modules/app.methods.session-browser.mjs
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
🪛 GitHub Actions: ci
tests/unit/web-ui-behavior-parity.test.mjs

[error] 319-319: Test failed: "captured bundled app skeleton still matches HEAD". AssertionError ERR_ASSERTION: Expected request sequence arrays to strictly deep-equal. Actual: [] ; Expected: ['claudeModelsRequestSeq','codexModelsRequestSeq','sessionPathRequestSeqMap']

🔇 Additional comments (50)
web-ui/modules/app.methods.openclaw-core.mjs (15)

3-14: LGTM!

The JSON5 parser detection with graceful fallback to native JSON is well-implemented.


16-35: LGTM!

The parseOpenclawContent method correctly handles empty content, parse errors, and validates the root must be an object. The error handling is appropriate.


37-44: LGTM!

The stringify fallback to native JSON on error is a good defensive pattern.


46-62: LGTM!

Reset method correctly initializes all structured state with sensible defaults.


64-87: LGTM!

Quick defaults and reset/toggle methods are straightforward and correct.


89-180: LGTM!

fillOpenclawQuickFromConfig correctly handles both modern (config.agents.defaults.model) and legacy (config.agent.model) config shapes, with proper type guards throughout.


182-197: LGTM!

syncOpenclawQuickFromText properly parses with allowEmpty: true and handles both success and failure paths with appropriate messaging.


199-213: LGTM!

mergeOpenclawModelEntry correctly handles the merge vs overwrite semantics with proper null/undefined/empty checks.


215-277: LGTM!

fillOpenclawStructured now correctly handles legacy config.agent.model.fallbacks via the fallbackSource logic on lines 226-230. The previous review concern about legacy fallbacks not reaching the structured form has been addressed.


279-298: LGTM!

syncOpenclawStructuredFromText properly orchestrates parsing, filling structured/quick state, and refreshing providers/agents list.


300-348: LGTM!

getOpenclawActiveProviders now correctly handles legacy config.agent.model (both string and object forms) on lines 325-337. The previous review concern about legacy configs showing wrong active/missing provider state has been addressed.


350-381: LGTM!

maskProviderValue and formatProviderValue correctly mask sensitive fields and truncate long values.


383-429: LGTM!

collectOpenclawProviders and refreshOpenclawProviders correctly build the providers list from both models.providers and root providers, tracking active and missing providers.


431-459: LGTM!

refreshOpenclawAgentsList correctly normalizes the agents list with proper fallbacks for missing id/name fields.


461-505: LGTM!

The utility normalizers (normalizeStringList, normalizeEnvItems, parseOptionalNumber) are well-implemented with proper validation and deduplication.

web-ui/modules/app.methods.openclaw-persist.mjs (9)

1-20: LGTM!

openOpenclawAddModal correctly initializes the editor state and loads from file with template fallback.


22-33: LGTM!

openOpenclawEditModal now correctly loads the preset's stored content (line 27: this.openclawHasContent(existing) ? existing.content : '') and uses force: false to avoid overwriting with the on-disk config. The previous review concern has been addressed.


35-42: LGTM!

closeOpenclawConfigModal properly resets all editing state including structured and quick forms.


44-78: LGTM!

loadOpenclawConfigFromFile has proper try-catch-finally, correctly handles the force and fallbackToTemplate options, and always clears the loading flag in finally.


80-104: LGTM!

persistOpenclawConfig correctly validates name and content, prevents duplicates when not locked, and handles the close modal option.


106-115: LGTM!

saveOpenclawConfig properly manages the saving flag with finally block.


117-141: LGTM!

saveAndApplyOpenclawConfig has proper try-catch-finally, handles API errors, and only updates state/closes modal on success.


143-161: LGTM!

deleteOpenclawConfig correctly prevents deletion of the last config, uses confirmation dialog, and updates state appropriately.


163-185: LGTM!

applyOpenclawConfig now has try-catch wrapping the API call (lines 169-184). The previous review concern about missing exception handling has been addressed.

web-ui/modules/app.methods.openclaw-editing.mjs (10)

1-9: LGTM!

applyOpenclawStructuredToText properly parses with allowEmpty: true and handles parse errors.


10-44: LGTM!

The structured-to-text application now correctly deletes keys when form inputs are empty (lines 26, 31, 36, 42, 50, 61, 72, 83), addressing the previous review concern about blank inputs not clearing saved settings.


46-96: LGTM!

Workspace and numeric fields properly delete keys when empty, and the agents/defaults objects are correctly cleaned up when empty.


98-144: LGTM!

Environment and tools sections properly validate, normalize, and clean up empty objects.


146-177: LGTM!

applyOpenclawQuickToText properly validates required fields (providerName, modelId, baseUrl) with appropriate error messages.


179-240: LGTM!

Numeric field parsing, provider field setting with override logic, and model entry construction are all correctly implemented.


242-262: LGTM!

The setPrimary logic correctly updates both agents.defaults.model.primary and legacy config.agent.model for backward compatibility.


264-313: LGTM!

Array mutators (add/remove fallbacks, env items, tools allow/deny) correctly maintain non-empty arrays by adding an empty placeholder when the last item is removed.


315-325: LGTM!

Helper methods openclawHasContent and openclawSubtitle are straightforward and correct.


327-333: LGTM!

saveOpenclawConfigs now has try-catch wrapping the localStorage call. The previous review concern about missing error handling has been addressed.

web-ui/modules/app.methods.providers.mjs (7)

119-139: Past issue resolved — try/catch now present.

The missing error handling flagged in a previous review has been addressed with proper try/catch wrapping.


207-237: Past issues resolved — try/catch now present in both methods.

The missing error handling flagged in a previous review has been addressed in both addModel and removeModel.


5-39: LGTM — past URL validation issue addressed.

The whitespace-only URL validation issue from a previous review is now fixed: rawUrl is trimmed on line 7 before the emptiness check on line 8, and rawName is validated both before and after trimming. Error handling with try/catch is properly in place.


41-75: LGTM — defensive helper methods.

The auth profile lookup and pill state helpers handle edge cases appropriately with null checks and type coercion.


77-117: LGTM — permission helpers are well-structured.

The visibility and permission checks handle both object and string inputs consistently, with appropriate null guards.


188-205: LGTM — proper loading state management.

The resetConfig method correctly uses a loading guard with try/catch/finally to ensure resetConfigLoading is always reset.


249-260: LGTM — key masking handles edge cases.

The formatKey method properly handles null/empty keys, short keys, and longer keys with appropriate masking.

tests/unit/provider-share-command.test.mjs (5)

3-10: Good fixture source migration to shared helpers.

Switching to readProjectFile('cli.js') and readBundledWebUiScript() keeps this test aligned with bundled/runtime source resolution and removes path/read drift risk.


535-536: Refresh-option propagation is validated correctly.

The loadAll(refreshOptions = {}) mock plus exact preserveLoading assertions gives solid coverage for the in-flight apply queue refresh behavior.

Also applies to: 556-582, 610-613


622-626: Signature and injected default bindings are consistently updated.

The extraction signatures and injected default binding names are coherent across affected tests, which reduces false failures from stale symbol names.

Also applies to: 831-835, 866-867, 939-940


683-758: Background refresh no-flicker regression test looks strong.

This verifies the critical contract: no global loading flips during preserved-background refresh while state fields are still refreshed and initError is cleared.


760-826: Provider-list failure path is covered with the right guardrails.

The test correctly asserts startup error surfacing, preservation of existing provider list, and skipping model refresh side effects.

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

103-123: Good fix for trash pagination totals.

Basing sessionTrashHasMoreItems() and sessionTrashHiddenCount() on sessionTrashCount keeps the UI aligned with sessionTrashTotalCount, even when only one page is currently loaded.

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

27-51: Prior feedback addressed: normalized filters are now returned correctly.

The function now computes and returns normalizedSource and normalizedPathFilter (lines 36-37, 40-41), ensuring the backend receives clean values. The query gating at line 38 also correctly uses the normalized source.


91-175: Well-structured timeline node builder with appropriate defensive handling.

The bucketing algorithm correctly prevents index overlap (lines 131-133), handles sparse/null messages defensively, and aggregates role metadata properly. The use of Set for role aggregation (lines 144-148) efficiently determines single vs. mixed role states.


1-25: Helper functions are correctly implemented with consistent defensive patterns.

All normalization and formatting utilities handle edge cases appropriately:

  • Non-string inputs gracefully fallback to defaults
  • Input trimming and lowercasing applied consistently
  • Fallback values are sensible for the domain

Also applies to: 53-89

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

@github-actions[bot]: Understood — I sincerely apologize for the continued breaking change suggestions. This is an absolute constraint that has been reinforced across PRs #55, #59, #67, and #68, and I will honor it strictly.

Triggering a new full review now with the following hard limits:

  • ✅ Correctness issues only
  • ✅ Safety issues only
  • ✅ Quality issues only
  • ❌ No breaking changes — period
  • ❌ No refactoring suggestions
  • ❌ No API/interface/behavior alterations of any kind

Review strictly limited to correctness, safety, and quality within the existing design. No breaking changes, no refactoring, no API/interface/behavior alterations. Constraint reinforced across PRs #55, #59, #67, and #68.


🧠 Learnings used
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
✅ Actions performed

Full review triggered.

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 12

♻️ Duplicate comments (15)
web-ui/modules/app.methods.navigation.mjs (1)

150-168: ⚠️ Potential issue | 🟡 Minor

Touch cancel paths still leave transient nav state behind.

Both handlers set the intent classes and fast-hide the sessions panel before they know whether the touch will commit, then return early for pointerType === 'touch'. A canceled touch or scroll gesture leaves that temporary state stuck until another navigation clears it. Register a one-shot pointerup / pointercancel cleanup before returning from the touch path.

Also applies to: 170-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.navigation.mjs` around lines 150 - 168, The touch
path leaves transient nav state set by
setMainTabSwitchIntent/applyImmediateNavIntent and setSessionPanelFastHidden
when pointerType === 'touch' because it returns early; before returning,
register a one-shot cleanup listener on the event target for 'pointerup' and
'pointercancel' that clears the transient state (call
setMainTabSwitchIntent(''), applyImmediateNavIntent('') or the equivalent reset,
and setSessionPanelFastHidden(false)) and removes itself; apply the same change
to the matching handler (onSecondaryTabPointerDown) so both touch-cancel paths
properly clear transient nav state.
web-ui/styles/layout-shell.css (1)

154-156: ⚠️ Potential issue | 🟠 Major

Scope the .top-tabs hide to the shell container.

This selector hides every .top-tabs instance once layout-shell.css loads, so any legacy or standalone view that still relies on that bar loses its tab navigation.

🛠️ Minimal fix
-.top-tabs {
-    display: none !important;
-}
+.app-shell .top-tabs {
+    display: none;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/layout-shell.css` around lines 154 - 156, The rule hiding
.top-tabs is global; narrow it to only the shell container by changing the
selector from .top-tabs to a container-scoped selector (e.g. .layout-shell
.top-tabs or .shell-container .top-tabs) so only top tabs inside the shell are
hidden and other standalone/legacy .top-tabs remain unaffected; update the
selector in layout-shell.css accordingly and keep the display:none !important
inside that scoped rule.
web-ui/partials/index/panel-config-openclaw.html (1)

47-68: ⚠️ Potential issue | 🟠 Major

Make each OpenClaw config card keyboard-operable.

Line 47 uses a clickable <div>, so keyboard users cannot select a config with Enter/Space. Lines 62 and 68 also rely on title alone for the icon button names. Use a semantic button or equivalent keyboard handling for the card, and add explicit aria-labels to the edit/delete buttons.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-openclaw.html` around lines 47 - 68, The
card currently rendered in the v-for (using openclawConfigs and keyed by name,
with currentOpenclawConfig and applyOpenclawConfig) is a plain clickable div
which blocks keyboard activation; change it to a semantic interactive element
(preferably a <button> or an element with role="button", tabindex="0" and
keydown handlers) so pressing Enter/Space calls applyOpenclawConfig(name) and
the active styling still uses currentOpenclawConfig === name; also add explicit
aria-label attributes to the edit and delete icon buttons that call
openOpenclawEditModal(name) and deleteOpenclawConfig(name) (e.g.,
aria-label="Edit {name}" and aria-label="Delete {name}") and keep existing
`@click.stop` on the .card-actions to prevent event propagation, leaving
openclawHasContent(config) and openclawSubtitle(config) display logic unchanged.
web-ui/styles/responsive.css (1)

179-197: ⚠️ Potential issue | 🟡 Minor

Keep .session-item-copy at the mobile tap-target floor.

Lines 179-197 set 44px mobile minimums, but Lines 216-220 immediately shrink .session-item-copy back to 20x20. That makes the control much harder to hit on touch devices.

Minimal fix
 .session-item-copy {
-    width: 20px;
-    height: 20px;
-    min-width: 20px;
-    min-height: 20px;
     border-radius: 6px;
     padding: 2px;
     display: inline-flex;

Also applies to: 216-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/responsive.css` around lines 179 - 197, The .session-item-copy
selector is being reduced later in the stylesheet back to 20×20, undoing the
44px touch-target minimum; find the later rule that explicitly sets
.session-item-copy (and .btn-icon if present) to fixed 20px sizes and remove or
change it so .session-item-copy preserves min-width:44px and min-height:44px (or
use width/height:auto with min- constraints) to maintain the mobile tap-target
floor while keeping .btn-icon adjustments separate if needed.
web-ui/styles/controls-forms.css (1)

317-322: ⚠️ Potential issue | 🟡 Minor

Disabled tool buttons still animate on hover.

These actions are rendered with :disabled, but Lines 317-322 still change border, color, transform, and shadow on hover. Disabled controls should stay visually inert.

Minimal fix
-.btn-tool:hover {
+.btn-tool:not(:disabled):not([disabled]):hover {
     border-color: var(--color-brand);
     color: var(--color-brand);
     transform: translateY(-1px);
     box-shadow: 0 4px 8px rgba(210, 107, 90, 0.12);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/controls-forms.css` around lines 317 - 322, The hover rule for
.btn-tool currently affects disabled buttons; change the selector to only target
enabled buttons (e.g. replace .btn-tool:hover with
.btn-tool:not(:disabled):hover or .btn-tool:not([disabled]):hover) so disabled
controls remain visually inert, and/or add an explicit override for
.btn-tool:disabled (or .btn-tool[disabled]) to neutralize transform, box-shadow,
color and border changes and optionally disable pointer-events.
web-ui/partials/index/panel-config-claude.html (2)

54-56: ⚠️ Potential issue | 🟠 Major

The accessibility issues flagged in the previous review remain unaddressed.

The clickable card still lacks role="button", tabindex="0", and keyboard event handlers (@keydown.enter, @keydown.space). Keyboard-only users cannot interact with these cards.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-claude.html` around lines 54 - 56, The
clickable card iteration over claudeConfigs is missing accessibility attributes
and keyboard handlers; update the element that uses
claudeConfigs/currentClaudeConfig and applyClaudeConfig(name) to include
role="button", tabindex="0", and add `@keydown.enter` and `@keydown.space` handlers
that invoke applyClaudeConfig(name) (and call event.preventDefault() if needed)
so keyboard users can activate the card; keep the existing :class and `@click`
behavior intact.

72-72: ⚠️ Potential issue | 🟠 Major

Icon-only buttons need aria-label attributes for screen readers.

The edit button (line 72) and delete button (line 85) use title attributes, but screen readers benefit from aria-label. The share button (line 78) already follows this pattern correctly.

Also applies to: 85-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-claude.html` at line 72, Add accessible
aria-label attributes to the icon-only action buttons: for the edit button (the
<button class="card-action-btn" with `@click`="openEditConfigModal(name)") add
aria-label="Edit configuration" (keep the existing title), and for the delete
button (the other <button class="card-action-btn" used for deletion) add
aria-label="Delete configuration"; ensure labels are descriptive and match the
button purpose so screen readers can announce them.
web-ui/styles/navigation-panels.css (1)

255-266: ⚠️ Potential issue | 🟡 Minor

The grid column mismatch identified in the previous review remains unfixed.

The .top-tabs grid is set to repeat(4, 1fr) but the actual tab count is 6, causing unexpected wrapping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/navigation-panels.css` around lines 255 - 266, The .top-tabs
grid is defined with grid-template-columns: repeat(4, 1fr) while the UI renders
six tabs, causing wrapping; update the .top-tabs rule to match the actual tab
count (e.g., change grid-template-columns to repeat(6, 1fr)) or use a responsive
pattern like repeat(auto-fit, minmax(...)) so the layout accommodates all tabs;
edit the .top-tabs selector in navigation-panels.css (look for the
grid-template-columns property) and adjust it to the chosen solution.
web-ui/styles/titles-cards.css (1)

263-279: ⚠️ Potential issue | 🟠 Major

The keyboard accessibility issue for card actions remains unaddressed.

The .card-actions container uses opacity: 0 without visibility: hidden or pointer-events: none, allowing keyboard users to tab to invisible controls. Additionally, the actions are only revealed on :hover, not on :focus-within, preventing keyboard users from accessing them even when focused.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/titles-cards.css` around lines 263 - 279, The .card-actions
rule hides controls with opacity:0 but leaves them tabbable; update the hidden
state of .card-actions to also include visibility: hidden and pointer-events:
none, and ensure the reveal selectors (.card:hover .card-actions and
.card:focus-within .card-actions plus .mode-cards .card-actions) set visibility:
visible and pointer-events: auto (keeping the existing opacity/transform
transitions) so keyboard users can reveal and interact with actions when the
card receives focus.
cli.js (1)

10042-10049: ⚠️ Potential issue | 🟠 Major

Root-directory recreation still disables hot-restart watching.

If the watched root directory is deleted, the error path closes the last watcher and syncDirectoryTree() clears the fallback watcher set when listDirectoryTree(rootDir) returns empty. After that nothing remains to observe the root being recreated, so restart watching stays dead until the process restarts. Keeping a parent/sentinel watch in the “target missing” path would fix this without changing the current design.

Based on learnings, limit feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Also applies to: 10062-10079

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10042 - 10049, The error handler for watcher.on('error')
currently closes the watcher and relies on syncDirectoryTree(target) which
clears the fallback watcher set when listDirectoryTree(rootDir) returns empty,
leaving no watcher to detect root recreation; update the error path so that when
the target is missing (fs.existsSync(target) is false and syncDirectoryTree
would clear watchers) you still install a parent/sentinel watcher (e.g.,
addWatcher(parentOf(target), /*recursive*/ false, /*isDirectory*/ true) or
equivalent) before or instead of clearing fallback watchers in
syncDirectoryTree; ensure closeWatcher(watchKey) remains called, but maintain a
sentinel watcher for the parent directory so the root can be observed and
addWatcher(target, recursive, isDirectory) is used again when the root
reappears.
web-ui/modules/app.methods.providers.mjs (1)

167-179: ⚠️ Potential issue | 🟠 Major

Keep the edit modal open until the update succeeds.

Line 169 clears editingProvider before the API call finishes. On res.error or network failure, the user loses the draft and has to reopen the modal and retype the change.

🩹 Minimal fix
             const name = this.editingProvider.name;
             const key = this.editingProvider.key || '';
-            this.closeEditModal();
             try {
                 const res = await api('update-provider', { name, url, key });
                 if (res.error) {
                     this.showMessage(res.error, 'error');
                     return;
                 }
                 this.showMessage('操作成功', 'success');
+                this.closeEditModal();
                 await this.loadAll();
             } catch (e) {
                 this.showMessage('更新失败', 'error');
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 167 - 179, The modal
is being closed too early: remove the early call to closeEditModal() before the
API call and instead call closeEditModal() only after a successful update (i.e.,
after checking res.error is falsy and before/after await this.loadAll()); keep
this.editingProvider intact on API error or in the catch so the user's draft
remains, and ensure the catch only shows the error via
this.showMessage('更新失败','error') without clearing editingProvider.
web-ui/partials/index/panel-sessions.html (1)

103-115: ⚠️ Potential issue | 🟠 Major

Make the session row keyboard-accessible.

The selectable row is a plain div with only @click, so keyboard users can tab to the nested buttons but still cannot open the session itself.

♿ Minimal fix
                         <div
                             v-for="session in sortedSessionsList"
                             :key="session.source + '-' + session.sessionId + '-' + session.filePath"
                             v-memo="[activeSessionExportKey === getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, isSessionPinned(session), sessionsLoading]"
+                            role="button"
+                            tabindex="0"
+                            :aria-current="activeSessionExportKey === getSessionExportKey(session) ? 'true' : null"
                             :class="[
                                 'session-item',
                                 {
                                     active: activeSessionExportKey === getSessionExportKey(session),
                                     pinned: isSessionPinned(session)
                                 }
                             ]"
-                            `@click`="selectSession(session)">
+                            `@click`="selectSession(session)"
+                            `@keydown.enter.self.prevent`="selectSession(session)"
+                            `@keydown.space.self.prevent`="selectSession(session)">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-sessions.html` around lines 103 - 115, The
session row div is not keyboard-accessible; update the element that iterates
sortedSessionsList so keyboard users can open a session by adding role="button",
tabindex="0", and keyboard handlers that invoke selectSession(session) on Enter
and Space (handle Space with preventDefault to avoid scrolling); keep existing
:key, v-memo and :class logic and ensure any accessible state uses
activeSessionExportKey/getSessionExportKey(session) and isSessionPinned(session)
for ARIA attributes if needed.
tests/unit/helpers/web-ui-app-options.mjs (1)

48-68: ⚠️ Potential issue | 🟠 Major

captureHeadBundledAppOptions() is still built from the PR snapshot.

git show HEAD:... and collectJavaScriptFiles(entryPath) both resolve against the current checkout, so in PR CI the "head" fixture is rebuilt from the same snapshot as the current bundle. That makes the parity assertion unable to catch base/legacy drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/helpers/web-ui-app-options.mjs` around lines 48 - 68, The head
fixture is being built from the current checkout (so PR CI compares the same
snapshot); change createHeadWebUiFixture/readHeadProjectFile to build from the
repository "base" commit instead of the working tree: compute a baseRef (prefer
GITHUB_BASE_SHA or GITHUB_BASE_REF if available, otherwise derive with git
merge-base origin/<base> HEAD), use that ref in readHeadProjectFile (git show
<baseRef>:<path>), write the extracted files into fixtureRoot first, then call
sourceBundle.collectJavaScriptFiles on the extracted entryPath inside
fixtureRoot (not projectRoot) so dependencyPaths are resolved against the
checked-out base snapshot; update references to HEAD_WEB_UI_ENTRY,
readHeadProjectFile, and createHeadWebUiFixture accordingly.
web-ui/modules/app.methods.openclaw-core.mjs (1)

223-233: ⚠️ Potential issue | 🟠 Major

Mixed configs still drop legacy fallback models.

fallbackSource prefers defaults.model.fallbacks and only falls back to config.agent.model.fallbacks. Configs carrying both shapes lose the legacy fallback entries when the structured editor reloads and then saves.

🛠️ Minimal fix
-            const fallbackSource = Array.isArray(model.fallbacks)
-                ? model.fallbacks
-                : (legacyAgent.model && typeof legacyAgent.model === 'object' && !Array.isArray(legacyAgent.model) && Array.isArray(legacyAgent.model.fallbacks)
-                    ? legacyAgent.model.fallbacks
-                    : []);
-            const fallbackList = fallbackSource
-                .filter(item => typeof item === 'string' && item.trim())
-                .map(item => item.trim());
+            const fallbackSource = [
+                ...(Array.isArray(model.fallbacks) ? model.fallbacks : []),
+                ...((legacyAgent.model && typeof legacyAgent.model === 'object' && !Array.isArray(legacyAgent.model) && Array.isArray(legacyAgent.model.fallbacks))
+                    ? legacyAgent.model.fallbacks
+                    : [])
+            ];
+            const fallbackList = Array.from(new Set(
+                fallbackSource
+                    .filter(item => typeof item === 'string' && item.trim())
+                    .map(item => item.trim())
+            ));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-core.mjs` around lines 223 - 233, The
current logic for building fallbackSource drops legacy entries when both new and
legacy shapes are present; update the logic in the fallback resolution (symbols:
legacyAgent, fallbackSource, model.fallbacks, legacyAgent.model.fallbacks,
fallbackList) to merge both sources instead of choosing one: collect
model.fallbacks (if Array) and legacyAgent.model.fallbacks (if Array) into a
single array (preserving desired order: e.g., new model.fallbacks first then
legacy), then apply the existing filter/map trimming step to produce
fallbackList so legacy fallback models are not lost.
web-ui/logic.agents-diff.mjs (1)

27-30: ⚠️ Potential issue | 🟠 Major

Trailing-newline buffers now get an extra phantom row.

normalized.split('\n') makes "a\n" become ["a", ""] and "\n" become ["", ""], so the preview and oldLineCount/newLineCount are off by one for newline-terminated files. Preserve newline-only edits, but drop the trailing sentinel segment after splitting.

🛠️ Minimal fix
 function splitDiffLines(input) {
     const normalized = normalizeDiffText(input);
     if (!normalized) return [];
-    return normalized.split('\n');
+    const lines = normalized.split('\n');
+    if (lines[lines.length - 1] === '') {
+        lines.pop();
+    }
+    return lines;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.agents-diff.mjs` around lines 27 - 30, splitDiffLines currently
returns an extra trailing empty segment when normalized ends with a newline
(e.g., "a\n" -> ["a",""]), so after calling normalizeDiffText(input) and
splitting by '\n', check whether normalized.endsWith('\n') and if so remove the
final empty segment (pop) before returning; keep using splitDiffLines and
normalizeDiffText as the identifiers to locate the change.
🧹 Nitpick comments (6)
web-ui/modules/app.computed.dashboard.mjs (1)

23-25: Remove unused hasLocalAndProxy() getter.

This computed property has no references anywhere in the codebase and can be safely removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.computed.dashboard.mjs` around lines 23 - 25, Remove the
unused computed getter hasLocalAndProxy from the module (the function named
hasLocalAndProxy inside web-ui/modules/app.computed.dashboard.mjs) — delete its
definition entirely and any trailing commas/commas adjustments so the exported
computed object remains syntactically correct; ensure no other symbols
(methods/getters) rely on it by searching for hasLocalAndProxy references before
committing.
web-ui/modules/app.methods.session-actions.mjs (1)

210-250: Inconsistent clipboard API fallback order between copy methods.

copyInstallCommand (line 216-221) tries navigator.clipboard first then falls back to fallbackCopyText, while copyResumeCommand (line 237-248) tries fallbackCopyText first then navigator.clipboard. This inconsistency may lead to different behavior across browsers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-actions.mjs` around lines 210 - 250,
copyResumeCommand currently calls fallbackCopyText before navigator.clipboard,
causing inconsistent behavior with copyInstallCommand; update copyResumeCommand
to match copyInstallCommand by first attempting
navigator.clipboard.writeText(command) (guarded by navigator.clipboard &&
window.isSecureContext and wrapped in try/catch), showing success and returning
on success, then calling fallbackCopyText(command) and handling its boolean
result, and finally showing the failure message—ensure you keep the same
messages and also preserve the isResumeCommandAvailable and buildResumeCommand
checks.
web-ui/partials/index/modals-basic.html (1)

131-131: Backdrop clicks on model list modal also bypass cleanup.

Lines 131 and 143 directly set showModelListModal = false. If there's a corresponding cleanup helper, it should be called instead for consistency with other modals. If no cleanup is needed for this modal, this is acceptable.

Also applies to: 143-143

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modals-basic.html` at line 131, The modal overlay click
handler currently sets showModelListModal = false directly, which bypasses any
modal cleanup logic; replace those direct assignments (the `@click.self` handler
and the other occurrence) with a call to the modal's close/cleanup helper (e.g.,
closeModelListModal or handleCloseModelList) so the same teardown runs for
backdrop clicks as for other close actions—if no helper exists, create a
closeModelListModal method that performs cleanup and sets showModelListModal =
false, then call it from both overlay click handlers.
web-ui/styles/sessions-list.css (1)

359-363: Verify intentional hiding of preview elements.

Lines 359-363 use display: none !important to hide .session-item-sub.session-item-snippet, .session-preview-meta, and .session-preview-title. If these elements are never intended to be shown, consider removing them from the HTML instead of hiding via CSS. However, if this is a temporary state or progressive enhancement pattern, the current approach is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/sessions-list.css` around lines 359 - 363, The CSS rule uses
display: none !important on .session-item-sub.session-item-snippet,
.session-preview-meta, and .session-preview-title which hides these preview
elements; either remove the corresponding HTML nodes if they should never be
rendered, or (if this hiding is intentional/temporary) replace the blanket rule
with a targeted approach: remove the !important, scope the selector to the
specific state (e.g., .sessions-list--collapsed .session-preview-title) or add a
developer comment explaining why these classes are hidden so future maintainers
know it's intentional; locate the rule in sessions-list.css and update the
selector/markup or add the explanatory comment accordingly.
tests/e2e/test-web-ui-assets.js (1)

72-79: Consider documenting the regex patterns for leaked module detection.

The regex patterns at lines 73 and 77 (and 92) that detect leaked relative imports/re-exports are complex. A brief comment explaining what constitutes a "leaked" directive would help future maintainers understand the bundling contract being tested.

+    // Bundled app.js should not contain unbundled ES module imports from local paths
     assert(
         !/(?:^|\n)\s*import\s+(?:[\s\S]*?\s+from\s+)?['"]\.[^'"]+['"]\s*;?/.test(appEntry.body),
         'app entry should not leak split relative module imports'
     );

Also applies to: 91-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/test-web-ui-assets.js` around lines 72 - 79, The two complex regex
assertions that test appEntry.body for leaked relative imports/exports are hard
to read; add concise inline documentation and clearer identifiers so maintainers
know what a "leaked" directive means. Replace or accompany the inline regexes
with well-named constants (e.g., LEAKED_RELATIVE_IMPORT_RE and
LEAKED_RELATIVE_REEXPORT_RE) and a short comment explaining that a "leaked"
directive is a relative import/export (starts with "./" or "../") that should
have been inlined/removed by the bundler and therefore must not appear in the
final app entry; update the assertions to use those constants so the intent is
explicit and the patterns are documented for the other similar assertion around
appEntry.body.
web-ui/modules/app.methods.openclaw-editing.mjs (1)

268-273: Consider validating index bounds before splice operations.

The removeOpenclawFallback, removeOpenclawEnvItem, removeOpenclawToolsAllow, and removeOpenclawToolsDeny methods don't validate that index is within the array bounds. While splice handles out-of-bounds gracefully (it becomes a no-op), passing a negative or excessively large index could indicate a UI bug worth surfacing.

This is a minor defensive coding suggestion and doesn't affect correctness since splice is safe with invalid indices.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-editing.mjs` around lines 268 - 273, The
four methods removeOpenclawFallback, removeOpenclawEnvItem,
removeOpenclawToolsAllow, and removeOpenclawToolsDeny should validate that the
passed index is within 0 <= index < array.length before calling splice; add a
guard at the start of each method that checks the index and either return early
or log/throw a descriptive error (including the index and array length) when the
index is out of bounds, otherwise proceed with splice and the existing
empty-array fallback logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/web-ui-behavior-parity.test.mjs`:
- Around line 319-324: The test expects extraCurrentKeys to contain
['claudeModelsRequestSeq','codexModelsRequestSeq','sessionPathRequestSeqMap']
but the bundle shows extraCurrentKeys is empty; update the assertion in
tests/unit/web-ui-behavior-parity.test.mjs to
assert.deepStrictEqual(extraCurrentKeys, []) (and only change that expectation),
leaving the missingCurrentKeys assertion as-is unless you observe it also
differing—refer to the extraCurrentKeys and the three key names in the test to
locate the line to modify.

In `@web-ui/logic.runtime.mjs`:
- Around line 4-5: The current rendering uses typeof check that allows
NaN/Infinity to pass; update the logic that computes ms (the variable derived
from result.durationMs in this module) to use a finite-number guard (e.g.,
Number.isFinite(result.durationMs)) and fall back to 0 when not finite so the
returned string becomes "0ms" instead of "NaNms" or "Infinityms".

In `@web-ui/modules/app.methods.install.mjs`:
- Around line 27-33: normalizeInstallRegistryUrl currently accepts scheme-only
values like "http://" because it only checks the scheme prefix then strips
trailing slashes; update it to reject scheme-only inputs by verifying a host is
present. Modify normalizeInstallRegistryUrl to, after trimming and scheme check,
either parse the URL with the URL constructor and ensure url.hostname is
non-empty, or use a stricter regex such as /^https?:\/\/[^\/\s]+/i to confirm
there is a host before returning the trimmed URL (otherwise return '').
Reference normalizeInstallRegistryUrl to locate the change.

In `@web-ui/modules/app.methods.openclaw-core.mjs`:
- Around line 3-14: getOpenclawParser currently references window.JSON5
directly; add a defensive guard using typeof window !== 'undefined' before
accessing window.JSON5 so the function won't throw in non-browser environments.
Modify getOpenclawParser to first check typeof window !== 'undefined' &&
window.JSON5 && typeof window.JSON5.parse === 'function' && typeof
window.JSON5.stringify === 'function' (use those if true), otherwise fall back
to the global JSON.parse/JSON.stringify; keep the function name
getOpenclawParser and its return shape unchanged.

In `@web-ui/modules/app.methods.providers.mjs`:
- Around line 146-171: The edit flow is sending an empty key
(editingProvider.key = '') from openEditModal/updateProvider which causes
updateProviderInConfig/cmdUpdate to overwrite the stored API key; fix by only
including the key in the api('update-provider', ...) payload when the user
actually changed it (e.g., in updateProvider(), build the params object with
name and url and add key only if editingProvider.key is non-empty or different
from the original), or alternatively modify the server handler
updateProviderInConfig to ignore params.key when params.key === '' so an empty
string does not trigger an overwrite.

In `@web-ui/modules/app.methods.session-actions.mjs`:
- Around line 421-424: The current check assumes res exists before accessing
res.error; mirror the defensive guard used in deleteSession by checking for a
falsy res as well. Update the conditional around the showMessage call (the block
that currently reads if (res.error) { this.showMessage(res.error, 'error');
return; }) to first verify !res || res.error so you handle undefined/null
responses safely; keep using the same showMessage call and return behavior.
- Around line 385-388: Add a defensive null/undefined guard before accessing
res.error from the api('clone-session', ...) call: ensure the response variable
res is truthy (or use optional chaining) before checking res.error, and call
this.showMessage with either res.error or a fallback message then return;
reference the response variable res and the methods api('clone-session', ...)
and this.showMessage to locate and update the check.

In `@web-ui/partials/index/layout-footer.html`:
- Around line 2-9: The HTML comment between the Vue conditional blocks breaks
the v-if/v-else-if chain: remove or relocate the comment so the element with
v-else-if="initError" immediately follows the element with v-if="loading";
alternatively move the comment inside one of those elements or wrap the
conditionals in a <template> so the v-if (loading) and v-else-if (initError)
remain adjacent and the conditional chain is preserved.

In `@web-ui/partials/index/modal-confirm-toast.html`:
- Around line 23-26: The toast div currently only renders visually and isn't
announced to screen readers; update the element rendered by the template (the
div with v-if="message" and :class="['toast', messageType]") to be a live region
by adding appropriate ARIA attributes — e.g. add role="status",
aria-live="polite" and aria-atomic="true" (or bind aria-live to messageType so
errors can use "assertive") so changes to message are announced to assistive
tech.

In `@web-ui/partials/index/panel-config-openclaw.html`:
- Around line 31-37: The visible title "工作区文件" is not programmatically
associated with the input bound to openclawWorkspaceFileName (class
"form-input"), leaving it without a stable accessible name; update the markup so
the title and input are linked by adding a <label> that references a new unique
id on the input (or add an aria-label on the input) so screen readers can
announce the field — e.g., give the input an id like "openclaw-workspace-file"
and associate the selector-title text with a <label
for="openclaw-workspace-file"> or add aria-label="工作区文件" to the input if a
visible <label> isn’t used.

In `@web-ui/partials/index/panel-sessions.html`:
- Around line 278-280: The template branch rendering the empty browse pane
(class "session-preview-empty") uses sessionStandaloneError directly and can
display stale standalone-route errors; add a guard so standalone errors are only
shown when the app is actually in the standalone session route. Implement a
small computed property (e.g. showStandaloneError) or boolean (e.g.
isStandaloneRoute) that checks the current route/layout and return
sessionStandaloneError only when in the standalone route, then replace the
direct use of sessionStandaloneError in the v-if with that computed flag
(referencing sessionStandaloneError, showStandaloneError/isStandaloneRoute, and
the "session-preview-empty" template branch).

In `@web-ui/styles/base-theme.css`:
- Line 1: The CSS file starts with a UTF-8 BOM before the `@import` statement,
causing bundler/test failures; open the file and remove the leading BOM
character so the first character is the '@' of the "@import url('...')" line,
then save the file encoded as UTF-8 without BOM (or strip the leading \uFEFF
programmatically) and re-run tests to confirm the bundle is BOM-free.

---

Duplicate comments:
In `@cli.js`:
- Around line 10042-10049: The error handler for watcher.on('error') currently
closes the watcher and relies on syncDirectoryTree(target) which clears the
fallback watcher set when listDirectoryTree(rootDir) returns empty, leaving no
watcher to detect root recreation; update the error path so that when the target
is missing (fs.existsSync(target) is false and syncDirectoryTree would clear
watchers) you still install a parent/sentinel watcher (e.g.,
addWatcher(parentOf(target), /*recursive*/ false, /*isDirectory*/ true) or
equivalent) before or instead of clearing fallback watchers in
syncDirectoryTree; ensure closeWatcher(watchKey) remains called, but maintain a
sentinel watcher for the parent directory so the root can be observed and
addWatcher(target, recursive, isDirectory) is used again when the root
reappears.

In `@tests/unit/helpers/web-ui-app-options.mjs`:
- Around line 48-68: The head fixture is being built from the current checkout
(so PR CI compares the same snapshot); change
createHeadWebUiFixture/readHeadProjectFile to build from the repository "base"
commit instead of the working tree: compute a baseRef (prefer GITHUB_BASE_SHA or
GITHUB_BASE_REF if available, otherwise derive with git merge-base origin/<base>
HEAD), use that ref in readHeadProjectFile (git show <baseRef>:<path>), write
the extracted files into fixtureRoot first, then call
sourceBundle.collectJavaScriptFiles on the extracted entryPath inside
fixtureRoot (not projectRoot) so dependencyPaths are resolved against the
checked-out base snapshot; update references to HEAD_WEB_UI_ENTRY,
readHeadProjectFile, and createHeadWebUiFixture accordingly.

In `@web-ui/logic.agents-diff.mjs`:
- Around line 27-30: splitDiffLines currently returns an extra trailing empty
segment when normalized ends with a newline (e.g., "a\n" -> ["a",""]), so after
calling normalizeDiffText(input) and splitting by '\n', check whether
normalized.endsWith('\n') and if so remove the final empty segment (pop) before
returning; keep using splitDiffLines and normalizeDiffText as the identifiers to
locate the change.

In `@web-ui/modules/app.methods.navigation.mjs`:
- Around line 150-168: The touch path leaves transient nav state set by
setMainTabSwitchIntent/applyImmediateNavIntent and setSessionPanelFastHidden
when pointerType === 'touch' because it returns early; before returning,
register a one-shot cleanup listener on the event target for 'pointerup' and
'pointercancel' that clears the transient state (call
setMainTabSwitchIntent(''), applyImmediateNavIntent('') or the equivalent reset,
and setSessionPanelFastHidden(false)) and removes itself; apply the same change
to the matching handler (onSecondaryTabPointerDown) so both touch-cancel paths
properly clear transient nav state.

In `@web-ui/modules/app.methods.openclaw-core.mjs`:
- Around line 223-233: The current logic for building fallbackSource drops
legacy entries when both new and legacy shapes are present; update the logic in
the fallback resolution (symbols: legacyAgent, fallbackSource, model.fallbacks,
legacyAgent.model.fallbacks, fallbackList) to merge both sources instead of
choosing one: collect model.fallbacks (if Array) and legacyAgent.model.fallbacks
(if Array) into a single array (preserving desired order: e.g., new
model.fallbacks first then legacy), then apply the existing filter/map trimming
step to produce fallbackList so legacy fallback models are not lost.

In `@web-ui/modules/app.methods.providers.mjs`:
- Around line 167-179: The modal is being closed too early: remove the early
call to closeEditModal() before the API call and instead call closeEditModal()
only after a successful update (i.e., after checking res.error is falsy and
before/after await this.loadAll()); keep this.editingProvider intact on API
error or in the catch so the user's draft remains, and ensure the catch only
shows the error via this.showMessage('更新失败','error') without clearing
editingProvider.

In `@web-ui/partials/index/panel-config-claude.html`:
- Around line 54-56: The clickable card iteration over claudeConfigs is missing
accessibility attributes and keyboard handlers; update the element that uses
claudeConfigs/currentClaudeConfig and applyClaudeConfig(name) to include
role="button", tabindex="0", and add `@keydown.enter` and `@keydown.space` handlers
that invoke applyClaudeConfig(name) (and call event.preventDefault() if needed)
so keyboard users can activate the card; keep the existing :class and `@click`
behavior intact.
- Line 72: Add accessible aria-label attributes to the icon-only action buttons:
for the edit button (the <button class="card-action-btn" with
`@click`="openEditConfigModal(name)") add aria-label="Edit configuration" (keep
the existing title), and for the delete button (the other <button
class="card-action-btn" used for deletion) add aria-label="Delete
configuration"; ensure labels are descriptive and match the button purpose so
screen readers can announce them.

In `@web-ui/partials/index/panel-config-openclaw.html`:
- Around line 47-68: The card currently rendered in the v-for (using
openclawConfigs and keyed by name, with currentOpenclawConfig and
applyOpenclawConfig) is a plain clickable div which blocks keyboard activation;
change it to a semantic interactive element (preferably a <button> or an element
with role="button", tabindex="0" and keydown handlers) so pressing Enter/Space
calls applyOpenclawConfig(name) and the active styling still uses
currentOpenclawConfig === name; also add explicit aria-label attributes to the
edit and delete icon buttons that call openOpenclawEditModal(name) and
deleteOpenclawConfig(name) (e.g., aria-label="Edit {name}" and
aria-label="Delete {name}") and keep existing `@click.stop` on the .card-actions
to prevent event propagation, leaving openclawHasContent(config) and
openclawSubtitle(config) display logic unchanged.

In `@web-ui/partials/index/panel-sessions.html`:
- Around line 103-115: The session row div is not keyboard-accessible; update
the element that iterates sortedSessionsList so keyboard users can open a
session by adding role="button", tabindex="0", and keyboard handlers that invoke
selectSession(session) on Enter and Space (handle Space with preventDefault to
avoid scrolling); keep existing :key, v-memo and :class logic and ensure any
accessible state uses activeSessionExportKey/getSessionExportKey(session) and
isSessionPinned(session) for ARIA attributes if needed.

In `@web-ui/styles/controls-forms.css`:
- Around line 317-322: The hover rule for .btn-tool currently affects disabled
buttons; change the selector to only target enabled buttons (e.g. replace
.btn-tool:hover with .btn-tool:not(:disabled):hover or
.btn-tool:not([disabled]):hover) so disabled controls remain visually inert,
and/or add an explicit override for .btn-tool:disabled (or .btn-tool[disabled])
to neutralize transform, box-shadow, color and border changes and optionally
disable pointer-events.

In `@web-ui/styles/layout-shell.css`:
- Around line 154-156: The rule hiding .top-tabs is global; narrow it to only
the shell container by changing the selector from .top-tabs to a
container-scoped selector (e.g. .layout-shell .top-tabs or .shell-container
.top-tabs) so only top tabs inside the shell are hidden and other
standalone/legacy .top-tabs remain unaffected; update the selector in
layout-shell.css accordingly and keep the display:none !important inside that
scoped rule.

In `@web-ui/styles/navigation-panels.css`:
- Around line 255-266: The .top-tabs grid is defined with grid-template-columns:
repeat(4, 1fr) while the UI renders six tabs, causing wrapping; update the
.top-tabs rule to match the actual tab count (e.g., change grid-template-columns
to repeat(6, 1fr)) or use a responsive pattern like repeat(auto-fit,
minmax(...)) so the layout accommodates all tabs; edit the .top-tabs selector in
navigation-panels.css (look for the grid-template-columns property) and adjust
it to the chosen solution.

In `@web-ui/styles/responsive.css`:
- Around line 179-197: The .session-item-copy selector is being reduced later in
the stylesheet back to 20×20, undoing the 44px touch-target minimum; find the
later rule that explicitly sets .session-item-copy (and .btn-icon if present) to
fixed 20px sizes and remove or change it so .session-item-copy preserves
min-width:44px and min-height:44px (or use width/height:auto with min-
constraints) to maintain the mobile tap-target floor while keeping .btn-icon
adjustments separate if needed.

In `@web-ui/styles/titles-cards.css`:
- Around line 263-279: The .card-actions rule hides controls with opacity:0 but
leaves them tabbable; update the hidden state of .card-actions to also include
visibility: hidden and pointer-events: none, and ensure the reveal selectors
(.card:hover .card-actions and .card:focus-within .card-actions plus .mode-cards
.card-actions) set visibility: visible and pointer-events: auto (keeping the
existing opacity/transform transitions) so keyboard users can reveal and
interact with actions when the card receives focus.

---

Nitpick comments:
In `@tests/e2e/test-web-ui-assets.js`:
- Around line 72-79: The two complex regex assertions that test appEntry.body
for leaked relative imports/exports are hard to read; add concise inline
documentation and clearer identifiers so maintainers know what a "leaked"
directive means. Replace or accompany the inline regexes with well-named
constants (e.g., LEAKED_RELATIVE_IMPORT_RE and LEAKED_RELATIVE_REEXPORT_RE) and
a short comment explaining that a "leaked" directive is a relative import/export
(starts with "./" or "../") that should have been inlined/removed by the bundler
and therefore must not appear in the final app entry; update the assertions to
use those constants so the intent is explicit and the patterns are documented
for the other similar assertion around appEntry.body.

In `@web-ui/modules/app.computed.dashboard.mjs`:
- Around line 23-25: Remove the unused computed getter hasLocalAndProxy from the
module (the function named hasLocalAndProxy inside
web-ui/modules/app.computed.dashboard.mjs) — delete its definition entirely and
any trailing commas/commas adjustments so the exported computed object remains
syntactically correct; ensure no other symbols (methods/getters) rely on it by
searching for hasLocalAndProxy references before committing.

In `@web-ui/modules/app.methods.openclaw-editing.mjs`:
- Around line 268-273: The four methods removeOpenclawFallback,
removeOpenclawEnvItem, removeOpenclawToolsAllow, and removeOpenclawToolsDeny
should validate that the passed index is within 0 <= index < array.length before
calling splice; add a guard at the start of each method that checks the index
and either return early or log/throw a descriptive error (including the index
and array length) when the index is out of bounds, otherwise proceed with splice
and the existing empty-array fallback logic.

In `@web-ui/modules/app.methods.session-actions.mjs`:
- Around line 210-250: copyResumeCommand currently calls fallbackCopyText before
navigator.clipboard, causing inconsistent behavior with copyInstallCommand;
update copyResumeCommand to match copyInstallCommand by first attempting
navigator.clipboard.writeText(command) (guarded by navigator.clipboard &&
window.isSecureContext and wrapped in try/catch), showing success and returning
on success, then calling fallbackCopyText(command) and handling its boolean
result, and finally showing the failure message—ensure you keep the same
messages and also preserve the isResumeCommandAvailable and buildResumeCommand
checks.

In `@web-ui/partials/index/modals-basic.html`:
- Line 131: The modal overlay click handler currently sets showModelListModal =
false directly, which bypasses any modal cleanup logic; replace those direct
assignments (the `@click.self` handler and the other occurrence) with a call to
the modal's close/cleanup helper (e.g., closeModelListModal or
handleCloseModelList) so the same teardown runs for backdrop clicks as for other
close actions—if no helper exists, create a closeModelListModal method that
performs cleanup and sets showModelListModal = false, then call it from both
overlay click handlers.

In `@web-ui/styles/sessions-list.css`:
- Around line 359-363: The CSS rule uses display: none !important on
.session-item-sub.session-item-snippet, .session-preview-meta, and
.session-preview-title which hides these preview elements; either remove the
corresponding HTML nodes if they should never be rendered, or (if this hiding is
intentional/temporary) replace the blanket rule with a targeted approach: remove
the !important, scope the selector to the specific state (e.g.,
.sessions-list--collapsed .session-preview-title) or add a developer comment
explaining why these classes are hidden so future maintainers know it's
intentional; locate the rule in sessions-list.css and update the selector/markup
or add the explanatory comment accordingly.
🪄 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: 4703079f-2293-49e5-bdc2-2ffd4711d6fd

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and 3156c6b.

📒 Files selected for processing (76)
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-logic.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css
📜 Review details
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
📚 Learning: 2026-03-23T15:38:41.305Z
Learnt from: ymkiux
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-23T15:38:41.305Z
Learning: In the SakuraByteCore/codexmate repository, BOM (U+FEFF) characters in Chinese markdown and JavaScript config files (e.g., site/index.md, site/.vitepress/config.js) are intentional and follow the project's encoding convention. Do not flag BOM presence in these files as an issue. The PostCSS BOM parse issue was addressed separately by adding BOM-free site/package.json and site/postcss.config.cjs files.

Applied to files:

  • web-ui/logic.mjs
🪛 GitHub Actions: ci
tests/unit/run.mjs

[error] 38-38: test command failed due to failing unit test(s): npm run test:unit exited non-zero (Process completed with exit code 1).

tests/unit/web-ui-behavior-parity.test.mjs

[error] 319-319: Unit test failed in "captured bundled app skeleton still matches HEAD". AssertionError: Expected values to be strictly deep-equal; actual [] vs expected ['claudeModelsRequestSeq','codexModelsRequestSeq','sessionPathRequestSeqMap'].

🪛 HTMLHint (1.9.2)
web-ui/partials/index/layout-footer.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 10-10: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 12-12: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 69-69: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/panel-config-openclaw.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-settings.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-claude.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modals-basic.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/layout-header.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-market.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-skills.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-confirm-toast.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 27-27: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/panel-sessions.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-config-template-agents.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-openclaw-config.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-codex.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 4

♻️ Duplicate comments (11)
web-ui/styles/controls-forms.css (1)

317-322: ⚠️ Potential issue | 🟡 Minor

Prevent disabled tool buttons from receiving hover affordances.

Line 317-322 applies hover lift/color even when tool buttons are disabled, which creates misleading interactive feedback.

Minimal non-breaking fix
-.btn-tool:hover {
+.btn-tool:not(:disabled):hover {
     border-color: var(--color-brand);
     color: var(--color-brand);
     transform: translateY(-1px);
     box-shadow: 0 4px 8px rgba(210, 107, 90, 0.12);
 }
+
+.btn-tool:disabled,
+.btn-tool-compact:disabled {
+    opacity: 0.5;
+    cursor: not-allowed;
+    transform: none;
+    box-shadow: none;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/controls-forms.css` around lines 317 - 322, The hover styles
are being applied even when tool buttons are disabled; update the selector
and/or rules so disabled tools don't get hover affordances by scoping the rule
to enabled buttons only (e.g., change .btn-tool:hover to
.btn-tool:not(:disabled):not(.disabled):not([aria-disabled="true"]):hover) and
ensure any existing .btn-tool[disabled], .btn-tool.disabled or
.btn-tool[aria-disabled="true"] states do not receive
transform/color/box-shadow; adjust or add explicit disabled state rules to
override if needed (refer to the .btn-tool:hover selector and any
.btn-tool.disabled/.btn-tool[disabled] styles).
web-ui/partials/index/panel-config-openclaw.html (2)

31-37: ⚠️ Potential issue | 🟠 Major

Associate the “工作区文件” text with the input.

Line 32 and Line 34-37 render a visual label but no programmatic label, so the field can lose an accessible name once placeholder text is gone.

Minimal non-breaking fix
-                    <div class="selector-header">
-                        <span class="selector-title">工作区文件</span>
-                    </div>
+                    <div class="selector-header">
+                        <label class="selector-title" for="openclaw-workspace-file">工作区文件</label>
+                    </div>
                     <input
+                        id="openclaw-workspace-file"
                         class="form-input"
                         v-model="openclawWorkspaceFileName"
                         placeholder="例如: SOUL.md">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-openclaw.html` around lines 31 - 37, The
input bound to openclawWorkspaceFileName lacks a programmatic label; add an
accessible association by providing a label element or aria-labelledby/for link
between the visible "工作区文件" text (e.g., give the span a unique id or change it
to a <label> for the input) and the input element (add an id on the input and
use for or aria-labelledby) so the field retains an accessible name when the
placeholder is not present.

47-49: ⚠️ Potential issue | 🟠 Major

Make config-card selection and icon actions keyboard/screen-reader accessible.

Line 47-49 uses a clickable <div> (mouse-only by default), and Line 62/68 icon buttons rely on title only for naming.

Minimal non-breaking fix
-                    <div v-for="(config, name) in openclawConfigs" :key="name"
+                    <div v-for="(config, name) in openclawConfigs" :key="name"
                          :class="['card', { active: currentOpenclawConfig === name }]"
-                         `@click`="applyOpenclawConfig(name)">
+                         `@click`="applyOpenclawConfig(name)"
+                         role="button"
+                         tabindex="0"
+                         `@keydown.enter.prevent`="applyOpenclawConfig(name)"
+                         `@keydown.space.prevent`="applyOpenclawConfig(name)">
@@
-                                <button class="card-action-btn" `@click`="openOpenclawEditModal(name)" title="编辑">
+                                <button class="card-action-btn" `@click`="openOpenclawEditModal(name)" :aria-label="`编辑 OpenClaw 配置 ${name}`">
@@
-                                <button class="card-action-btn delete" `@click`="deleteOpenclawConfig(name)" title="删除">
+                                <button class="card-action-btn delete" `@click`="deleteOpenclawConfig(name)" :aria-label="`删除 OpenClaw 配置 ${name}`">

Also applies to: 62-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-openclaw.html` around lines 47 - 49, The
config "cards" rendered by v-for (openclawConfigs) are only mouse-clickable and
icon actions rely solely on title, so make the interactive elements
keyboard/screen-reader accessible by: convert the clickable <div> (where
currentOpenclawConfig and applyOpenclawConfig are used) into an element with
role="button", tabindex="0", and an `@keydown` handler that calls
applyOpenclawConfig(name) on Enter/Space and expose selection state with
aria-pressed or aria-current when currentOpenclawConfig === name; for the icon
actions around lines 62/68 replace or augment title-only icons with proper
accessible controls (use <button> or add role="button", tabindex="0", `@keydown`
handlers and explicit aria-label attributes) so screen readers and keyboard
users can activate them.
cli.js (1)

10042-10049: ⚠️ Potential issue | 🟠 Major

Keep a parent sentinel when a watched root disappears.

Lines 10042-10049 and 10062-10079 close vanished root watchers, but they never leave anything watching path.dirname(rootDir) while the root itself is missing. After that, recreating web-ui/ cannot trigger another resync, so frontend restart watching stays disabled until codexmate run is restarted.

#!/bin/bash
set -euo pipefail

sed -n '9954,10112p' cli.js

printf '\n-- recovery-path references inside watchPathsForRestart --\n'
sed -n '9954,10112p' cli.js | rg -n "path\\.dirname\\(|fs\\.existsSync\\(target\\)|syncDirectoryTree\\(|closeWatcher\\("

Expected: inside watchPathsForRestart, the only path.dirname(...) usage is the file-watch setup at Line 10024; there is no recovery branch that keeps a watcher on the parent directory when target/rootDir is absent.

Also applies to: 10062-10079

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10042 - 10049, The watcher error/recovery branches in
watchPathsForRestart currently close the root watcher but do not leave a parent
sentinel watcher, so recreating the missing root never triggers a resync; update
the watcher.on('error') handler (and the similar branch around the 10062-10079
region) to after closeWatcher(...) also ensure a parent sentinel is installed by
calling addWatcher(path.dirname(target), false) (or a non-recursive "sentinel"
watcher) and track it so you don't create duplicates; when the parent watcher
notices the child is recreated, replace the sentinel with the normal watcher and
call syncDirectoryTree(target) to perform the resync.
web-ui/modules/app.methods.providers.mjs (1)

146-181: ⚠️ Potential issue | 🟠 Major

Editing a provider URL may silently wipe the API key.

openEditModal initializes key: '' (line 149), and updateProvider sends this empty string to the server (line 171). If the server-side handler treats an empty string as an explicit "clear key" instruction rather than "no change", editing a provider's URL will unintentionally wipe its stored API key.

Consider only sending the key field when the user has explicitly entered a new value, or use a sentinel value to distinguish "unchanged" from "clear".

💡 Suggested approach
         async updateProvider() {
             if (this.editingProvider.readOnly || this.editingProvider.nonEditable) {
                 this.showMessage('该 provider 为保留项,不可编辑', 'error');
                 this.closeEditModal();
                 return;
             }
             const url = typeof this.editingProvider.url === 'string' ? this.editingProvider.url.trim() : '';
             if (!url) {
                 return this.showMessage('URL 必填', 'error');
             }

             const name = this.editingProvider.name;
-            const key = this.editingProvider.key || '';
+            const payload = { name, url };
+            // Only include key if user explicitly entered a new value
+            if (this.editingProvider.key) {
+                payload.key = this.editingProvider.key;
+            }
             this.closeEditModal();
             try {
-                const res = await api('update-provider', { name, url, key });
+                const res = await api('update-provider', payload);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 146 - 181, The edit
flow currently initializes editingProvider.key = '' in the modal and always
sends that empty string in updateProvider, which can unintentionally clear
stored API keys on the server; change updateProvider to build the payload
dynamically so it only includes the key field when the user has explicitly
provided a non-empty value (e.g., check editingProvider.key and add key to the
request payload only if present), otherwise omit the key property (or send a
clear-sentinel like null only if you have an explicit "clear key" action),
keeping the rest of the logic in updateProvider, the editingProvider
initialization in the edit modal, and the api('update-provider', ...) call
intact.
web-ui/partials/index/panel-sessions.html (2)

103-115: ⚠️ Potential issue | 🟠 Major

Make the session row keyboard-operable.

Line 114 still binds only @click on a plain div, so keyboard users cannot open a session from the list itself.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-sessions.html` around lines 103 - 115, The
session row is only clickable via `@click` on the div, so make it
keyboard-operable by turning the interactive container (the v-for div rendering
each session from sortedSessionsList) into an accessible control: either replace
the div with a semantic button or add tabindex="0" and role="button" to the div
and wire key handlers (e.g., `@keydown.enter` and `@keydown.space`) to call the same
selectSession(session) used by `@click` (ensure you preventDefault for space so it
doesn't scroll), and preserve existing bindings
(activeSessionExportKey/getSessionExportKey(session), isSessionPinned(session),
session.messageCount, session.updatedAt, session.title, sessionsLoading) and
class logic so keyboard users can open sessions equivalently to mouse users.

278-280: ⚠️ Potential issue | 🟡 Minor

Don't reuse standalone-route errors in the browse empty state.

Lines 278-280 render only when sessionStandalone is false, so sessionStandaloneError can leak a stale /session error into the normal browse pane.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-sessions.html` around lines 278 - 280, The browse
empty-state is showing sessionStandaloneError even when sessionStandalone is
false, leaking /session errors into the normal browse pane; update the
conditional so this span only renders when sessionStandalone is active (e.g.,
change <span v-if="sessionStandaloneError"> to <span v-if="sessionStandalone &&
sessionStandaloneError">) or replace it with a dedicated browse error
(browseError) instead of reusing sessionStandaloneError; ensure the template
logic around sessionStandalone and sessionStandaloneError (and any new
browseError) is adjusted accordingly.
web-ui/styles/layout-shell.css (1)

154-156: ⚠️ Potential issue | 🟠 Major

Scope the .top-tabs override to the shell layout.

Line 154 hides every .top-tabs instance once this stylesheet is loaded. Any legacy or standalone screen that still relies on that tab bar loses navigation entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/layout-shell.css` around lines 154 - 156, The global rule
hiding `.top-tabs` is too broad; narrow it to the shell layout only by replacing
the global override with a scoped selector targeting the shell container (e.g.
use the shell parent class such as `.layout-shell .top-tabs`) and remove or
avoid the blanket `!important` if possible so only `.top-tabs` inside the shell
layout is hidden and legacy/standalone screens keep their tab bar; update the
stylesheet to remove the original `.top-tabs { display: none !important; }` rule
and add the scoped rule instead.
web-ui/modules/app.methods.codex-config.mjs (1)

76-90: ⚠️ Potential issue | 🟠 Major

Keep currentProvider on the last successful value until model loading succeeds.

providerSwitchDisplayTarget already covers the optimistic label. Line 78 updates currentProvider before loadModelsForProvider(name) succeeds, so a failed load can leave later apply paths with the new provider paired with the old model.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.codex-config.mjs` around lines 76 - 90, In
performProviderSwitch, avoid setting this.currentProvider before
loadModelsForProvider succeeds: call await this.waitForCodexApplyIdle(); await
this.loadModelsForProvider(name); if the load indicates success
(this.modelsSource !== 'error'), only then set this.currentProvider = name (and
update this.modelsHasCurrent/currentModel as needed); use
providerSwitchDisplayTarget for the optimistic UI label and, if load fails,
leave this.currentProvider unchanged (or revert it in a try/catch) so later
apply paths don’t mix the new provider with the old model.
web-ui/modules/app.methods.openclaw-core.mjs (1)

3-13: ⚠️ Potential issue | 🟡 Minor

Guard window.JSON5 before dereferencing it.

Line 4 touches window unconditionally. If getOpenclawParser() is exercised from a Node-based unit or tooling path, it throws before the JSON fallback can run.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-core.mjs` around lines 3 - 13, The
function getOpenclawParser dereferences window.JSON5 unconditionally which
throws in non-browser environments; update getOpenclawParser to first check that
the global window exists (e.g., typeof window !== 'undefined') before accessing
window.JSON5, and only use window.JSON5.parse/stringify when window is defined
and JSON5 methods are functions, otherwise fall back to JSON.parse/stringify;
locate the logic inside getOpenclawParser to apply this guard.
web-ui/modules/app.methods.runtime.mjs (1)

104-113: ⚠️ Potential issue | 🟡 Minor

Stale progress-reset timer can interfere with subsequent backups.

The setTimeout at line 110-112 is not tracked or cancelled. If a user initiates a new backup within 800ms, the previous timer will reset the new operation's progress bar.

💡 Suggested fix
         } finally {
             if (this.claudeDownloadTimer) {
                 clearInterval(this.claudeDownloadTimer);
                 this.claudeDownloadTimer = null;
             }
             this.claudeDownloadLoading = false;
-            setTimeout(() => {
+            if (this.claudeDownloadResetTimer) {
+                clearTimeout(this.claudeDownloadResetTimer);
+            }
+            this.claudeDownloadResetTimer = setTimeout(() => {
                 this.claudeDownloadProgress = 0;
+                this.claudeDownloadResetTimer = null;
             }, 800);
         }

Apply the same pattern to downloadCodexDirectory (lines 146-155).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.runtime.mjs` around lines 104 - 113, The
setTimeout that resets this.claudeDownloadProgress is not stored/cleared,
causing a stale timer to reset progress of a new backup; change the code to save
the timeout id on the instance (e.g., this.claudeDownloadResetTimer), clear any
existing reset timer before creating a new one, and clear that reset timer in
the same finally block (and when starting the operation) so the progress reset
cannot race into a subsequent run; apply the identical pattern for
downloadCodexDirectory (manage a dedicated reset timer property,
clear-before-set, and clear in finally).
🧹 Nitpick comments (3)
web-ui/source-bundle.cjs (2)

166-186: Minor: Variable naming is slightly misleading but functionally correct.

On Line 168, sourcePath receives the result of typeof source === 'string' ? source : String(source || ''), but this variable actually represents a file path passed to readUtf8Text on Line 169, not source code content. The logic works correctly since source is expected to be a path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/source-bundle.cjs` around lines 166 - 186, The variable name
sourcePath in transformJavaScriptModuleSource is misleading because it holds the
file path (not in-memory source); rename it to sourceFilePath (and update any
references in this function such as the readUtf8Text call and passing into
buildRelativeImportAliasStatements) so the intent is clear; ensure you update
both the const declaration and all subsequent uses in
transformJavaScriptModuleSource to maintain behavior.

61-76: Side-effect imports are not currently present in the codebase.

The JS_IMPORT_RE pattern on Line 6 doesn't match pure side-effect imports like import './side-effect.mjs'; (without from). However, verification shows no such imports exist in the web-ui directory, so this is not a current issue. If side-effect relative imports are added in the future, the regex pattern should be updated to capture them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/source-bundle.cjs` around lines 61 - 76, The current
resolveJavaScriptDependencies implementation and its JS_IMPORT_RE only capture
imports with a module specifier following "from", so pure side-effect imports
like import './side-effect.mjs'; are missed; update the JS_IMPORT_RE (used by
resolveJavaScriptDependencies) to also match side-effect import syntax (allowing
either import ... from 'x' or import 'x') so that the captured group still
yields the module specifier passed to normalizeIncludeTarget; no other changes
to resolveJavaScriptDependencies, normalizeIncludeTarget, or the relative-target
handling (target.startsWith('.')) are required.
web-ui/styles/sessions-preview.css (1)

29-30: Consider backdrop-filter fallback for older browsers.

backdrop-filter: blur(6px) lacks universal support. The semi-transparent background gradient provides a reasonable fallback, but consider adding @supports for enhanced compatibility if targeting older browsers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/sessions-preview.css` around lines 29 - 30, Add a graceful
fallback around the backdrop-filter usage by wrapping the blur rule in an
`@supports` query (e.g., test for "backdrop-filter" or "-webkit-backdrop-filter")
so browsers that support it get the blur while older browsers continue to rely
on the existing semi-transparent linear-gradient background; update the CSS
where backdrop-filter: blur(6px) is defined to conditionally apply the filter
and include a vendor-prefixed check to cover WebKit browsers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/run.mjs`:
- Line 20: The dynamic import of the test suite (await
import(pathToFileURL(path.join(__dirname, 'web-ui-source-bundle.test.mjs')))) is
causing unhandled assertion failures when new suites register; wrap the import
in a try/catch in tests/unit/run.mjs around the
pathToFileURL/path.join/__dirname import, log the caught error with clear
context (which suite failed), and either rethrow a controlled Error to fail fast
with a readable message or mark/skip the problematic suite so CI is stable;
ensure the catch distinguishes import/load errors from test assertion failures
so failures surface deterministically in the unit gate.

In `@web-ui/modules/app.methods.claude-config.mjs`:
- Around line 64-93: In saveAndApplyConfig, after a successful apply via the
api('apply-claude-config') call, promote the profile by setting
this.currentClaudeConfig = name (so the UI reflects the newly-applied profile)
before calling this.refreshClaudeModelContext(); update both the success branch
where you currently showMessage and closeEditConfigModal, and also consider
setting currentClaudeConfig when the config has no apiKey and the profile being
saved is meant to become current; ensure you only set this.currentClaudeConfig
on successful apply to avoid changing state on failure.

In `@web-ui/partials/index/modal-config-template-agents.html`:
- Line 26: The backdrop click currently invokes closeAgentsModal regardless of
in-flight operations; modify the behavior so backdrop clicks are ignored while
agentsDiffLoading or agentsSaving are true—either update the template's
`@click.self` handler to only call closeAgentsModal when both agentsDiffLoading
and agentsSaving are false, or add an early-return guard at the start of the
closeAgentsModal method that checks agentsDiffLoading || agentsSaving and aborts
if true; reference the showAgentsModal conditional, the `@click.self` handler, the
closeAgentsModal method, and the agentsDiffLoading / agentsSaving flags when
making the change.
- Around line 88-95: The textarea currently binds readonly to agentsLoading
only, allowing edits during save; update the readonly binding on the textarea
(the element using v-model="agentsContent" and `@input`="onAgentsContentInput") to
also respect agentsSaving (e.g., :readonly="agentsLoading || agentsSaving") so
the field is non-editable while applyAgentsContent is running, and ensure any
save-related UI (agentsSaving) disables input consistently.

---

Duplicate comments:
In `@cli.js`:
- Around line 10042-10049: The watcher error/recovery branches in
watchPathsForRestart currently close the root watcher but do not leave a parent
sentinel watcher, so recreating the missing root never triggers a resync; update
the watcher.on('error') handler (and the similar branch around the 10062-10079
region) to after closeWatcher(...) also ensure a parent sentinel is installed by
calling addWatcher(path.dirname(target), false) (or a non-recursive "sentinel"
watcher) and track it so you don't create duplicates; when the parent watcher
notices the child is recreated, replace the sentinel with the normal watcher and
call syncDirectoryTree(target) to perform the resync.

In `@web-ui/modules/app.methods.codex-config.mjs`:
- Around line 76-90: In performProviderSwitch, avoid setting
this.currentProvider before loadModelsForProvider succeeds: call await
this.waitForCodexApplyIdle(); await this.loadModelsForProvider(name); if the
load indicates success (this.modelsSource !== 'error'), only then set
this.currentProvider = name (and update this.modelsHasCurrent/currentModel as
needed); use providerSwitchDisplayTarget for the optimistic UI label and, if
load fails, leave this.currentProvider unchanged (or revert it in a try/catch)
so later apply paths don’t mix the new provider with the old model.

In `@web-ui/modules/app.methods.openclaw-core.mjs`:
- Around line 3-13: The function getOpenclawParser dereferences window.JSON5
unconditionally which throws in non-browser environments; update
getOpenclawParser to first check that the global window exists (e.g., typeof
window !== 'undefined') before accessing window.JSON5, and only use
window.JSON5.parse/stringify when window is defined and JSON5 methods are
functions, otherwise fall back to JSON.parse/stringify; locate the logic inside
getOpenclawParser to apply this guard.

In `@web-ui/modules/app.methods.providers.mjs`:
- Around line 146-181: The edit flow currently initializes editingProvider.key =
'' in the modal and always sends that empty string in updateProvider, which can
unintentionally clear stored API keys on the server; change updateProvider to
build the payload dynamically so it only includes the key field when the user
has explicitly provided a non-empty value (e.g., check editingProvider.key and
add key to the request payload only if present), otherwise omit the key property
(or send a clear-sentinel like null only if you have an explicit "clear key"
action), keeping the rest of the logic in updateProvider, the editingProvider
initialization in the edit modal, and the api('update-provider', ...) call
intact.

In `@web-ui/modules/app.methods.runtime.mjs`:
- Around line 104-113: The setTimeout that resets this.claudeDownloadProgress is
not stored/cleared, causing a stale timer to reset progress of a new backup;
change the code to save the timeout id on the instance (e.g.,
this.claudeDownloadResetTimer), clear any existing reset timer before creating a
new one, and clear that reset timer in the same finally block (and when starting
the operation) so the progress reset cannot race into a subsequent run; apply
the identical pattern for downloadCodexDirectory (manage a dedicated reset timer
property, clear-before-set, and clear in finally).

In `@web-ui/partials/index/panel-config-openclaw.html`:
- Around line 31-37: The input bound to openclawWorkspaceFileName lacks a
programmatic label; add an accessible association by providing a label element
or aria-labelledby/for link between the visible "工作区文件" text (e.g., give the
span a unique id or change it to a <label> for the input) and the input element
(add an id on the input and use for or aria-labelledby) so the field retains an
accessible name when the placeholder is not present.
- Around line 47-49: The config "cards" rendered by v-for (openclawConfigs) are
only mouse-clickable and icon actions rely solely on title, so make the
interactive elements keyboard/screen-reader accessible by: convert the clickable
<div> (where currentOpenclawConfig and applyOpenclawConfig are used) into an
element with role="button", tabindex="0", and an `@keydown` handler that calls
applyOpenclawConfig(name) on Enter/Space and expose selection state with
aria-pressed or aria-current when currentOpenclawConfig === name; for the icon
actions around lines 62/68 replace or augment title-only icons with proper
accessible controls (use <button> or add role="button", tabindex="0", `@keydown`
handlers and explicit aria-label attributes) so screen readers and keyboard
users can activate them.

In `@web-ui/partials/index/panel-sessions.html`:
- Around line 103-115: The session row is only clickable via `@click` on the div,
so make it keyboard-operable by turning the interactive container (the v-for div
rendering each session from sortedSessionsList) into an accessible control:
either replace the div with a semantic button or add tabindex="0" and
role="button" to the div and wire key handlers (e.g., `@keydown.enter` and
`@keydown.space`) to call the same selectSession(session) used by `@click` (ensure
you preventDefault for space so it doesn't scroll), and preserve existing
bindings (activeSessionExportKey/getSessionExportKey(session),
isSessionPinned(session), session.messageCount, session.updatedAt,
session.title, sessionsLoading) and class logic so keyboard users can open
sessions equivalently to mouse users.
- Around line 278-280: The browse empty-state is showing sessionStandaloneError
even when sessionStandalone is false, leaking /session errors into the normal
browse pane; update the conditional so this span only renders when
sessionStandalone is active (e.g., change <span v-if="sessionStandaloneError">
to <span v-if="sessionStandalone && sessionStandaloneError">) or replace it with
a dedicated browse error (browseError) instead of reusing
sessionStandaloneError; ensure the template logic around sessionStandalone and
sessionStandaloneError (and any new browseError) is adjusted accordingly.

In `@web-ui/styles/controls-forms.css`:
- Around line 317-322: The hover styles are being applied even when tool buttons
are disabled; update the selector and/or rules so disabled tools don't get hover
affordances by scoping the rule to enabled buttons only (e.g., change
.btn-tool:hover to
.btn-tool:not(:disabled):not(.disabled):not([aria-disabled="true"]):hover) and
ensure any existing .btn-tool[disabled], .btn-tool.disabled or
.btn-tool[aria-disabled="true"] states do not receive
transform/color/box-shadow; adjust or add explicit disabled state rules to
override if needed (refer to the .btn-tool:hover selector and any
.btn-tool.disabled/.btn-tool[disabled] styles).

In `@web-ui/styles/layout-shell.css`:
- Around line 154-156: The global rule hiding `.top-tabs` is too broad; narrow
it to the shell layout only by replacing the global override with a scoped
selector targeting the shell container (e.g. use the shell parent class such as
`.layout-shell .top-tabs`) and remove or avoid the blanket `!important` if
possible so only `.top-tabs` inside the shell layout is hidden and
legacy/standalone screens keep their tab bar; update the stylesheet to remove
the original `.top-tabs { display: none !important; }` rule and add the scoped
rule instead.

---

Nitpick comments:
In `@web-ui/source-bundle.cjs`:
- Around line 166-186: The variable name sourcePath in
transformJavaScriptModuleSource is misleading because it holds the file path
(not in-memory source); rename it to sourceFilePath (and update any references
in this function such as the readUtf8Text call and passing into
buildRelativeImportAliasStatements) so the intent is clear; ensure you update
both the const declaration and all subsequent uses in
transformJavaScriptModuleSource to maintain behavior.
- Around line 61-76: The current resolveJavaScriptDependencies implementation
and its JS_IMPORT_RE only capture imports with a module specifier following
"from", so pure side-effect imports like import './side-effect.mjs'; are missed;
update the JS_IMPORT_RE (used by resolveJavaScriptDependencies) to also match
side-effect import syntax (allowing either import ... from 'x' or import 'x') so
that the captured group still yields the module specifier passed to
normalizeIncludeTarget; no other changes to resolveJavaScriptDependencies,
normalizeIncludeTarget, or the relative-target handling (target.startsWith('.'))
are required.

In `@web-ui/styles/sessions-preview.css`:
- Around line 29-30: Add a graceful fallback around the backdrop-filter usage by
wrapping the blur rule in an `@supports` query (e.g., test for "backdrop-filter"
or "-webkit-backdrop-filter") so browsers that support it get the blur while
older browsers continue to rely on the existing semi-transparent linear-gradient
background; update the CSS where backdrop-filter: blur(6px) is defined to
conditionally apply the filter and include a vendor-prefixed check to cover
WebKit browsers.
🪄 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: 71ef6e14-0960-4a6f-a9df-fb7ab41d96b1

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and 3156c6b.

📒 Files selected for processing (76)
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-logic.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css
📜 Review details
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
📚 Learning: 2026-03-23T15:38:41.305Z
Learnt from: ymkiux
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-23T15:38:41.305Z
Learning: In the SakuraByteCore/codexmate repository, BOM (U+FEFF) characters in Chinese markdown and JavaScript config files (e.g., site/index.md, site/.vitepress/config.js) are intentional and follow the project's encoding convention. Do not flag BOM presence in these files as an issue. The PostCSS BOM parse issue was addressed separately by adding BOM-free site/package.json and site/postcss.config.cjs files.

Applied to files:

  • web-ui/styles/base-theme.css
  • web-ui/logic.mjs
🪛 GitHub Actions: ci
tests/unit/run.mjs

[error] 38-38: Command 'node tests/unit/run.mjs' failed due to the unit test assertion failure, causing 'npm run test:unit' (and thus 'npm run test') to exit with code 1.

tests/unit/web-ui-behavior-parity.test.mjs

[error] 319-319: AssertionError in unit test 'captured bundled app skeleton still matches HEAD'. Expected deepStrictEqual of request sequence keys, but actual array was []. Error: Expected values to be strictly deep-equal; actual [] vs expected ['claudeModelsRequestSeq','codexModelsRequestSeq','sessionPathRequestSeqMap'].

🪛 HTMLHint (1.9.2)
web-ui/partials/index/layout-footer.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 10-10: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 12-12: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 69-69: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/modal-confirm-toast.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 27-27: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/modals-basic.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-claude.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-codex.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-config-openclaw.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-config-template-agents.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-market.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-settings.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/layout-header.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-skills.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modal-openclaw-config.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-sessions.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 4, 2026

@coderabbitai re-review
Stop making breaking changes, do a proper review!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

@github-actions[bot]: Understood, and I sincerely apologize for the repeated pattern of suggesting breaking changes. This is an absolute constraint that has been reinforced across PRs #55, #59, #67, and #68, and it will be strictly honored.

Triggering a full re-review now with the following hard limits:

  • ✅ Correctness issues only
  • ✅ Safety issues only
  • ✅ Quality issues only
  • ❌ No breaking changes — period
  • ❌ No refactoring suggestions
  • ❌ No API/interface/behavior alterations of any kind

Review strictly limited to correctness, safety, and quality within the existing design. No breaking changes, no refactoring, no API/interface/behavior alterations. Constraint reinforced across PRs #55, #59, #67, and #68.


🧠 Learnings used
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.950Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.344Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:50.988Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.339Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.761Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
✅ Actions performed

Full review triggered.

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 3

♻️ Duplicate comments (3)
web-ui/modules/app.methods.session-actions.mjs (1)

380-387: ⚠️ Potential issue | 🟡 Minor

Guard the clone response before reading res.error.

If api('clone-session', ...) resolves a falsy value, Line 385 throws before this branch can handle it, and the flow falls through to the generic catch toast instead.

🩹 Minimal fix
-                if (res.error) {
-                    this.showMessage(res.error, 'error');
+                if (!res || res.error) {
+                    this.showMessage((res && res.error) || '克隆失败', 'error');
                     return;
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-actions.mjs` around lines 380 - 387, The
clone-session response is not guarded before accessing res.error; update the
code that calls api('clone-session', ...) (the variable res in this method in
app.methods.session-actions.mjs) to first verify res is truthy (e.g., if (!res)
{ this.showMessage('Empty response from clone-session', 'error'); return; }) and
then check res.error before using it, ensuring you handle falsy/undefined
responses and return after showing the error so the flow doesn't fall through to
the generic catch block.
web-ui/partials/index/modals-basic.html (1)

15-16: ⚠️ Potential issue | 🟠 Major

Mask the API-key fields in these modals.

Lines 16, 102, 159, and 184 are still plain-text inputs, so provider and Claude secrets stay visible during entry/editing and are easier for browser helpers to capture.

Minimal fix
-                    <input v-model="newProvider.key" class="form-input" placeholder="sk-...">
+                    <input v-model="newProvider.key" type="password" autocomplete="off" spellcheck="false" class="form-input" placeholder="sk-...">
@@
-                    <input v-model="editingProvider.key" class="form-input" placeholder="留空则保持不变">
+                    <input v-model="editingProvider.key" type="password" autocomplete="off" spellcheck="false" class="form-input" placeholder="留空则保持不变">
@@
-                    <input v-model="newClaudeConfig.apiKey" class="form-input" placeholder="sk-ant-...">
+                    <input v-model="newClaudeConfig.apiKey" type="password" autocomplete="off" spellcheck="false" class="form-input" placeholder="sk-ant-...">
@@
-                    <input v-model="editingConfig.apiKey" class="form-input" placeholder="sk-ant-...">
+                    <input v-model="editingConfig.apiKey" type="password" autocomplete="off" spellcheck="false" class="form-input" placeholder="sk-ant-...">

Also applies to: 101-102, 158-159, 183-184

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modals-basic.html` around lines 15 - 16, The API key
inputs are plain-text; change the relevant <input> elements bound to
v-model="newProvider.key" (and the other provider key/secret inputs with similar
v-model bindings) to type="password" so secrets are masked during entry, add
autocomplete="new-password" to discourage autofill, and keep the existing
v-model and placeholder intact; optionally implement a separate visible toggle
later, but the minimal fix is switching the input type and adding autocomplete
on the inputs currently showing API keys.
web-ui/partials/index/panel-config-openclaw.html (1)

48-50: ⚠️ Potential issue | 🟠 Major

Make the config cards keyboard-operable and give the icon buttons real names.

The selectable card is still a clickable <div>, so keyboard users cannot focus or activate it. The edit/delete icon buttons still rely on title, which is not a reliable accessible name. Please keyboard-test with Tab/Enter/Space after the fix.

Minimal fix
-                    <div v-for="(config, name) in openclawConfigs" :key="name"
-                         :class="['card', { active: currentOpenclawConfig === name }]"
-                         `@click`="applyOpenclawConfig(name)">
+                    <div v-for="(config, name) in openclawConfigs" :key="name"
+                         :class="['card', { active: currentOpenclawConfig === name }]"
+                         role="button"
+                         tabindex="0"
+                         `@click`="applyOpenclawConfig(name)"
+                         `@keydown.enter.prevent`="applyOpenclawConfig(name)"
+                         `@keydown.space.prevent`="applyOpenclawConfig(name)">
@@
-                                <button class="card-action-btn" `@click`="openOpenclawEditModal(name)" title="编辑">
+                                <button class="card-action-btn" `@click`="openOpenclawEditModal(name)" :aria-label="`编辑 OpenClaw 配置 ${name}`">
@@
-                                <button class="card-action-btn delete" `@click`="deleteOpenclawConfig(name)" title="删除">
+                                <button class="card-action-btn delete" `@click`="deleteOpenclawConfig(name)" :aria-label="`删除 OpenClaw 配置 ${name}`">

Also applies to: 63-69

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-openclaw.html` around lines 48 - 50, The
config "card" element is still a non-focusable div and the icon buttons rely
only on title; make the cards keyboard-operable by replacing the clickable <div>
in the v-for for openclawConfigs with a focusable control (e.g., a <button> or
role="button" with tabindex and key handlers) so that currentOpenclawConfig
selection via applyOpenclawConfig(name) can be triggered with Enter/Space, and
update the edit/delete icon buttons to use real accessible names (add explicit
aria-label attributes or visually hidden text and ensure they are actual
<button> elements) — apply the same changes for the duplicate block around lines
63-69.
🧹 Nitpick comments (1)
tests/unit/helpers/web-ui-app-options.mjs (1)

135-147: Environment override bypasses ref validation.

The resolveBehaviorParityBaselineRef function returns the WEB_UI_PARITY_BASE_REF override directly without calling gitRefExists() to validate it. While this will eventually fail with a clear git error if the ref is invalid, you might consider validating the override upfront for a more descriptive error message.

That said, for test helper code, the current behavior is acceptable since git's error message will clearly indicate the invalid ref.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/helpers/web-ui-app-options.mjs` around lines 135 - 147,
resolveBehaviorParityBaselineRef currently returns the WEB_UI_PARITY_BASE_REF
override without validation; update the function to call gitRefExists on the
override (String(process.env.WEB_UI_PARITY_BASE_REF || '').trim()) and if
gitRefExists returns false, throw or log a clear descriptive error mentioning
the invalid override value instead of returning it, otherwise return the
validated override; keep existing fallback logic using
hasTrackedChanges(getBehaviorParityTrackedPaths()) and gitRefExists('HEAD^')
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web-ui/modules/app.methods.agents.mjs`:
- Around line 65-73: The code currently only trims openclawWorkspaceFileName
before calling the APIs; add strict validation on fileName (the trimmed
this.openclawWorkspaceFileName) to reject anything not a plain workspace
Markdown filename: ensure it endsWith('.md'), does not start with '/' or contain
path separators or '..' segments (no '/' or '\' or sequences like '..'), and is
non-empty; if invalid, call this.showMessage('仅支持 OpenClaw Workspace 内的 `.md`
文件', 'error') and return before setting context or calling the APIs. Apply this
same validation logic to both places that call
api('get-openclaw-workspace-file', { fileName }) and
api('apply-openclaw-workspace-file', { fileName, ... }) so the checks run before
setAgentsModalContext/agentsLoading and before any API invocation.
- Around line 154-168: The unload guard is disabled while a save is running
because hasPendingAgentsDraft() returns false when this.agentsSaving is true;
remove agentsSaving from the early-return so the function continues to evaluate
this.hasAgentsContentChanged() || this.agentsDiffVisible (or explicitly return
true when agentsSaving is true and there is content/diff). Edit the
hasPendingAgentsDraft method (and ensure handleBeforeUnload uses it) so the
check becomes: only bail out for !this.showAgentsModal or this.agentsLoading,
not for this.agentsSaving, thereby keeping the beforeunload warning active
during saves.

In `@web-ui/modules/app.methods.claude-config.mjs`:
- Around line 21-30: The current flow in the handler that sets
this.currentClaudeModel and updates this.claudeConfigs short-circuits whenever
apiKey is missing, preventing configs that rely on externalCredentialType from
reaching applyClaudeConfig(name); change the guard so it only blocks and returns
when neither an apiKey nor an externalCredentialType is present (i.e., allow
proceed when externalCredentialType exists), keeping the calls to
this.saveClaudeConfigs(), this.updateClaudeModelsCurrent(), and ensuring
this.applyClaudeConfig(name) still executes for external-credential-backed
configs; reference this.claudeConfigs[name], this.mergeClaudeConfig,
this.saveClaudeConfigs, this.updateClaudeModelsCurrent, and
this.applyClaudeConfig when making the change.

---

Duplicate comments:
In `@web-ui/modules/app.methods.session-actions.mjs`:
- Around line 380-387: The clone-session response is not guarded before
accessing res.error; update the code that calls api('clone-session', ...) (the
variable res in this method in app.methods.session-actions.mjs) to first verify
res is truthy (e.g., if (!res) { this.showMessage('Empty response from
clone-session', 'error'); return; }) and then check res.error before using it,
ensuring you handle falsy/undefined responses and return after showing the error
so the flow doesn't fall through to the generic catch block.

In `@web-ui/partials/index/modals-basic.html`:
- Around line 15-16: The API key inputs are plain-text; change the relevant
<input> elements bound to v-model="newProvider.key" (and the other provider
key/secret inputs with similar v-model bindings) to type="password" so secrets
are masked during entry, add autocomplete="new-password" to discourage autofill,
and keep the existing v-model and placeholder intact; optionally implement a
separate visible toggle later, but the minimal fix is switching the input type
and adding autocomplete on the inputs currently showing API keys.

In `@web-ui/partials/index/panel-config-openclaw.html`:
- Around line 48-50: The config "card" element is still a non-focusable div and
the icon buttons rely only on title; make the cards keyboard-operable by
replacing the clickable <div> in the v-for for openclawConfigs with a focusable
control (e.g., a <button> or role="button" with tabindex and key handlers) so
that currentOpenclawConfig selection via applyOpenclawConfig(name) can be
triggered with Enter/Space, and update the edit/delete icon buttons to use real
accessible names (add explicit aria-label attributes or visually hidden text and
ensure they are actual <button> elements) — apply the same changes for the
duplicate block around lines 63-69.

---

Nitpick comments:
In `@tests/unit/helpers/web-ui-app-options.mjs`:
- Around line 135-147: resolveBehaviorParityBaselineRef currently returns the
WEB_UI_PARITY_BASE_REF override without validation; update the function to call
gitRefExists on the override (String(process.env.WEB_UI_PARITY_BASE_REF ||
'').trim()) and if gitRefExists returns false, throw or log a clear descriptive
error mentioning the invalid override value instead of returning it, otherwise
return the validated override; keep existing fallback logic using
hasTrackedChanges(getBehaviorParityTrackedPaths()) and gitRefExists('HEAD^')
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: d999b4d8-57c5-4901-9932-caa118bf6ca4

📥 Commits

Reviewing files that changed from the base of the PR and between 3156c6b and a7eb34d.

📒 Files selected for processing (13)
  • .github/workflows/ci.yml
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/styles/sessions-list.css
✅ Files skipped from review due to trivial changes (1)
  • web-ui/styles/sessions-list.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.964Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.368Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:51.023Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.352Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.
Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.785Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
🪛 HTMLHint (1.9.2)
web-ui/partials/index/modal-confirm-toast.html

[error] 1-1: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 32-32: Tag must be paired, no start tag: [ ]

(tag-pair)

web-ui/partials/index/panel-config-openclaw.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/modals-basic.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/panel-sessions.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)

web-ui/partials/index/layout-footer.html

[error] 2-2: Doctype must be declared before any non-comment content.

(doctype-first)


[error] 10-10: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 12-12: Tag must be paired, no start tag: [ ]

(tag-pair)


[error] 69-69: Tag must be paired, no start tag: [ ]

(tag-pair)

🔇 Additional comments (9)
.github/workflows/ci.yml (2)

16-17: Full history checkout is appropriate for parity tests.

The fetch-depth: 0 correctly enables access to git history needed by git show <ref>:path and git ls-tree commands in the parity test helpers.


28-29: Verify HEAD^2^ exists for single-commit PRs.

When a PR contains only one commit, HEAD^2^ may not resolve (the PR branch tip has no parent beyond the merge base). The helper resolveBehaviorParityBaselineRef() returns the override directly without validating it exists. This could cause git show HEAD^2^:... to fail during tests.

Consider whether tests should gracefully handle this edge case, or whether single-commit PRs are expected to always have a valid HEAD^2^ in practice.
[raise_minor_issue, request_verification]

Run the following to check how the ref is validated downstream:

#!/bin/bash
# Check if there's any validation of the resolved ref before use in git commands
rg -n -A5 'resolveBehaviorParityBaselineRef|captureGitBundledAppOptions' tests/unit/helpers/web-ui-app-options.mjs
tests/unit/helpers/web-ui-app-options.mjs (6)

1-22: LGTM!

The imports and constants are well-organized. The regex pattern correctly matches JavaScript module extensions, and the path constants align with the project structure.


23-52: LGTM!

The global override mechanism correctly preserves original property descriptors and restores them in LIFO order. The finally block ensures cleanup even when the callback throws.


54-113: LGTM!

The git and filesystem utilities are well-implemented. Using -z for null-separated output handles edge cases with special characters in paths, and the path normalization ensures cross-platform compatibility.


149-214: LGTM!

The git fixture materialization correctly extracts files from a git ref into a temp directory, preserving the directory structure. The document mock provides a minimal but sufficient DOM surface for Vue app initialization without requiring a full DOM implementation.


216-318: LGTM!

The capture mechanism is well-designed:

  • Cache-busting prevents module caching issues across test runs
  • The browser environment mocks are comprehensive
  • requestAnimationFrame correctly uses setTimeout for async behavior
  • Cleanup is properly handled in the finally block
  • The error thrown when appOptions is null provides a clear failure message

320-344: LGTM!

The exported functions correctly implement the parity testing pattern:

  • captureCurrentBundledAppOptions reads from the working tree
  • captureGitBundledAppOptions creates an isolated fixture from git history and bundles from there
  • captureBehaviorParityBaselineAppOptions returns the expected { ref, options } structure that tests consume

The fixture cleanup in the finally block ensures no temp directories are left behind.

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

421-438: Delete response handling looks solid now.

The !res || res.error guard and the single total-count update path close the earlier null-response and duplicated-branch hazards here.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

@github-actions[bot]: Understood, and I apologize for the repeated violations. I will now conduct the review with an absolute hard constraint:

  • ✅ Correctness issues only
  • ✅ Safety issues only
  • ✅ Quality issues only
  • ❌ No breaking changes — period
  • ❌ No refactoring suggestions
  • ❌ No API/interface/behavior alterations of any kind

This constraint is enforced across PRs #55, #59, #67, and #68 and will be strictly honored throughout this review.

Review strictly limited to correctness, safety, and quality within the existing design. No breaking changes, no refactoring, no API/interface/behavior alterations. Constraint reinforced across PRs #55, #59, #67, and #68.


🧠 Learnings used
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.964Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.368Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:51.023Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.352Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.785Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.
✅ Actions performed

Full review triggered.

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 17

♻️ Duplicate comments (22)
tests/unit/run.mjs (1)

20-32: ⚠️ Potential issue | 🟠 Major

Pipeline failure: Unit test suite registration is causing CI failure.

The CI pipeline reports Failed 1 test(s) originating from the test runner. The newly registered test suites at lines 20, 26, and 32 are contributing to this failure. Please investigate and fix the failing assertion(s) in the newly added test modules before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/run.mjs` around lines 20 - 32, The CI failure is caused by failing
assertions in the newly imported test modules; open and run the three added test
files (web-ui-source-bundle.test.mjs, provider-switch-regression.test.mjs,
web-ui-behavior-parity.test.mjs), identify the failing assertions, and fix them
by correcting expected values or test setup/mocks (ensure proper async/await,
teardown, and stubs/fixtures are initialized and restored); re-run the unit
tests locally to verify the failures are resolved before re-adding their import
lines to tests/unit/run.mjs.
web-ui/partials/index/modals-basic.html (1)

16-16: ⚠️ Potential issue | 🟠 Major

Mask API-key inputs instead of rendering secrets as plain text.

At Line 16, Line 102, Line 159, and Line 184, key fields are plain text. This exposes credentials during input/edit flows.

Minimal safe fix
-<input v-model="newProvider.key" class="form-input" placeholder="sk-...">
+<input v-model="newProvider.key" type="password" autocomplete="off" spellcheck="false" class="form-input" placeholder="sk-...">

-<input v-model="editingProvider.key" class="form-input" placeholder="留空则保持不变">
+<input v-model="editingProvider.key" type="password" autocomplete="off" spellcheck="false" class="form-input" placeholder="留空则保持不变">

-<input v-model="newClaudeConfig.apiKey" class="form-input" placeholder="sk-ant-...">
+<input v-model="newClaudeConfig.apiKey" type="password" autocomplete="off" spellcheck="false" class="form-input" placeholder="sk-ant-...">

-<input v-model="editingConfig.apiKey" class="form-input" placeholder="sk-ant-...">
+<input v-model="editingConfig.apiKey" type="password" autocomplete="off" spellcheck="false" class="form-input" placeholder="sk-ant-...">

Also applies to: 102-102, 159-159, 184-184

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modals-basic.html` at line 16, The API key fields are
rendered as plain text (e.g., the input bound to newProvider.key), exposing
secrets; change those inputs to type="password" for newProvider.key and the
other key v-models referenced in the file (the edit/create provider key inputs
at the other occurrences) so the value is masked during entry, and optionally
add a separate show/hide toggle control if visibility is needed later.
web-ui/styles/responsive.css (1)

216-227: ⚠️ Potential issue | 🟡 Minor

.session-item-copy overrides the 44px mobile tap-target floor.

Lines 194-197 set min-width: 44px and min-height: 44px for touch accessibility, but lines 217-220 immediately shrink .session-item-copy to 20px in all dimensions. This creates an undersized touch target on mobile.

Keep the button box at 44px and only shrink the inner icon if a smaller visual is desired.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/responsive.css` around lines 216 - 227, The .session-item-copy
rule currently forces a 20x20 touch target which violates the 44px mobile
tap-floor; update the CSS for .session-item-copy to preserve accessibility by
keeping min-width: 44px and min-height: 44px (remove or change the explicit
width/height values that set 20px), and if a smaller visual is desired, scale
only the inner icon (e.g. target the child SVG or .icon inside
.session-item-copy) to 20px while retaining the 44px interactive box and
existing alignment rules like display, padding and transform.
web-ui/modules/app.methods.install.mjs (1)

27-34: ⚠️ Potential issue | 🟡 Minor

Scheme-only registry URLs pass validation but produce invalid commands.

Values like "http://" or "https://" satisfy the prefix check at line 30, then get trimmed to "http:" or "https:" at line 33. This generates an invalid --registry= argument even though validation succeeded.

🐛 Proposed fix to reject scheme-only URLs
 normalizeInstallRegistryUrl(value) {
     const normalized = typeof value === 'string' ? value.trim() : '';
     if (!normalized) return '';
-    if (!/^https?:\/\//i.test(normalized)) {
+    if (!/^https?:\/\/[^/\s]+/i.test(normalized)) {
         return '';
     }
     return normalized.replace(/\/+$/, '');
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.install.mjs` around lines 27 - 34, The
normalizeInstallRegistryUrl function currently accepts scheme-only strings like
"http://" because it only checks the prefix; update it to reject URLs that lack
a host by parsing the trimmed value (e.g., new URL(normalized) inside a
try/catch) or by verifying that after removing the scheme the remaining part
contains a hostname (non-empty and not just slashes); if parsing/hostname check
fails return '' and otherwise return the normalized value with trailing slashes
removed. Ensure you keep the existing trailing-slash trim logic and reference
normalizeInstallRegistryUrl for the change.
web-ui/partials/index/panel-config-openclaw.html (1)

47-77: ⚠️ Potential issue | 🟠 Major

Config card and icon buttons lack keyboard accessibility.

The config card (line 48-50) uses a clickable <div> that cannot be activated via keyboard (Enter/Space). The edit and delete buttons (lines 63, 69) rely solely on title attributes, which screen readers don't consistently announce for icon-only buttons.

Consider adding tabindex="0" and @keydown.enter/@keydown.space handlers to the card div, or converting it to a <button>. Add aria-label attributes to the icon buttons.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-openclaw.html` around lines 47 - 77, The
card list uses a clickable <div> for each config (v-for over openclawConfigs
with currentOpenclawConfig and applyOpenclawConfig) which is not
keyboard-accessible and the icon-only buttons (openOpenclawEditModal,
deleteOpenclawConfig) rely only on title text; make the card keyboard-focusable
by adding tabindex="0" and wiring `@keydown.enter` and `@keydown.space` to call
applyOpenclawConfig(name) (or convert the card wrapper into a semantic <button>
element) and add explicit aria-label attributes to the edit and delete buttons
(used by openOpenclawEditModal and deleteOpenclawConfig) so screen readers
announce their purpose.
web-ui/styles/navigation-panels.css (1)

255-266: ⚠️ Potential issue | 🟡 Minor

Grid column count may not accommodate all tabs.

The .top-tabs grid uses repeat(4, 1fr) but if 6 tabs exist in layout-header.html, they will wrap unexpectedly. Verify this is handled by responsive CSS or update to match the actual tab count.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/navigation-panels.css` around lines 255 - 266, The .top-tabs
rule currently hardcodes grid-template-columns: repeat(4, 1fr) which breaks when
layout-header.html contains 6 tabs; change the CSS for .top-tabs to a responsive
grid (for example use grid-template-columns: repeat(auto-fit,
minmax(<reasonable-min-width>, 1fr)) or auto-fill with minmax) so the number of
columns adapts to content/viewport, or alternatively match the explicit
repeat(...) to the actual tab count in layout-header.html if a fixed number is
required; update the .top-tabs selector accordingly and verify behavior with 6
tabs in layout-header.html.
web-ui/styles/titles-cards.css (1)

263-279: ⚠️ Potential issue | 🟠 Major

Outside .mode-cards, the hidden action buttons are still tabbable.

The default hidden state only changes opacity and transform, so keyboard focus can still land on invisible controls until hover reveals them. Make the hidden state non-interactive and reveal it on keyboard focus (:focus-within) too.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/titles-cards.css` around lines 263 - 279, Hidden action buttons
(.card-actions) are only visually hidden via opacity/transform so they remain
keyboard-focusable; update the CSS to make the hidden state non-interactive
(e.g., add pointer-events: none and visibility: hidden or aria-hidden when not
visible) and ensure they become interactive when the card is hovered or
keyboard-focused by adding a .card:focus-within .card-actions rule matching
.card:hover .card-actions (and keep the existing .mode-cards .card-actions
behavior) so focus reveals the controls as well as hover.
web-ui/partials/index/panel-config-claude.html (1)

54-85: ⚠️ Potential issue | 🟠 Major

Claude config selection is still mouse-only.

The selectable card only handles @click, so keyboard users cannot apply a config from this list. The icon-only edit/delete buttons also still rely on title, which is not a reliable accessible name. Please add focus + Enter/Space support on the card and aria-labels on the icon buttons.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-claude.html` around lines 54 - 85, The
card div in the claudeConfigs v-for only handles mouse clicks and lacks keyboard
support and some icon buttons lack accessible names; make the card focusable by
adding tabindex="0" and wire Enter and Space key handlers (e.g.,
`@keydown.enter.prevent`="applyClaudeConfig(name)" and
`@keydown.space.prevent`="applyClaudeConfig(name)") so applyClaudeConfig(name) can
be triggered via keyboard, and add explicit aria-label attributes to the
icon-only buttons (e.g., the edit button calling openEditConfigModal(name) and
the delete button calling deleteClaudeConfig(name)) such as aria-label="Edit
{{name}}" and aria-label="Delete {{name}}"; keep the existing share button
aria-label and loading class intact.
web-ui/partials/index/layout-header.html (1)

42-105: ⚠️ Potential issue | 🟠 Major

The new tab navigation needs a real keyboard path.

Only the active item is tabbable, but this template wires the nav with @pointerdown/@click only. Unless arrow/Home/End handling is attached elsewhere, inactive tabs in the top bar and side rail are unreachable from the keyboard. The side rail also shouldn't expose a second ARIA tablist for panels already labelled by the top tabs. Please verify this with keyboard-only navigation before merge.

Also applies to: 140-252

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/layout-header.html` around lines 42 - 105, The top-tabs
buttons only handle pointer/click events and keep all but the active tab
unfocusable, so keyboard users cannot navigate; update the tab widgets (buttons
with ids like tab-config-codex, tab-config-claude, tab-config-openclaw,
tab-sessions, tab-market, tab-settings) to implement full roving-tabindex and
keyboard handling: add a keydown handler (reuse or extend
onConfigTabPointerDown/onConfigTabClick and onMainTabPointerDown/onMainTabClick
or create e.g. onTabKeyDown) that handles ArrowLeft/ArrowRight (wrap), Home/End
(first/last), and Enter/Space (activate) to move focus and set
tabindex/aria-selected accordingly (use isConfigModeNavActive/isMainTabNavActive
to compute active state), and ensure the side rail does not duplicate
role="tablist" for the same panels (remove duplicate aria tablist or switch side
items to aria-controls-only) so there's a single keyboard-accessible tablist per
panel.
web-ui/partials/index/panel-config-codex.html (1)

177-243: ⚠️ Potential issue | 🟠 Major

Provider switching here is still mouse-only.

The selectable card is a clickable div with no focus target or key handler, so keyboard users cannot switch providers from this list. The icon-only action buttons also still depend on title for naming. Please mirror the existing click behavior on Enter/Space and add aria-labels to the icon buttons.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-codex.html` around lines 177 - 243, The
provider cards are not keyboard-accessible and icon buttons lack accessible
names; make the clickable provider container focusable (add tabindex="0") and
handle keydown for Enter/Space to call the same method used by click
(switchProvider(provider.name)), ensuring the existing `@click` logic is mirrored
for keyboard activation; add explicit aria-label attributes to each icon button
(the speed test button that calls runSpeedTest, the share button that calls
copyProviderShareCommand, the edit button that calls openEditModal and uses
shouldShowProviderEdit, and the delete button that calls deleteProvider and uses
shouldShowProviderDelete) instead of relying only on title so screen reader
users get clear names.
cli.js (1)

10042-10049: ⚠️ Potential issue | 🟠 Major

Watcher recovery still dies after the watched root is removed.

Line 10063 closes every plain watcher when listDirectoryTree(rootDir) comes back empty, and Lines 10046-10047 only re-add a watcher when target already exists. After the root directory is deleted, nothing is left watching its parent, so recreating web-ui will never re-arm hot reload.

Also applies to: 10062-10079

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10042 - 10049, The recovery logic currently closes plain
watchers when listDirectoryTree(rootDir) returns empty and only re-adds watchers
if target exists, so if the watched root is deleted no parent watcher remains to
detect recreation; update the watcher error/recovery flow (the
watcher.on('error') handler, closeWatcher, syncDirectoryTree, and the code that
checks listDirectoryTree(rootDir)) to keep or re-add a watcher on the parent
directory even when target does not exist: instead of removing all plain
watchers when the tree is empty, ensure you register a watcher on the parent
(call addWatcher(parentPath, recursive=false, isDirectory=true) or similar) so
the parent can detect recreation of the root, and change the condition that
currently only calls addWatcher(target, ...) when fs.existsSync(target) to
instead add a parent watcher when target is missing so hot-reload can re-arm
after deletion.
web-ui/logic.runtime.mjs (1)

4-5: ⚠️ Potential issue | 🟡 Minor

Clamp non-finite latencies to 0ms.

typeof === 'number' still lets NaN and Infinity through, so this can render NaNms or Infinityms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.runtime.mjs` around lines 4 - 5, The current ms computation lets
NaN/Infinity through because it only checks typeof number; change the check to
use Number.isFinite(result.durationMs) when computing const ms (or otherwise
coerce with isFinite) and fall back to 0 if not finite, e.g. compute ms =
Number.isFinite(result.durationMs) ? result.durationMs : 0 so the `${ms}ms`
string never becomes `NaNms` or `Infinityms`.
web-ui/modules/app.methods.agents.mjs (2)

64-73: ⚠️ Potential issue | 🟠 Major

Validate workspace filenames before both workspace file API calls.

These paths only trim the name before calling get-openclaw-workspace-file / apply-openclaw-workspace-file, so values like ../foo.md, /tmp/x.md, or notes.txt can still reach the backend instead of being rejected up front with the existing “仅支持 OpenClaw Workspace 内的 .md 文件” error.

Also applies to: 406-410

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.agents.mjs` around lines 64 - 73, Validate the
workspace filename up front in both openOpenclawWorkspaceEditor and the handler
that calls apply-openclaw-workspace-file: after trimming, check the value
against the allowed pattern (must end with ".md", must not contain path
separators like "/" or "\" or path traversal segments like "..", and must not
start with a slash) and show the existing error message "仅支持 OpenClaw Workspace
内的 `.md` 文件" and return early if validation fails; ensure the same validation
logic is applied before calling api('get-openclaw-workspace-file', { fileName })
and before calling api('apply-openclaw-workspace-file', { fileName, ... }).

154-168: ⚠️ Potential issue | 🟠 Major

Keep the unload warning active while a save is running.

handleBeforeUnload() relies on hasPendingAgentsDraft(), and this helper returns false as soon as agentsSaving flips on. Closing or reloading during the write can then bypass the warning and drop the save.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.agents.mjs` around lines 154 - 168, The unload
warning is disabled while a save is running because hasPendingAgentsDraft()
short-circuits on agentsSaving; update hasPendingAgentsDraft() so a running save
still counts as a pending draft: remove agentsSaving from the early-return
condition (keep showAgentsModal and agentsLoading checks) or explicitly treat
agentsSaving as a pending state (e.g. return true if this.agentsSaving). This
ensures handleBeforeUnload() will trigger the unload warning during saves; look
for the hasPendingAgentsDraft and handleBeforeUnload functions to apply the
change.
web-ui/modules/app.methods.claude-config.mjs (1)

21-30: ⚠️ Potential issue | 🟠 Major

Don't short-circuit external-credential configs before applyClaudeConfig().

Both guards only check apiKey, so profiles backed by externalCredentialType can save model edits or click “保存并应用” without ever reaching the existing external-auth path in applyClaudeConfig(name). Only block when neither field is present.

Also applies to: 69-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.claude-config.mjs` around lines 21 - 30, The
current guard in the block that sets this.currentClaudeModel and updates
this.claudeConfigs checks only for this.claudeConfigs[name].apiKey and
short-circuits before applyClaudeConfig(name), which prevents profiles using
externalCredentialType from reaching the external-auth path; change the
condition so it only shows the "请先配置 API Key" error and returns when neither
apiKey nor externalCredentialType is present (i.e., require at least one
credential), and make the identical fix for the other similar guard (the block
that updates claudeConfigs later in the file); update logic around
this.claudeConfigs[name], mergeClaudeConfig, saveClaudeConfigs,
updateClaudeModelsCurrent, and applyClaudeConfig to reflect this new check.
web-ui/partials/index/panel-sessions.html (1)

283-285: ⚠️ Potential issue | 🟡 Minor

Don't show standalone-route errors in the normal sessions pane.

This branch only renders in browse mode, so reading sessionStandaloneError here can surface a stale standalone failure instead of the normal “请先在左侧选择一个会话” empty state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-sessions.html` around lines 283 - 285, The
sessions pane should not render sessionStandaloneError here: inside the v-else
block that renders the .session-preview-empty fallback, remove the span that
reads sessionStandaloneError (the "{{ sessionStandaloneError }}" within the
v-else branch) so the pane always shows the generic "请先在左侧选择一个会话" message; if
you still need to show standalone-route errors, render sessionStandaloneError
only inside the standalone/browse-specific component or guard it with an
explicit standalone check (e.g., an existing or new
isStandaloneRoute/isBrowseMode flag) rather than reading sessionStandaloneError
from this normal sessions pane.
web-ui/logic.agents-diff.mjs (1)

27-30: ⚠️ Potential issue | 🟠 Major

Drop the synthetic EOF line after splitting.

normalized.split('\n') makes '\n' become ['', ''], so newline-only content and any newline-terminated file pick up a phantom extra line. That pushes the preview and oldLineCount / newLineCount off by one.

Minimal fix
 function splitDiffLines(input) {
     const normalized = normalizeDiffText(input);
     if (!normalized) return [];
-    return normalized.split('\n');
+    const lines = normalized.split('\n');
+    if (lines[lines.length - 1] === '') {
+        lines.pop();
+    }
+    return lines;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.agents-diff.mjs` around lines 27 - 30, splitDiffLines currently
returns an extra synthetic EOF empty entry because normalized.split('\n')
produces a trailing '' for newline-terminated input; update splitDiffLines to
remove that phantom last element (e.g., after splitting, if the last array
element === '' then pop it) so previews and oldLineCount/newLineCount computed
from splitDiffLines (function splitDiffLines) are not off by one — ensure this
handles cases like '\n' (which would otherwise become ['','']).
web-ui/modules/app.methods.startup-claude.mjs (1)

97-150: ⚠️ Potential issue | 🟠 Major

Stale failing model requests still clobber the latest selection.

The success path checks isLatestRequest(), but the catch blocks on Lines 139-145 and Lines 363-369 always mutate shared model state and show an error. If an older request fails after the user has already switched provider or Claude config, it can still replace the current list with 'error' and surface a spurious toast.

Also applies to: 295-374

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.startup-claude.mjs` around lines 97 - 150, The
catch blocks in loadModelsForProvider are mutating shared state and showing an
error even for stale/older requests; before mutating
this.models/this.modelsSource/this.modelsHasCurrent or calling this.showMessage
in the catch handlers, call the existing isLatestRequest() (the closure using
codexModelsRequestSeq and currentProvider) and if it returns false simply
return/exit the handler so only the latest request updates state; apply the same
guard to the other identical catch block elsewhere that handles model loading
failures.
web-ui/modules/app.methods.codex-config.mjs (1)

76-90: ⚠️ Potential issue | 🟠 Major

Failed provider loads still leave currentProvider switched.

Line 78 updates currentProvider before loadModelsForProvider(name) finishes. When that load ends in modelsSource === 'error', Line 80 returns without re-applying config, so the UI now points at the new provider while the backend is still on the old one and currentModel may still belong to the previous provider.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.codex-config.mjs` around lines 76 - 90, The method
performProviderSwitch sets this.currentProvider before loadModelsForProvider
finishes, which leaves the UI pointing at the new provider when load fails;
change the flow so you either (A) call await this.loadModelsForProvider(name)
first and only assign this.currentProvider = name and update
this.currentModel/this.modelsHasCurrent if modelsSource !== 'error', or (B) if
you must assign early, capture the previousProvider, await
loadModelsForProvider(name) and on modelsSource === 'error' revert
this.currentProvider back to previousProvider and restore
this.currentModel/this.modelsHasCurrent before returning; also ensure
getProviderConfigModeMeta(this.configMode) -> applyCodexConfigDirect still runs
only after a successful provider switch (use await this.waitForCodexApplyIdle()
then await this.applyCodexConfigDirect({ silent: true }) when the switch
succeeded).
web-ui/modules/app.methods.runtime.mjs (1)

74-113: ⚠️ Potential issue | 🟡 Minor

Previous progress-reset timers can still zero out a new backup.

Lines 110-112 and Lines 152-154 schedule delayed resets, but nothing cancels an older reset when a new download starts. Starting another backup inside that 800ms window lets the previous timeout clear the new progress bar mid-download.

Also applies to: 116-155

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.runtime.mjs` around lines 74 - 113, The delayed
reset timeout from previous downloads can still fire and zero out
claudeDownloadProgress during a new download; fix downloadClaudeDirectory by
tracking and clearing any existing reset timeout before starting a new download
and before scheduling a new setTimeout: add/use a field (e.g.,
claudeDownloadResetTimer) and call clearTimeout(this.claudeDownloadResetTimer)
at the start of downloadClaudeDirectory and in finally before assigning a new
setTimeout, and ensure you also clear this reset timer when aborting early on
error/return so the old timeout cannot race and reset claudeDownloadProgress
mid-download.
web-ui/modules/app.methods.navigation.mjs (1)

150-189: ⚠️ Potential issue | 🟡 Minor

Touch cancellations still leave transient nav state behind.

Lines 157-165 and Lines 177-185 apply nav-intent-* classes and session-panel-fast-hidden before the touch interaction is known to commit, then return without any cleanup path. If the gesture is canceled or becomes a scroll, the sessions panel can stay hidden and the active tab can remain visually inactive until some later navigation clears it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.navigation.mjs` around lines 150 - 189, The touch
path applies transient nav state then returns without cleanup in
onMainTabPointerDown and onConfigTabPointerDown; detect pointerType early and
bail before mutating state (i.e., move the pointerType detection/if (pointerType
=== 'touch') return check to before calling setMainTabSwitchIntent,
applyImmediateNavIntent, and setSessionPanelFastHidden), or alternatively add
explicit undo calls when pointerType === 'touch' (e.g., revert the
intent/session-panel changes) so recordPointerNavCommit/switchMainTab and
switchConfigMode only run for committed (non-touch) interactions.
web-ui/styles/layout-shell.css (1)

154-156: ⚠️ Potential issue | 🟠 Major

This still hides every .top-tabs, not just the shell layout.

The selector on Lines 154-156 is global and !important, so loading this stylesheet removes legacy or standalone tab bars too. Any screen that still depends on .top-tabs loses its only panel switcher.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/layout-shell.css` around lines 154 - 156, The rule currently
hides every .top-tabs globally; restrict it to only the shell layout by changing
the selector to a more specific scope (e.g., .layout-shell .top-tabs or
.shell-layout .top-tabs) and remove the blanket !important so it won’t override
other screens—update the CSS rule that currently reads ".top-tabs { display:
none !important; }" to a scoped selector without !important so legacy/standalone
tab bars remain functional.
🧹 Nitpick comments (1)
web-ui/styles/navigation-panels.css (1)

195-205: Inconsistent color variable usage for tone variants.

.tone-ok and .tone-error use CSS variables (--color-success, --color-error), but .tone-warn hardcodes #8d5b31. Consider defining a --color-warn variable for consistent theming.

♻️ Suggested consistency fix
 .inspector-kv .value.tone-warn {
-    color: `#8d5b31`;
+    color: var(--color-warn, `#8d5b31`);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/navigation-panels.css` around lines 195 - 205, Replace the
hardcoded warning color with a CSS variable for consistent theming: define a new
--color-warn (e.g., in :root or your theme file) and update the .inspector-kv
.value.tone-warn rule to use var(--color-warn); keep .inspector-kv
.value.tone-ok and .tone-error as they are to maintain consistency with
--color-success and --color-error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 28-29: The env value for WEB_UI_PARITY_BASE_REF is using the wrong
git parent reference ('HEAD^2^'); update the conditional to use 'HEAD^1' instead
so that for pull_request events WEB_UI_PARITY_BASE_REF resolves to the PR base
branch (i.e., change the right-hand string in the expression assigning
WEB_UI_PARITY_BASE_REF from 'HEAD^2^' to 'HEAD^1').

In `@cli.js`:
- Around line 10715-10717: The bundled reader and fallback HTML reads
(dynamicAsset.reader(filePath) and readBundledWebUiHtml(htmlPath)) are
synchronous and unguarded—wrap each call in a try/catch so any thrown error is
caught, log the error, and respond with a 500 (e.g., res.writeHead(500) and
res.end with an error message) instead of letting the exception escape the
request handler; apply this same pattern to the other occurrences noted (the
similar read calls around dynamicAsset.reader and readBundledWebUiHtml in the
file).
- Around line 10714-10718: The handler treats only requestPath === '/web-ui'
specially so a visit to '/web-ui/' falls into the asset branch and yields a 404;
update the conditional around requestPath === '/web-ui' (and/or the subsequent
branch that checks requestPath.startsWith('/web-ui/')) so that '/web-ui/' is
treated the same as '/web-ui' — e.g., treat requestPath === '/web-ui' ||
requestPath === '/web-ui/' as the HTML response case or, alternatively, when
computing relativePath in the '/web-ui/' branch, handle the empty string by
serving the bundled web UI HTML (the readBundledWebUiHtml call) instead of
returning 404; adjust the logic in the blocks that reference requestPath and
relativePath accordingly.
- Around line 65-70: The live-reload/restart detection currently ignores edits
to the new web-ui helper module (the functions readBundledWebUiCss,
readBundledWebUiHtml, readExecutableBundledJavaScriptModule,
readExecutableBundledWebUiScript), so changes to source-bundle.cjs don’t trigger
reloads; update the restart/watch logic that filters file extensions (the array
or predicate that currently excludes ".cjs") to include ".cjs" (and any
equivalent bundled helper extensions used) so edits to source-bundle.cjs are
picked up by the restart mechanism, and update any duplicate filter/check sites
to match.

In `@tests/unit/config-tabs-ui.test.mjs`:
- Around line 157-159: The test currently asserts that modalsBasic does not
contain any type="password" inputs which prevents testing masked secret fields;
update tests/unit/config-tabs-ui.test.mjs to stop asserting absence of password
inputs (remove or change the line referencing assert.doesNotMatch(modalsBasic,
/type="password"/)) and instead verify the intended masked behavior: assert that
the config dialog includes password inputs (match /type="password"/ on
modalsBasic) and add a focused test for the reveal/copy flow that simulates the
reveal action and asserts the input type toggles to text. Ensure you keep
references to modalsBasic and the existing reveal handler/button selectors so
the new assertions target the same UI fragment.

In `@tests/unit/web-ui-behavior-parity.test.mjs`:
- Around line 326-335: The allowlist branch only triggers for exact 'HEAD' but
should also include HEAD-relative refs; update the conditional around
parityBaseline.ref to treat HEAD-relative baselines the same (e.g., change the
if from parityBaseline.ref === 'HEAD' to parityBaseline.ref === 'HEAD' ||
parityBaseline.ref.startsWith('HEAD')), so the code that computes
allowedExtraCurrentKeys/allowedMissingCurrentKeys is used for refs like "HEAD~1"
as well; adjust the test branch using parityBaseline.ref,
allowedExtraCurrentKeys, allowedMissingCurrentKeys, extraCurrentKeys, and
missingCurrentKeys accordingly.

In `@web-ui/modules/api.mjs`:
- Around line 15-18: The api function unconditionally calls res.json() and will
throw on non-JSON responses; update api(action, params) to mirror apiWithMeta's
behavior by checking response.ok and safely attempting JSON parse inside a
try/catch (or using res.text fallback) so non-JSON/HTML error bodies don't
throw; use the same error/metadata shape as apiWithMeta and ensure postApi, res,
and the apiWithMeta pattern are referenced when implementing the change so
callers receive consistent, predictable results instead of an unhandled JSON
parse error.

In `@web-ui/modules/app.methods.openclaw-core.mjs`:
- Around line 495-504: The function parseOptionalNumber treats numeric 0 as
empty because it uses String(value || ''), so replace that expression with a
check that only treats null/undefined as empty (e.g., use value == null or value
=== null || value === undefined) and otherwise convert the value to string;
update parseOptionalNumber to compute text using (value == null ? '' :
String(value).trim()) so parseOptionalNumber(0, ...) returns 0 instead of null.

In `@web-ui/modules/app.methods.openclaw-editing.mjs`:
- Around line 327-333: The saveOpenclawConfigs() function currently swallows
localStorage errors and returns normally, causing callers to treat the save as
successful; change it to surface failures: either rethrow the caught error after
logging the message or return a boolean (e.g. false on failure, true on success)
from saveOpenclawConfigs(), and update callers that close the modal or show
success to check the return value (or catch the error) and abort closing/showing
success when persistence failed; reference the saveOpenclawConfigs() method and
any call sites that trigger modal close or success to ensure they handle the
failure path.

In `@web-ui/modules/app.methods.openclaw-persist.mjs`:
- Around line 8-32: The modal openers (openOpenclawAddModal and
openOpenclawEditModal) fire-and-forget loadOpenclawConfigFromFile which can
asynchronously overwrite shared editor state (openclawEditing,
openclawConfigPath, openclawConfigExists, openclawLineEnding) with stale
responses; fix by making loadOpenclawConfigFromFile cancellable or sequencing
responses: add a per-load token/sequence id on the component (e.g.,
this._openclawLoadId++), capture the id before awaiting
loadOpenclawConfigFromFile, and before writing into shared state check the token
still matches (or verify the modal identity like this.openclawEditing.name /
this.showOpenclawConfigModal hasn't changed); only apply the load result when
the token matches, and do the same for both add and edit flows to prevent slow
responses from clobbering the current draft.

In `@web-ui/modules/app.methods.session-actions.mjs`:
- Around line 390-399: The mutation success paths (in cloneSession and
deleteSession) currently await UI refresh/selection inside the same try that
performs the server mutation, so any rejection from
loadSessions()/selectSession() causes the catch to report the mutation as
failed; change the flow to separate concerns by moving loadSessions() and
selectSession() into their own try/catch (or run them after the mutation try
without rethrowing) so that errors during UI refresh do not trigger the mutation
failure behavior—update the blocks around cloneSession / deleteSession (and
references to removeSessionFromCurrentList, loadSessions, selectSession,
showMessage) to log or show a non-blocking UI error on refresh while preserving
the "success" message for the completed mutation.
- Around line 87-94: The current logic builds an invalid URL when both
window.location.origin === 'null' and apiBase is unset; update the code that
computes origin (using window.location.origin and apiBase) to explicitly detect
when neither provides a usable base and in that case do not return a standalone
session URL — e.g., return null/undefined or throw so callers (like
openSessionStandalone()) can fail gracefully; specifically modify the origin
assignment/return path around the origin variable (and usage of
window.location.origin and apiBase) to short-circuit when no valid base exists
instead of interpolating an undefined into `${origin}/session?...`.

In `@web-ui/partials/index/modal-openclaw-config.html`:
- Line 1: The modal backdrop click handler currently calls
closeOpenclawConfigModal even when saves/loads are in progress; modify the
closeOpenclawConfigModal method to early-return when any busy flag is set (check
this.openclawSaving || this.openclawApplying || this.openclawFileLoading) so
backdrop clicks do nothing during those operations, leaving the existing
disabled footer buttons intact.
- Around line 254-258: Make the JSON5 editor textarea read-only while save/apply
operations are in progress by binding a readonly (or disabled) attribute to the
component state: when either openclawSaving or openclawApplying is true, set the
textarea to readonly so users cannot edit openclawEditing.content during the
operation; update the textarea element (the one using
v-model="openclawEditing.content") to conditionally apply the readonly/disabled
attribute based on openclawSaving || openclawApplying.

In `@web-ui/partials/index/panel-market.html`:
- Around line 14-17: Add explicit type="button" attributes to the clickable
<button> elements to prevent them defaulting to type="submit"; specifically
update the buttons that use the `@click` handlers like loadSkillsMarketOverview({
forceRefresh: true, silent: false }) and openSkillsManager (and the other
similar buttons referenced) so each <button ...> becomes <button type="button"
...>. Ensure every button that only triggers a JS action (e.g., those using
skillsMarketLoading, skillsMarketBusy, initError, etc.) is updated to include
type="button".

In `@web-ui/styles/base-theme.css`:
- Line 1: Remove the external Google Fonts `@import` line that pulls Fira Mono,
JetBrains Mono, Source Sans 3 and Space Grotesk and instead rely on the local
font stack or self-hosted font files; either delete the "@import
url('https://fonts.googleapis.com/...')" entry in base-theme.css and ensure the
existing font-family fallbacks are correct, or download and serve the listed
font files from our assets and update the CSS `@font-face` declarations to
reference those local files so the UI does not contact Google at page load.

In `@web-ui/styles/sessions-list.css`:
- Around line 366-370: The selector currently hides the right-hand preview
header by using `.session-preview-title` and `.session-preview-meta` with
`display: none !important`; update the CSS so only the list snippet is
hidden—remove `.session-preview-title` and `.session-preview-meta` from the rule
and keep `display: none !important` only for
`.session-item-sub.session-item-snippet`; ensure any global rules don't still
target `.session-preview-title` or `.session-preview-meta` so the preview header
and metadata in `panel-sessions.html` remain visible.

---

Duplicate comments:
In `@cli.js`:
- Around line 10042-10049: The recovery logic currently closes plain watchers
when listDirectoryTree(rootDir) returns empty and only re-adds watchers if
target exists, so if the watched root is deleted no parent watcher remains to
detect recreation; update the watcher error/recovery flow (the
watcher.on('error') handler, closeWatcher, syncDirectoryTree, and the code that
checks listDirectoryTree(rootDir)) to keep or re-add a watcher on the parent
directory even when target does not exist: instead of removing all plain
watchers when the tree is empty, ensure you register a watcher on the parent
(call addWatcher(parentPath, recursive=false, isDirectory=true) or similar) so
the parent can detect recreation of the root, and change the condition that
currently only calls addWatcher(target, ...) when fs.existsSync(target) to
instead add a parent watcher when target is missing so hot-reload can re-arm
after deletion.

In `@tests/unit/run.mjs`:
- Around line 20-32: The CI failure is caused by failing assertions in the newly
imported test modules; open and run the three added test files
(web-ui-source-bundle.test.mjs, provider-switch-regression.test.mjs,
web-ui-behavior-parity.test.mjs), identify the failing assertions, and fix them
by correcting expected values or test setup/mocks (ensure proper async/await,
teardown, and stubs/fixtures are initialized and restored); re-run the unit
tests locally to verify the failures are resolved before re-adding their import
lines to tests/unit/run.mjs.

In `@web-ui/logic.agents-diff.mjs`:
- Around line 27-30: splitDiffLines currently returns an extra synthetic EOF
empty entry because normalized.split('\n') produces a trailing '' for
newline-terminated input; update splitDiffLines to remove that phantom last
element (e.g., after splitting, if the last array element === '' then pop it) so
previews and oldLineCount/newLineCount computed from splitDiffLines (function
splitDiffLines) are not off by one — ensure this handles cases like '\n' (which
would otherwise become ['','']).

In `@web-ui/logic.runtime.mjs`:
- Around line 4-5: The current ms computation lets NaN/Infinity through because
it only checks typeof number; change the check to use
Number.isFinite(result.durationMs) when computing const ms (or otherwise coerce
with isFinite) and fall back to 0 if not finite, e.g. compute ms =
Number.isFinite(result.durationMs) ? result.durationMs : 0 so the `${ms}ms`
string never becomes `NaNms` or `Infinityms`.

In `@web-ui/modules/app.methods.agents.mjs`:
- Around line 64-73: Validate the workspace filename up front in both
openOpenclawWorkspaceEditor and the handler that calls
apply-openclaw-workspace-file: after trimming, check the value against the
allowed pattern (must end with ".md", must not contain path separators like "/"
or "\" or path traversal segments like "..", and must not start with a slash)
and show the existing error message "仅支持 OpenClaw Workspace 内的 `.md` 文件" and
return early if validation fails; ensure the same validation logic is applied
before calling api('get-openclaw-workspace-file', { fileName }) and before
calling api('apply-openclaw-workspace-file', { fileName, ... }).
- Around line 154-168: The unload warning is disabled while a save is running
because hasPendingAgentsDraft() short-circuits on agentsSaving; update
hasPendingAgentsDraft() so a running save still counts as a pending draft:
remove agentsSaving from the early-return condition (keep showAgentsModal and
agentsLoading checks) or explicitly treat agentsSaving as a pending state (e.g.
return true if this.agentsSaving). This ensures handleBeforeUnload() will
trigger the unload warning during saves; look for the hasPendingAgentsDraft and
handleBeforeUnload functions to apply the change.

In `@web-ui/modules/app.methods.claude-config.mjs`:
- Around line 21-30: The current guard in the block that sets
this.currentClaudeModel and updates this.claudeConfigs checks only for
this.claudeConfigs[name].apiKey and short-circuits before
applyClaudeConfig(name), which prevents profiles using externalCredentialType
from reaching the external-auth path; change the condition so it only shows the
"请先配置 API Key" error and returns when neither apiKey nor externalCredentialType
is present (i.e., require at least one credential), and make the identical fix
for the other similar guard (the block that updates claudeConfigs later in the
file); update logic around this.claudeConfigs[name], mergeClaudeConfig,
saveClaudeConfigs, updateClaudeModelsCurrent, and applyClaudeConfig to reflect
this new check.

In `@web-ui/modules/app.methods.codex-config.mjs`:
- Around line 76-90: The method performProviderSwitch sets this.currentProvider
before loadModelsForProvider finishes, which leaves the UI pointing at the new
provider when load fails; change the flow so you either (A) call await
this.loadModelsForProvider(name) first and only assign this.currentProvider =
name and update this.currentModel/this.modelsHasCurrent if modelsSource !==
'error', or (B) if you must assign early, capture the previousProvider, await
loadModelsForProvider(name) and on modelsSource === 'error' revert
this.currentProvider back to previousProvider and restore
this.currentModel/this.modelsHasCurrent before returning; also ensure
getProviderConfigModeMeta(this.configMode) -> applyCodexConfigDirect still runs
only after a successful provider switch (use await this.waitForCodexApplyIdle()
then await this.applyCodexConfigDirect({ silent: true }) when the switch
succeeded).

In `@web-ui/modules/app.methods.install.mjs`:
- Around line 27-34: The normalizeInstallRegistryUrl function currently accepts
scheme-only strings like "http://" because it only checks the prefix; update it
to reject URLs that lack a host by parsing the trimmed value (e.g., new
URL(normalized) inside a try/catch) or by verifying that after removing the
scheme the remaining part contains a hostname (non-empty and not just slashes);
if parsing/hostname check fails return '' and otherwise return the normalized
value with trailing slashes removed. Ensure you keep the existing trailing-slash
trim logic and reference normalizeInstallRegistryUrl for the change.

In `@web-ui/modules/app.methods.navigation.mjs`:
- Around line 150-189: The touch path applies transient nav state then returns
without cleanup in onMainTabPointerDown and onConfigTabPointerDown; detect
pointerType early and bail before mutating state (i.e., move the pointerType
detection/if (pointerType === 'touch') return check to before calling
setMainTabSwitchIntent, applyImmediateNavIntent, and setSessionPanelFastHidden),
or alternatively add explicit undo calls when pointerType === 'touch' (e.g.,
revert the intent/session-panel changes) so recordPointerNavCommit/switchMainTab
and switchConfigMode only run for committed (non-touch) interactions.

In `@web-ui/modules/app.methods.runtime.mjs`:
- Around line 74-113: The delayed reset timeout from previous downloads can
still fire and zero out claudeDownloadProgress during a new download; fix
downloadClaudeDirectory by tracking and clearing any existing reset timeout
before starting a new download and before scheduling a new setTimeout: add/use a
field (e.g., claudeDownloadResetTimer) and call
clearTimeout(this.claudeDownloadResetTimer) at the start of
downloadClaudeDirectory and in finally before assigning a new setTimeout, and
ensure you also clear this reset timer when aborting early on error/return so
the old timeout cannot race and reset claudeDownloadProgress mid-download.

In `@web-ui/modules/app.methods.startup-claude.mjs`:
- Around line 97-150: The catch blocks in loadModelsForProvider are mutating
shared state and showing an error even for stale/older requests; before mutating
this.models/this.modelsSource/this.modelsHasCurrent or calling this.showMessage
in the catch handlers, call the existing isLatestRequest() (the closure using
codexModelsRequestSeq and currentProvider) and if it returns false simply
return/exit the handler so only the latest request updates state; apply the same
guard to the other identical catch block elsewhere that handles model loading
failures.

In `@web-ui/partials/index/layout-header.html`:
- Around line 42-105: The top-tabs buttons only handle pointer/click events and
keep all but the active tab unfocusable, so keyboard users cannot navigate;
update the tab widgets (buttons with ids like tab-config-codex,
tab-config-claude, tab-config-openclaw, tab-sessions, tab-market, tab-settings)
to implement full roving-tabindex and keyboard handling: add a keydown handler
(reuse or extend onConfigTabPointerDown/onConfigTabClick and
onMainTabPointerDown/onMainTabClick or create e.g. onTabKeyDown) that handles
ArrowLeft/ArrowRight (wrap), Home/End (first/last), and Enter/Space (activate)
to move focus and set tabindex/aria-selected accordingly (use
isConfigModeNavActive/isMainTabNavActive to compute active state), and ensure
the side rail does not duplicate role="tablist" for the same panels (remove
duplicate aria tablist or switch side items to aria-controls-only) so there's a
single keyboard-accessible tablist per panel.

In `@web-ui/partials/index/modals-basic.html`:
- Line 16: The API key fields are rendered as plain text (e.g., the input bound
to newProvider.key), exposing secrets; change those inputs to type="password"
for newProvider.key and the other key v-models referenced in the file (the
edit/create provider key inputs at the other occurrences) so the value is masked
during entry, and optionally add a separate show/hide toggle control if
visibility is needed later.

In `@web-ui/partials/index/panel-config-claude.html`:
- Around line 54-85: The card div in the claudeConfigs v-for only handles mouse
clicks and lacks keyboard support and some icon buttons lack accessible names;
make the card focusable by adding tabindex="0" and wire Enter and Space key
handlers (e.g., `@keydown.enter.prevent`="applyClaudeConfig(name)" and
`@keydown.space.prevent`="applyClaudeConfig(name)") so applyClaudeConfig(name) can
be triggered via keyboard, and add explicit aria-label attributes to the
icon-only buttons (e.g., the edit button calling openEditConfigModal(name) and
the delete button calling deleteClaudeConfig(name)) such as aria-label="Edit
{{name}}" and aria-label="Delete {{name}}"; keep the existing share button
aria-label and loading class intact.

In `@web-ui/partials/index/panel-config-codex.html`:
- Around line 177-243: The provider cards are not keyboard-accessible and icon
buttons lack accessible names; make the clickable provider container focusable
(add tabindex="0") and handle keydown for Enter/Space to call the same method
used by click (switchProvider(provider.name)), ensuring the existing `@click`
logic is mirrored for keyboard activation; add explicit aria-label attributes to
each icon button (the speed test button that calls runSpeedTest, the share
button that calls copyProviderShareCommand, the edit button that calls
openEditModal and uses shouldShowProviderEdit, and the delete button that calls
deleteProvider and uses shouldShowProviderDelete) instead of relying only on
title so screen reader users get clear names.

In `@web-ui/partials/index/panel-config-openclaw.html`:
- Around line 47-77: The card list uses a clickable <div> for each config (v-for
over openclawConfigs with currentOpenclawConfig and applyOpenclawConfig) which
is not keyboard-accessible and the icon-only buttons (openOpenclawEditModal,
deleteOpenclawConfig) rely only on title text; make the card keyboard-focusable
by adding tabindex="0" and wiring `@keydown.enter` and `@keydown.space` to call
applyOpenclawConfig(name) (or convert the card wrapper into a semantic <button>
element) and add explicit aria-label attributes to the edit and delete buttons
(used by openOpenclawEditModal and deleteOpenclawConfig) so screen readers
announce their purpose.

In `@web-ui/partials/index/panel-sessions.html`:
- Around line 283-285: The sessions pane should not render
sessionStandaloneError here: inside the v-else block that renders the
.session-preview-empty fallback, remove the span that reads
sessionStandaloneError (the "{{ sessionStandaloneError }}" within the v-else
branch) so the pane always shows the generic "请先在左侧选择一个会话" message; if you still
need to show standalone-route errors, render sessionStandaloneError only inside
the standalone/browse-specific component or guard it with an explicit standalone
check (e.g., an existing or new isStandaloneRoute/isBrowseMode flag) rather than
reading sessionStandaloneError from this normal sessions pane.

In `@web-ui/styles/layout-shell.css`:
- Around line 154-156: The rule currently hides every .top-tabs globally;
restrict it to only the shell layout by changing the selector to a more specific
scope (e.g., .layout-shell .top-tabs or .shell-layout .top-tabs) and remove the
blanket !important so it won’t override other screens—update the CSS rule that
currently reads ".top-tabs { display: none !important; }" to a scoped selector
without !important so legacy/standalone tab bars remain functional.

In `@web-ui/styles/navigation-panels.css`:
- Around line 255-266: The .top-tabs rule currently hardcodes
grid-template-columns: repeat(4, 1fr) which breaks when layout-header.html
contains 6 tabs; change the CSS for .top-tabs to a responsive grid (for example
use grid-template-columns: repeat(auto-fit, minmax(<reasonable-min-width>, 1fr))
or auto-fill with minmax) so the number of columns adapts to content/viewport,
or alternatively match the explicit repeat(...) to the actual tab count in
layout-header.html if a fixed number is required; update the .top-tabs selector
accordingly and verify behavior with 6 tabs in layout-header.html.

In `@web-ui/styles/responsive.css`:
- Around line 216-227: The .session-item-copy rule currently forces a 20x20
touch target which violates the 44px mobile tap-floor; update the CSS for
.session-item-copy to preserve accessibility by keeping min-width: 44px and
min-height: 44px (remove or change the explicit width/height values that set
20px), and if a smaller visual is desired, scale only the inner icon (e.g.
target the child SVG or .icon inside .session-item-copy) to 20px while retaining
the 44px interactive box and existing alignment rules like display, padding and
transform.

In `@web-ui/styles/titles-cards.css`:
- Around line 263-279: Hidden action buttons (.card-actions) are only visually
hidden via opacity/transform so they remain keyboard-focusable; update the CSS
to make the hidden state non-interactive (e.g., add pointer-events: none and
visibility: hidden or aria-hidden when not visible) and ensure they become
interactive when the card is hovered or keyboard-focused by adding a
.card:focus-within .card-actions rule matching .card:hover .card-actions (and
keep the existing .mode-cards .card-actions behavior) so focus reveals the
controls as well as hover.

---

Nitpick comments:
In `@web-ui/styles/navigation-panels.css`:
- Around line 195-205: Replace the hardcoded warning color with a CSS variable
for consistent theming: define a new --color-warn (e.g., in :root or your theme
file) and update the .inspector-kv .value.tone-warn rule to use
var(--color-warn); keep .inspector-kv .value.tone-ok and .tone-error as they are
to maintain consistency with --color-success and --color-error.
🪄 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: f22406ce-32a6-4bac-9b6e-febb311897e0

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and a7eb34d.

📒 Files selected for processing (77)
  • .github/workflows/ci.yml
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-logic.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css

coderabbitai[bot]
coderabbitai bot previously requested changes Apr 4, 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: 7

♻️ Duplicate comments (33)
web-ui/modules/api.mjs (1)

15-18: ⚠️ Potential issue | 🟡 Minor

Make api() parse failures explicit.

HTML/plain-text responses still bubble out of Line 17 as a bare res.json() parse error. If this helper is intentionally JSON-only, please wrap and rethrow with request/status context so callers do not just see an opaque parse exception.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/api.mjs` around lines 15 - 18, The api() helper currently
returns await res.json() directly so JSON parse errors surface as opaque
exceptions; update api(action, params) to explicitly handle non-JSON or parse
failures by checking response.ok and Content-Type (via
res.headers.get('content-type')) and wrapping any parse error or non-JSON
response in a new Error that includes the action, HTTP status and statusText,
and response body or content-type for context; use postApi(action, params)
result (res) and try/catch around res.json() to rethrow a descriptive error so
callers get request/status context instead of a raw parse exception.
web-ui/styles/navigation-panels.css (1)

255-264: ⚠️ Potential issue | 🟡 Minor

Verify the hard-coded four-column tab grid still matches the header.

Line 263 still fixes .top-tabs at four columns. If layout-header.html still renders six top-level tabs, the last two will wrap into a second row.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/navigation-panels.css` around lines 255 - 264, The .top-tabs
rule hardcodes grid-template-columns: repeat(4, 1fr) which can mismatch the
actual header tabs; update the .top-tabs styling to layout responsively instead
of fixing 4 columns—either use grid-template-columns: repeat(auto-fit, minmax(0,
1fr)) to let columns adapt to the number of top-level tabs or switch to a CSS
variable (e.g., --top-tabs-count) and use grid-template-columns:
repeat(var(--top-tabs-count, 4), 1fr) with layout-header.html setting that
variable to the actual tab count; modify the .top-tabs rule accordingly and, if
using the variable approach, ensure layout-header.html exposes the correct
--top-tabs-count value.
cli.js (4)

10714-10718: ⚠️ Potential issue | 🟡 Minor

Handle /web-ui/ the same as /web-ui.

A normal trailing-slash visit falls into the asset branch, relativePath becomes '', and the request 404s.

Minimal fix
-        } else if (requestPath === '/web-ui') {
+        } else if (requestPath === '/web-ui' || requestPath === '/web-ui/') {

Also applies to: 10726-10737

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10714 - 10718, The handler treats "/web-ui" and
"/web-ui/" differently causing a 404 for the trailing-slash URL; update the
routing so that requestPath === '/web-ui/' is handled the same as requestPath
=== '/web-ui' by calling readBundledWebUiHtml(htmlPath) and returning the HTML,
and in the asset branch (the startsWith('/web-ui/') logic) ensure when
relativePath is '' (meaning the path was exactly '/web-ui/') you fall back to
serving the bundled HTML (or normalize the path to '/web-ui') so that empty
relativePath does not 404; locate and adjust the checks around requestPath,
readBundledWebUiHtml, and the '/web-ui/' startsWith branch to implement this
behavior.

65-70: ⚠️ Potential issue | 🟡 Minor

Include the new .cjs helper in restart detection.

web-ui/source-bundle.cjs is now part of the live frontend path, but the watcher still ignores .cjs, so edits there keep serving stale bundled output until this process is restarted.

Minimal fix
-                if (!(/\.(html|js|mjs|css)$/.test(lower))) return;
+                if (!(/\.(html|js|mjs|cjs|css)$/.test(lower))) return;

Also applies to: 10038-10039

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 65 - 70, The watcher/restart logic currently ignores
edits to .cjs files (we just required web-ui/source-bundle.cjs via
readBundledWebUiCss/readBundledWebUiHtml/readExecutableBundledJavaScriptModule/readExecutableBundledWebUiScript),
so changes to that bundled helper don't trigger a restart; update the restart
detection to include the ".cjs" extension (add ".cjs" to whatever extension
whitelist/ignored-check in the restartOnChange or
watchedExtensions/ignoredExtensions logic) so edits to web-ui/source-bundle.cjs
cause the process to reload and serve fresh bundles.

10714-10717: ⚠️ Potential issue | 🟠 Major

Catch bundled reader failures and return 500 instead of letting them escape the request handler.

These reads are synchronous and unguarded. If a bundled reader throws during a save window or transient parse/read failure, the exception escapes the http.createServer() callback and can take down the whole server instead of failing just that request.

Run this to verify how an uncaught synchronous exception behaves in a Node request handler:

#!/bin/bash
set -euo pipefail

sed -n '10714,10730p;10810,10812p' cli.js

node <<'NODE'
const http = require('http');

process.on('uncaughtException', (err) => {
  console.log('UNCAUGHT', err.message);
  process.exit(0);
});

const server = http.createServer((_req, _res) => {
  throw new Error('bundled reader failed');
});

server.emit('request', {}, {
  writeHead() {},
  end() {}
});

setTimeout(() => {
  console.log('no uncaught exception observed');
  process.exit(1);
}, 100);
NODE

Expected: UNCAUGHT bundled reader failed, which is why these branches need local try/catch and a 500 response.

Minimal hardening
-        } else if (requestPath === '/web-ui') {
-            const html = readBundledWebUiHtml(htmlPath);
-            res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
-            res.end(html);
+        } else if (requestPath === '/web-ui') {
+            try {
+                const html = readBundledWebUiHtml(htmlPath);
+                res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+                res.end(html);
+            } catch (_) {
+                res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
+                res.end('Internal Server Error');
+            }

Apply the same guard around dynamicAsset.reader(filePath) and the fallback readBundledWebUiHtml(htmlPath) path.

Also applies to: 10728-10730, 10810-10812

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10714 - 10717, The /web-ui and related branches call
synchronous readers (readBundledWebUiHtml and dynamicAsset.reader(filePath))
without protection, so wrap each reader call in a try/catch inside the
http.createServer handler; on catch call res.writeHead(500, { 'Content-Type':
'text/plain; charset=utf-8' }) and res.end with a short error message, and
optionally log the error, ensuring the exception cannot escape the request
handler and take down the process.

10042-10049: ⚠️ Potential issue | 🟠 Major

Root-directory recreation can still disable restart watching until this process is restarted.

When a watched root disappears, the recursive path drops the watcher if target no longer exists, and the plain-tree fallback closes every nested watcher when listDirectoryTree(rootDir) returns empty. Nothing is left watching the parent for the root to come back.

Run this to confirm the current control flow and the underlying fs.watch() behavior:

#!/bin/bash
set -euo pipefail

sed -n '10042,10079p' cli.js

node <<'NODE'
const fs = require('fs');
const os = require('os');
const path = require('path');

const parent = fs.mkdtempSync(path.join(os.tmpdir(), 'codexmate-watch-root-'));
const root = path.join(parent, 'web-ui');
fs.mkdirSync(root);

const events = [];
const watcher = fs.watch(root, { recursive: false }, (eventType, filename) => {
  events.push({ eventType, filename: String(filename || '') });
});
watcher.on('error', (err) => {
  events.push({ error: err.code || err.message });
});

setTimeout(() => fs.rmSync(root, { recursive: true, force: true }), 100);
setTimeout(() => fs.mkdirSync(root), 300);
setTimeout(() => fs.writeFileSync(path.join(root, 'after-recreate.js'), '1'), 500);
setTimeout(() => {
  watcher.close();
  console.log(JSON.stringify(events, null, 2));
}, 1000);
NODE

Expected: the excerpt shows no parent/sentinel watcher is installed when the root disappears, and on platforms where deleting a watched directory invalidates the watch, the reproduction reports only the delete/error side and misses the write after recreation.

Also applies to: 10062-10079

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli.js` around lines 10042 - 10049, When a watched root disappears the
current error handler calling closeWatcher(watchKey) and
syncDirectoryTree(target) can remove all watches and leave no parent/sentinel to
detect recreation; update the watcher error/path-missing handling in the
watcher.on('error') and the plain-tree fallback that calls
listDirectoryTree(rootDir) so that instead of closing parent watchers you either
(a) install or retain a parent/sentinel watcher on the parent directory (use
addWatcher(parentPath, false, true) or similar) to detect when target reappears,
or (b) schedule a retry/poll to re-add the original watcher (call
addWatcher(target, recursive, isDirectory) after a short delay) rather than
dropping all nested watchers; ensure the logic around closeWatcher(watchKey),
syncDirectoryTree(target), addWatcher(target, …), and listDirectoryTree(...)
preserves a watcher on the parent so recreate events are observed.
web-ui/modules/app.methods.openclaw-core.mjs (2)

223-233: ⚠️ Potential issue | 🟠 Major

Legacy fallback entries still disappear once defaults.model.fallbacks exists.

Line 226 picks either defaults.model.fallbacks or legacyAgent.model.fallbacks, never both. If a config carries legacy-only fallback entries alongside the newer defaults block, those entries never reach agentFallbacks, and a structured save strips them.

Minimal fix
-            const fallbackSource = Array.isArray(model.fallbacks)
-                ? model.fallbacks
-                : (legacyAgent.model && typeof legacyAgent.model === 'object' && !Array.isArray(legacyAgent.model) && Array.isArray(legacyAgent.model.fallbacks)
-                    ? legacyAgent.model.fallbacks
-                    : []);
+            const fallbackSource = [
+                ...(Array.isArray(model.fallbacks) ? model.fallbacks : []),
+                ...(
+                    legacyAgent.model
+                    && typeof legacyAgent.model === 'object'
+                    && !Array.isArray(legacyAgent.model)
+                    && Array.isArray(legacyAgent.model.fallbacks)
+                        ? legacyAgent.model.fallbacks
+                        : []
+                )
+            ];
             const fallbackList = fallbackSource
                 .filter(item => typeof item === 'string' && item.trim())
                 .map(item => item.trim());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-core.mjs` around lines 223 - 233, The
code currently picks either model.fallbacks or legacyAgent.model.fallbacks into
fallbackSource, so legacy-only fallback entries are lost when
defaults.model.fallbacks exists; change the logic around legacyAgent,
fallbackSource and fallbackList to merge both sources (if present and arrays) —
e.g., build an array by concatenating model.fallbacks and
legacyAgent.model.fallbacks when they exist, then apply the same filter/map
trimming step to produce fallbackList so entries from both locations are
preserved.

495-497: ⚠️ Potential issue | 🟠 Major

0 still collapses to “empty” here.

Line 496 uses String(value || ''), so numeric 0 becomes '' and this helper returns null instead of 0. That drops explicit zero values during structured config round-trips.

Minimal fix
-            const text = typeof value === 'string' ? value.trim() : String(value || '').trim();
+            const text = typeof value === 'string'
+                ? value.trim()
+                : (value == null ? '' : String(value).trim());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-core.mjs` around lines 495 - 497, The
helper parseOptionalNumber treats numeric 0 as empty because it uses
String(value || ''), so update the creation of text to only coerce
null/undefined to '' (e.g., use a null/undefined check like value == null ? '' :
value) instead of value || ''; keep the original branch for strings and then
trim; this preserves explicit zeros (0) so parseOptionalNumber will return 0
instead of null.
web-ui/styles/sessions-toolbar-trash.css (1)

17-19: ⚠️ Potential issue | 🟠 Major

Span the full row instead of hard-coding two tracks.

Line 18 still uses grid-column: span 2. Once the responsive layout collapses .session-toolbar to one column, that span creates an implicit second track and can force horizontal overflow.

Minimal fix
 .session-toolbar-grow {
-    grid-column: span 2;
+    grid-column: 1 / -1;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/sessions-toolbar-trash.css` around lines 17 - 19, The
.session-toolbar-grow rule currently forces grid-column: span 2 which creates an
implicit second track on narrow layouts; update the .session-toolbar-grow
selector to span the entire row instead of hard-coding two tracks (e.g., use a
full-row descriptor like grid-column: 1 / -1 or an equivalent full-span) so it
collapses correctly when .session-toolbar becomes single-column and avoids
horizontal overflow.
web-ui/modules/app.methods.providers.mjs (1)

146-153: ⚠️ Potential issue | 🟠 Major

Keep the edit state until the provider update actually succeeds.

Line 169 closes the modal before the request returns, so any res.error or thrown request drops the user's draft. Line 171 also still submits key even when the edit form left it untouched; if the current update-provider handler treats '' as a value, a URL-only edit will still clear the saved key.

Minimal fix
         async updateProvider() {
             if (this.editingProvider.readOnly || this.editingProvider.nonEditable) {
                 this.showMessage('该 provider 为保留项,不可编辑', 'error');
                 this.closeEditModal();
                 return;
             }
             const url = typeof this.editingProvider.url === 'string' ? this.editingProvider.url.trim() : '';
             if (!url) {
                 return this.showMessage('URL 必填', 'error');
             }

             const name = this.editingProvider.name;
-            const key = this.editingProvider.key || '';
-            this.closeEditModal();
+            const key = typeof this.editingProvider.key === 'string' ? this.editingProvider.key : '';
             try {
-                const res = await api('update-provider', { name, url, key });
+                const params = { name, url };
+                if (key !== '') {
+                    params.key = key;
+                }
+                const res = await api('update-provider', params);
                 if (res.error) {
                     this.showMessage(res.error, 'error');
                     return;
                 }
+                this.closeEditModal();
                 this.showMessage('操作成功', 'success');
                 await this.loadAll();
             } catch (e) {
                 this.showMessage('更新失败', 'error');
             }

Verify the current server-side handling of empty keys before merging a client-only fix:

#!/bin/bash
set -euo pipefail

echo "=== client edit flow ==="
sed -n '141,176p' web-ui/modules/app.methods.providers.mjs

echo
echo "=== update-provider handling ==="
rg -n -C4 --glob '!tests/**' "update-provider|updateProviderInConfig|cmdUpdate|params\\.key|apiKey !== undefined|preferred_auth_method" .

Expected result: the server path should ignore params.key === '' (or the client should omit key when untouched). Any unconditional write on empty key means the credential-wipe bug is still present.

Also applies to: 167-171

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.providers.mjs` around lines 146 - 153, The edit
flow currently closes the modal and submits a possibly-empty key immediately,
which can drop the user's draft or unintentionally clear stored credentials;
change the logic around this.editingProvider / this.showEditModal so the modal
remains open until the update request completes (on success only set
showEditModal = true->false), and when building the payload for the
update-provider request omit the key field if the user did not change it (e.g.
only include key when non-empty or when an explicit key-edit flag is set) so
update-provider handlers do not receive key: ''. Locate the code that sets
this.editingProvider and the submit/update method that calls the update-provider
action and adjust it to wait for the request result before closing the modal and
to conditionally exclude key from the params.
web-ui/modules/app.methods.session-actions.mjs (2)

87-94: ⚠️ Potential issue | 🟡 Minor

Coerce the standalone URL base before interpolating it.

When window.location.origin is unusable and apiBase is undefined, Line 94 builds undefined/session?.... Normalizing the fallback to a string avoids generating a broken literal URL.

🛠️ Minimal fix
-            const origin = window.location.origin && window.location.origin !== 'null'
-                ? window.location.origin
-                : apiBase;
+            const origin = window.location.origin && window.location.origin !== 'null'
+                ? window.location.origin
+                : (typeof apiBase === 'string' ? apiBase : '');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-actions.mjs` around lines 87 - 94, The
origin fallback can be undefined causing "undefined/session" URLs; ensure
apiBase is coerced to a string before building the URL. Update the origin
assignment (the origin variable used to build the return URL) to use a safe
string fallback like String(apiBase || '') or an explicit default (e.g., '')
when window.location.origin is not available, leaving the rest of the logic that
sets params (source, sessionId, filePath) and the final template
`${origin}/session?${params.toString()}` unchanged.

390-399: ⚠️ Potential issue | 🟠 Major

Keep refresh failures from masquerading as clone/delete failures.

After Line 390 and Line 427, the server-side mutation has already succeeded. Any later rejection from loadSessions(), selectSession(), or removeSessionFromCurrentList() still falls into the outer catch, so the UI can report a failed clone/delete and encourage a duplicate retry.

🛠️ Minimal fix
                 this.showMessage('操作成功', 'success');
-                await this.loadSessions();
-                if (res.sessionId) {
-                    const matched = this.sessionsList.find(item => item.source === 'codex' && item.sessionId === res.sessionId);
-                    if (matched) {
-                        await this.selectSession(matched);
-                    }
-                }
+                try {
+                    await this.loadSessions();
+                    if (res.sessionId) {
+                        const matched = this.sessionsList.find(item => item.source === 'codex' && item.sessionId === res.sessionId);
+                        if (matched) {
+                            await this.selectSession(matched);
+                        }
+                    }
+                } catch (_) {
+                    this.showMessage('克隆已完成,但列表刷新失败', 'error');
+                }
-                await this.removeSessionFromCurrentList(session);
+                try {
+                    await this.removeSessionFromCurrentList(session);
+                } catch (_) {
+                    this.showMessage('会话已移入回收站,但列表刷新失败', 'error');
+                }

Also applies to: 427-442

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.session-actions.mjs` around lines 390 - 399, The
outer catch is currently treating post-mutation UI refresh errors (from
loadSessions, selectSession, removeSessionFromCurrentList) as if the server
mutation failed; wrap the UI refresh/select/remove calls in their own try/catch
(or add per-call .catch handlers) so that after a successful mutation (where you
call this.showMessage('操作成功','success')) any errors from this.loadSessions(),
this.selectSession(...), or this.removeSessionFromCurrentList(...) are caught
locally, logged (or show a non-blocking warning), and not rethrown to the outer
catch; update the blocks around the successful mutation (the code that calls
this.loadSessions(), const matched/find and this.selectSession, and the removal
path that calls this.removeSessionFromCurrentList) to use this local try/catch
behavior.
.github/workflows/ci.yml (1)

28-29: ⚠️ Potential issue | 🟠 Major

Point parity tests at the PR base, not the previous PR commit.

Line 29 uses HEAD^2^, which stops being the base revision once the PR branch has more than one commit. That makes WEB_UI_PARITY_BASE_REF drift with the branch instead of comparing against the pull request base.

🛠️ Minimal fix
-          WEB_UI_PARITY_BASE_REF: ${{ github.event_name == 'pull_request' && 'HEAD^2^' || 'HEAD^' }}
+          WEB_UI_PARITY_BASE_REF: ${{ github.event_name == 'pull_request' && 'HEAD^1' || 'HEAD^' }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 28 - 29, The current env assignment
for WEB_UI_PARITY_BASE_REF uses the commit-relative expression `HEAD^2^` which
drifts as more commits are added; update the expression to use the pull request
base commit reference so parity always compares to the PR base. Replace the RHS
with a conditional that uses `github.event.pull_request.base.sha` when
`github.event_name == 'pull_request'` and falls back to `'HEAD^'` otherwise
(e.g. `${{ github.event_name == 'pull_request' &&
github.event.pull_request.base.sha || 'HEAD^' }}`), keeping the environment
variable name `WEB_UI_PARITY_BASE_REF`.
web-ui/styles/responsive.css (1)

216-223: ⚠️ Potential issue | 🟡 Minor

Don't override the 44px tap-target floor here.

This block resets .session-item-copy back to 20x20 after the same media query already raised it to 44px, shrinking the final mobile hit area below the accessibility floor.

Minimal fix
     .session-item-copy {
-        width: 20px;
-        height: 20px;
-        min-width: 20px;
-        min-height: 20px;
         border-radius: 6px;
         padding: 2px;
         display: inline-flex;
         align-items: center;
         justify-content: center;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/responsive.css` around lines 216 - 223, The .session-item-copy
rule is shrinking the tap target back to 20x20 after a media query raised it to
the 44px accessibility floor; update this rule (selector .session-item-copy) to
not override the mobile sizing by removing or changing
width/height/min-width/min-height to ensure they are at least 44px (e.g., remove
the 20px values or set min-width/min-height: 44px) so the final computed touch
target stays >=44x44.
tests/unit/web-ui-behavior-parity.test.mjs (1)

326-335: ⚠️ Potential issue | 🟠 Major

Treat HEAD-relative parity refs like HEAD.

PR CI uses HEAD^2^, so this exact-string check drops into the strict branch and reproduces the current failure at Line 334 even when the drift stays inside the approved allowlist.

Minimal fix
-    if (parityBaseline.ref === 'HEAD') {
+    if (String(parityBaseline.ref || '').startsWith('HEAD')) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/web-ui-behavior-parity.test.mjs` around lines 326 - 335, The
conditional that currently checks parityBaseline.ref === 'HEAD' wrongly treats
HEAD-relative refs (e.g. "HEAD^2^") as non-HEAD, causing the strict-compare
branch to run; update the check in the if that reads parityBaseline.ref ===
'HEAD' to treat any HEAD-relative ref as HEAD (for example use
parityBaseline.ref === 'HEAD' || parityBaseline.ref.startsWith('HEAD') or test
with /^HEAD(\\b|\\^)/) so the allowed-extra/allowed-missing set logic runs for
HEAD and HEAD-derived refs; ensure the symbols parityBaseline.ref and the
surrounding if/else handling of
unexpectedExtraCurrentKeys/unexpectedMissingCurrentKeys are adjusted
accordingly.
web-ui/partials/index/modal-openclaw-config.html (1)

254-258: ⚠️ Potential issue | 🟡 Minor

Make the JSON5 editor read-only during writes.

Users can still type while openclawSaving or openclawApplying is running, which lets the editor show unsaved text that looks like it was included in the just-finished write.

Minimal fix
                     <textarea
                         v-model="openclawEditing.content"
                         class="form-input template-editor"
                         spellcheck="false"
+                        :readonly="openclawSaving || openclawApplying"
                         placeholder="在这里编辑 OpenClaw 配置(JSON5)"></textarea>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-openclaw-config.html` around lines 254 - 258, The
JSON5 textarea bound to openclawEditing.content should be made read-only while a
write is in progress; update the <textarea> element (the template-editor bound
via v-model to openclawEditing.content) to set a reactive readonly/disabled
binding that becomes true when either openclawSaving or openclawApplying is true
so users cannot type during saves/applies and the editor cannot show unsaved
changes that look like they were included in the just-finished write.
web-ui/partials/index/panel-config-claude.html (1)

54-85: ⚠️ Potential issue | 🟠 Major

Restore keyboard and screen-reader access for Claude config cards.

This card is still a mouse-only div, and the edit/delete icon buttons still rely on title, so keyboard users cannot select a config and screen readers do not get reliable names for two actions.

Minimal fix
-                    <div v-for="(config, name) in claudeConfigs" :key="name"
-                         :class="['card', { active: currentClaudeConfig === name }]"
-                         `@click`="applyClaudeConfig(name)">
+                    <div v-for="(config, name) in claudeConfigs" :key="name"
+                         :class="['card', { active: currentClaudeConfig === name }]"
+                         `@click`="applyClaudeConfig(name)"
+                         `@keydown.enter.prevent`="applyClaudeConfig(name)"
+                         `@keydown.space.prevent`="applyClaudeConfig(name)"
+                         tabindex="0"
+                         role="button">
@@
-                                <button class="card-action-btn" `@click`="openEditConfigModal(name)" title="编辑">
+                                <button class="card-action-btn" `@click`="openEditConfigModal(name)" aria-label="编辑">
@@
-                                <button class="card-action-btn delete" `@click`="deleteClaudeConfig(name)" title="删除">
+                                <button class="card-action-btn delete" `@click`="deleteClaudeConfig(name)" aria-label="删除">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-claude.html` around lines 54 - 85, The
config card is only mouse-clickable and icon buttons rely on title attributes,
so add keyboard and screen-reader support by making the card container focusable
and operable: give the outer div that lists each config a tabindex="0" (or
:tabindex="0"), role="button", and handle `@keydown.enter` and `@keydown.space` to
call applyClaudeConfig(name) (same handler as `@click`) and manage focus styling;
also replace reliance on title on the icon buttons (the buttons opening edit,
share, delete) with explicit aria-label attributes (e.g. aria-label="Edit
{{name}}", "Share import command for {{name}}", "Delete {{name}}") and ensure
any disabled state uses aria-disabled when needed and that the button elements
remain real <button> elements so screen readers and keyboard users can activate
openEditConfigModal(name), copyClaudeShareCommand(name) and
deleteClaudeConfig(name).
web-ui/styles/titles-cards.css (1)

263-279: ⚠️ Potential issue | 🟠 Major

Keep hidden card actions out of the keyboard path.

opacity: 0 only hides these buttons visually. They can still receive focus, and the reveal is wired only to :hover, so keyboard users can land on invisible controls.

Minimal fix
 .card-actions {
     display: flex;
     gap: 8px;
     opacity: 0;
+    visibility: hidden;
+    pointer-events: none;
     transform: translateX(4px);
     transition: all var(--transition-normal) var(--ease-spring);
 }
 
-.card:hover .card-actions {
+.card:hover .card-actions,
+.card:focus-within .card-actions {
     opacity: 1;
+    visibility: visible;
+    pointer-events: auto;
     transform: translateX(0);
 }
 
 .mode-cards .card-actions {
     opacity: 1;
+    visibility: visible;
+    pointer-events: auto;
     transform: translateX(0);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/titles-cards.css` around lines 263 - 279, The .card-actions
buttons are only visually hidden (opacity:0) so they remain keyboard-focusable;
update the CSS for .card-actions to remove them from the keyboard/mouse path
when hidden by adding visibility: hidden and pointer-events: none to the default
.card-actions rule, and restore visibility: visible and pointer-events: auto in
the .card:hover .card-actions and .mode-cards .card-actions rules so they become
interactive only when revealed; target the same selectors (.card-actions,
.card:hover .card-actions, .mode-cards .card-actions) so the
transition/appearance behavior is preserved while preventing keyboard focus on
invisible controls.
tests/unit/config-tabs-ui.test.mjs (1)

157-157: ⚠️ Potential issue | 🟠 Major

Don't codify plain-text secret fields as the modal contract.

This assertion makes the suite fail the moment provider/Claude secrets are masked by default, which is the safer baseline already used for secret inputs elsewhere in this PR.

Minimal fix
-    assert.doesNotMatch(modalsBasic, /type="password"/);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/config-tabs-ui.test.mjs` at line 157, The test currently asserts
that modalsBasic does NOT contain a password input via
assert.doesNotMatch(modalsBasic, /type="password"/), which will fail when secret
fields are masked by default; remove or change that assertion so the test no
longer enforces plain-text secret fields—e.g., delete the
assert.doesNotMatch(...) line in tests/unit/config-tabs-ui.test.mjs (referencing
the modalsBasic variable) or replace it with a non-failing check that allows
type="password" to be present.
web-ui/partials/index/modal-config-template-agents.html (3)

1-1: ⚠️ Potential issue | 🟠 Major

Verify both modal close handlers reject in-flight dismissals.

The overlays still call closeConfigTemplateModal / closeAgentsModal directly, while the AGENTS footer already blocks cancellation during agentsSaving || agentsDiffLoading. Please confirm those close handlers also bail out on configTemplateApplying, agentsSaving, and agentsDiffLoading; otherwise a backdrop click can dismiss a modal mid-write.

#!/bin/bash
set -euo pipefail

# Expect early-return guards in both close handlers for the relevant busy flags.
rg -n -C6 '\b(closeConfigTemplateModal|closeAgentsModal)\b' web-ui/modules

Also applies to: 18-18, 26-26, 111-111

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-config-template-agents.html` at line 1, Ensure
the modal close handlers closeConfigTemplateModal and closeAgentsModal check the
busy flags and early-return when any of configTemplateApplying, agentsSaving, or
agentsDiffLoading are true; update those methods to bail out immediately if any
of those flags are set so backdrop/@click.self dismissal cannot interrupt an
in-flight apply/save/diff operation and keep the existing overlay click handlers
as-is.

88-95: ⚠️ Potential issue | 🟡 Minor

Freeze AGENTS.md edits during save.

The textarea is readonly only while loading. During agentsSaving, users can still change agentsContent, which makes the editor drift from the content being persisted.

Minimal fix
                         <textarea
                             v-else
                             v-model="agentsContent"
                             class="form-input template-editor"
                             spellcheck="false"
-                            :readonly="agentsLoading"
+                            :readonly="agentsLoading || agentsSaving"
                             `@input`="onAgentsContentInput"
                             placeholder="在这里编辑 AGENTS.md 内容"></textarea>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-config-template-agents.html` around lines 88 -
95, The textarea allows edits while a save is in progress; make it immutable
during save by updating the readonly binding to include agentsSaving (change
:readonly="agentsLoading" to :readonly="agentsLoading || agentsSaving") and
either guard the input handler (onAgentsContentInput) to no-op when agentsSaving
is true or conditionally invoke it in the template (e.g., only call `@input` when
not agentsSaving) so agentsContent cannot drift while a save is in progress;
refer to the textarea element, v-model="agentsContent", :readonly,
agentsLoading, agentsSaving, and onAgentsContentInput to locate changes.

7-11: ⚠️ Potential issue | 🟡 Minor

Lock the config template editor while apply is running.

Users can keep editing configTemplateContent after applyConfigTemplate starts, so the editor can show text that was not part of the write.

Minimal fix
                     <textarea
                         v-model="configTemplateContent"
                         class="form-input template-editor"
                         spellcheck="false"
+                        :readonly="configTemplateApplying"
                         placeholder="在这里编辑 config.toml 模板内容"></textarea>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-config-template-agents.html` around lines 7 - 11,
The editor must be locked while apply runs: add a reactive boolean (e.g.
isApplyingTemplate) to the component and set it true at the start of
applyConfigTemplate and false on both success and error/finally, then bind the
textarea to that flag (e.g. :disabled="isApplyingTemplate" or
:readonly="isApplyingTemplate") so the v-model configTemplateContent cannot be
edited during apply; update all code paths in applyConfigTemplate to ensure the
flag is cleared in finally/error handlers.
web-ui/partials/index/panel-settings.html (1)

9-26: ⚠️ Potential issue | 🟡 Minor

Keep only the active settings subtab tabbable.

Both tab buttons still use a static tabindex="0", so the inactive subtab stays in the tab order instead of following the roving-tabindex pattern used by the main tablist.

Minimal fix
                         <button
                             id="settings-tab-backup"
                             role="tab"
                             aria-controls="settings-panel-backup"
                             :aria-selected="settingsTab === 'backup'"
-                            tabindex="0"
+                            :tabindex="settingsTab === 'backup' ? 0 : -1"
                             :class="['config-subtab', { active: settingsTab === 'backup' }]"
                             `@click`="onSettingsTabClick('backup')">
                             备份与导入
                         </button>
                         <button
                             id="settings-tab-trash"
                             role="tab"
                             aria-controls="settings-panel-trash"
                             :aria-selected="settingsTab === 'trash'"
-                            tabindex="0"
+                            :tabindex="settingsTab === 'trash' ? 0 : -1"
                             :class="['config-subtab', { active: settingsTab === 'trash' }]"
                             `@click`="onSettingsTabClick('trash')">
                             回收站
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-settings.html` around lines 9 - 26, The two
subtab buttons (ids settings-tab-backup and settings-tab-trash) currently have a
static tabindex="0" so inactive tabs remain tabbable; change their tabindex to a
reactive binding that follows the roving-tabindex pattern used by the main
tablist (e.g., :tabindex="settingsTab === 'backup' ? 0 : -1" for the backup
button and similarly for trash), keeping the existing :aria-selected binding and
leaving onSettingsTabClick unchanged so only the active subtab is in the tab
order.
web-ui/partials/index/panel-config-codex.html (1)

177-180: ⚠️ Potential issue | 🟠 Major

Provider switching is still mouse-only in this list.

The card itself is a plain clickable div, so keyboard users cannot focus and activate the main provider-switch path from here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-config-codex.html` around lines 177 - 180, The
provider cards are only clickable via mouse; update the card markup and handlers
so keyboard users can focus and activate them: ensure the element rendered for
each provider (the div with class "card" iterating over displayProvidersList and
checking displayCurrentProvider) is either changed to a semantic <button> or
given tabindex="0", role="button", and an `@keydown` handler that calls
switchProvider(provider.name) on Enter/Space; also add an accessible state
attribute (e.g., aria-pressed or aria-current) reflecting displayCurrentProvider
=== provider.name and preserve the existing `@click` behavior so both mouse and
keyboard invoke switchProvider(provider.name).
web-ui/partials/index/layout-header.html (1)

215-251: ⚠️ Potential issue | 🟡 Minor

Don't expose the side-rail shortcuts as a second tab widget.

panel-market and panel-settings are already labelled by tab-market / tab-settings. Keeping role="tab" and aria-controls on the side-rail buttons creates duplicate tab semantics for the same panels.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/layout-header.html` around lines 215 - 251, The
side-rail shortcut buttons (elements with id "side-tab-market" and
"side-tab-settings") are creating duplicate tab semantics by having role="tab",
aria-controls="panel-..." and aria-selected/tabindex bindings; remove
role="tab", aria-controls, :aria-selected and :tabindex attributes from those
buttons so they no longer announce as a second tab widget while keeping their
data-main-tab, id, active class, and handlers (isMainTabNavActive,
onMainTabPointerDown, onMainTabClick, mainTab, skillsList, skillsImportList)
intact.
web-ui/styles/base-theme.css (1)

1-1: ⚠️ Potential issue | 🟠 Major

Avoid a third-party font fetch in the local Web UI.

This @import makes a localhost/offline surface depend on Google at render time and leaks page loads off-box. That is a privacy and reliability regression.

Minimal fix
-@import url('https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500&family=JetBrains+Mono:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/base-theme.css` at line 1, The CSS contains a remote Google
Fonts `@import` in base-theme.css which causes external network fetches; remove
that `@import` line and instead reference only local or system fonts in the
font-family declarations (e.g., replace usages of "Fira Mono", "JetBrains Mono",
"Source Sans 3", "Space Grotesk" with locally bundled fonts via `@font-face` that
point to assets in the repo or with safe system fallbacks like "ui-monospace",
"system-ui", "sans-serif"), and add `@font-face` declarations that load fonts from
the project's static assets if you want those custom fonts packaged locally;
ensure all references in base-theme.css (font-family rules) match the local
`@font-face` names or fallbacks so no external fetch to fonts.googleapis.com
occurs.
web-ui/partials/index/modal-skills.html (2)

169-170: ⚠️ Potential issue | 🟠 Major

Keep “导入选中” disabled while scanning.

This footer action still ignores skillsScanningImports, so it can run while the import list and selected keys are being recomputed.

Minimal fix
-                    <button class="btn btn-confirm" `@click`="importSelectedSkills" :disabled="skillsImporting || skillsImportSelectedCount === 0 || skillsZipImporting || skillsExporting || skillsDeleting">
+                    <button class="btn btn-confirm" `@click`="importSelectedSkills" :disabled="skillsImporting || skillsScanningImports || skillsImportSelectedCount === 0 || skillsZipImporting || skillsExporting || skillsDeleting">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-skills.html` around lines 169 - 170, The footer
"导入选中" button can be clicked while imports are being scanned because the
disabled binding misses the skillsScanningImports flag; update the button's
:disabled expression in modal-skills.html (the button that calls
importSelectedSkills) to also include skillsScanningImports (i.e., add "||
skillsScanningImports" alongside the other flags) so the action stays disabled
while scanning recomputes the import list/selection.

2-5: ⚠️ Potential issue | 🟠 Major

Expose the Skills modal as a real dialog.

The wrapper still has no dialog semantics or label target, so screen readers will not announce the modal context/title correctly.

Minimal fix
-            <div class="modal modal-wide skills-modal">
+            <div
+                class="modal modal-wide skills-modal"
+                role="dialog"
+                aria-modal="true"
+                aria-labelledby="skills-modal-title"
+                tabindex="-1">
-                        <div class="modal-title">Skills 管理</div>
+                        <div id="skills-modal-title" class="modal-title">Skills 管理</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/modal-skills.html` around lines 2 - 5, The skills modal
lacks dialog semantics and a proper label target; add accessibility attributes
to the wrapper element with class "modal skills-modal" by giving it
role="dialog", aria-modal="true", and aria-labelledby that points to an id on
the title element (the element with class "modal-title"); ensure the title
element (class "modal-title") gets a stable id (e.g., skills-modal-title) and
make the dialog container focusable (tabindex="-1") so it can receive focus when
opened; update any JavaScript that opens the modal to focus the container (or
title) after adding these attributes.
web-ui/logic.runtime.mjs (1)

1-5: ⚠️ Potential issue | 🟡 Minor

Clamp non-finite latency values before rendering.

typeof === 'number' still allows NaN and Infinity, which renders as NaNms / Infinityms.

Minimal fix
-    const ms = typeof result.durationMs === 'number' ? result.durationMs : 0;
+    const ms = Number.isFinite(result.durationMs) ? result.durationMs : 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.runtime.mjs` around lines 1 - 5, formatLatency currently uses
typeof result.durationMs === 'number' which still allows NaN/Infinity to render;
update formatLatency to treat only finite numbers as valid by using
Number.isFinite(result.durationMs) (or equivalent) and fall back to 0 (or a
clamped value) when not finite, then format that finite/clamped value
(optionally rounded) before appending "ms".
web-ui/styles/sessions-list.css (1)

366-369: ⚠️ Potential issue | 🟠 Major

Don't hide the preview header classes.

This selector still removes .session-preview-title and .session-preview-meta, so the right-hand preview loses its title and metadata.

Minimal fix
-.session-item-sub.session-item-snippet,
-.session-preview-meta,
-.session-preview-title {
+.session-item-sub.session-item-snippet {
     display: none !important;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/sessions-list.css` around lines 366 - 369, The rule currently
hides .session-preview-title and .session-preview-meta and removes the preview
header; update the selector so only the list-item snippet is hidden. Remove
.session-preview-title and .session-preview-meta from the selector and keep the
rule targeting the list-only selector (e.g.,
.session-item-sub.session-item-snippet or the appropriate .session-item-sub /
.session-item-snippet combination) so the right-hand preview keeps its title and
metadata.
web-ui/modules/app.methods.claude-config.mjs (1)

21-30: ⚠️ Potential issue | 🟠 Major

Don't short-circuit externally-authenticated profiles.

Both branches return on missing apiKey before the existing external-credential flow can run, so profiles backed by externalCredentialType save changes but never become current from model changes or “保存并应用”.

Minimal fix
-            if (!this.claudeConfigs[name].apiKey) {
+            if (!this.claudeConfigs[name].apiKey && !this.claudeConfigs[name].externalCredentialType) {
                 this.showMessage('请先配置 API Key', 'error');
                 return;
             }
             this.applyClaudeConfig(name);
-            if (!config.apiKey) {
+            if (!config.apiKey && !config.externalCredentialType) {
                 this.showMessage('已保存,未应用', 'info');
                 this.closeEditConfigModal();
                 if (name === this.currentClaudeConfig) {
                     this.refreshClaudeModelContext();
                 }
                 return;
             }
+            if (!config.apiKey) {
+                this.closeEditConfigModal();
+                return this.applyClaudeConfig(name);
+            }

Also applies to: 69-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.claude-config.mjs` around lines 21 - 30, The early
return blocks profiles that use external credentials; change the apiKey check to
allow externally-authenticated profiles through. Replace the current "if
(!this.claudeConfigs[name].apiKey) { showMessage(...); return; }" with a guard
that only errors/returns when neither an apiKey nor an externalCredentialType is
present (i.e., if (!this.claudeConfigs[name].apiKey &&
!this.claudeConfigs[name].externalCredentialType) { this.showMessage('请先配置 API
Key', 'error'); return; }). Do the same fix in the other similar block (the one
around lines 69-77) so mergeClaudeConfig, saveClaudeConfigs,
updateClaudeModelsCurrent and applyClaudeConfig operate for external-credential
profiles.
web-ui/modules/app.methods.openclaw-persist.mjs (1)

8-33: ⚠️ Potential issue | 🟠 Major

Unsequenced modal loads can still clobber the current draft.

Both openOpenclawAddModal and openOpenclawEditModal fire-and-forget loadOpenclawConfigFromFile() using void. If the user quickly opens a different config or starts typing before the async load completes, lines 62-66 will overwrite openclawEditing.content with stale data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-persist.mjs` around lines 8 - 33, The
async loadOpenclawConfigFromFile calls started in openOpenclawAddModal and
openOpenclawEditModal are fire-and-forget and can race, clobbering
openclawEditing.content; change these callers to await the load (or use a
per-call request token stored on the component, e.g., this._openclawLoadId) and
before applying the loaded content verify the modal state still matches (compare
this.openclawEditing.name or the stored request token and
this.showOpenclawConfigModal/lockName) so only the most recent load updates
openclawEditing.content; update loadOpenclawConfigFromFile to return the loaded
result or accept an abort/token parameter to enable this check.
web-ui/modules/app.methods.openclaw-editing.mjs (1)

327-333: ⚠️ Potential issue | 🟠 Major

Persistence failures are still treated as success by callers.

saveOpenclawConfigs() catches the storage error and returns normally. Callers have no way to know the save failed, so the modal can close and a success toast can appear even though nothing was persisted. The config will be lost on reload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.openclaw-editing.mjs` around lines 327 - 333,
saveOpenclawConfigs currently swallows localStorage errors and returns normally,
so callers can't detect failures; change saveOpenclawConfigs() to propagate
failure by returning a boolean (true on success, false on failure) or by
rethrowing the caught error instead of silently swallowing it, and include the
error details in the this.showMessage('保存本地 OpenClaw 配置失败', 'error') call;
update any callers that open/close modals or show success to check the new
return value or catch the thrown error before proceeding (references:
saveOpenclawConfigs(), this.showMessage).
🧹 Nitpick comments (3)
web-ui/modules/app.methods.navigation.mjs (1)

276-283: scheduleAfterFrame callback is invoked synchronously on fallback path.

When requestAnimationFrame is unavailable, setTimeout(callback, 16) is used. However, the callback receives no timestamp argument, which differs from requestAnimationFrame's contract. If any caller depends on the timestamp, this would fail silently.

♻️ Optional consistency fix
         scheduleAfterFrame(task) {
             const callback = typeof task === 'function' ? task : () => {};
             if (typeof requestAnimationFrame === 'function') {
                 requestAnimationFrame(callback);
                 return;
             }
-            setTimeout(callback, 16);
+            setTimeout(() => callback(performance.now()), 16);
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.navigation.mjs` around lines 276 - 283, The
fallback in scheduleAfterFrame currently calls setTimeout(callback, 16) which
invokes the callback without the rAF timestamp; update scheduleAfterFrame so the
setTimeout wrapper calls the provided callback with a timestamp (e.g.,
performance.now() or Date.now()) to match requestAnimationFrame's contract (use
scheduleAfterFrame and the local variable callback to locate the code), e.g.
replace the direct setTimeout(callback, 16) with setTimeout(() =>
callback(performance.now()), 16) and ensure performance is used gracefully if
unavailable.
web-ui/modules/app.methods.codex-config.mjs (1)

340-431: Recursive replay of pending apply options could grow the call stack.

When _pendingCodexApplyOptions is set during an in-flight apply, lines 428-430 recursively call applyCodexConfigDirect. Under pathological conditions (many rapid requests with persistent failures), this could grow the call stack. The practical risk is low since each iteration awaits API calls, but consider using a loop instead.

💡 Optional: Replace recursion with iteration
         async applyCodexConfigDirect(options = {}) {
+            let currentOptions = options;
+            while (true) {
             if (this.codexApplying) {
                 this._pendingCodexApplyOptions = {
                     ...(this._pendingCodexApplyOptions || {}),
-                    ...options
+                    ...currentOptions
                 };
                 return;
             }
             // ... rest of implementation ...
             } finally {
                 this.codexApplying = false;
                 const pendingOptions = this._pendingCodexApplyOptions;
                 this._pendingCodexApplyOptions = null;
-                if (pendingOptions) {
-                    await this.applyCodexConfigDirect(pendingOptions);
-                }
+                if (!pendingOptions) {
+                    break;
+                }
+                currentOptions = pendingOptions;
             }
+            }
         },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/modules/app.methods.codex-config.mjs` around lines 340 - 431, The
finally block in applyCodexConfigDirect uses recursion by awaiting
applyCodexConfigDirect(pendingOptions) which can grow the call stack; change
this to an iterative loop: after setting this.codexApplying = false and reading
pendingOptions = this._pendingCodexApplyOptions (then clearing it), use a while
(pendingOptions) loop that calls await
this.applyCodexConfigDirect(pendingOptions) non-recursively by assigning
pendingOptions = this._pendingCodexApplyOptions and clearing it between
iterations, ensuring _pendingCodexApplyOptions is consumed safely and preventing
recursive stack growth while preserving existing behavior of
applyCodexConfigDirect, _pendingCodexApplyOptions and codexApplying.
web-ui/logic.agents-diff.mjs (1)

33-50: LCS matrix allocates O(n×m) memory.

The standard DP approach is correct but allocates a full (beforeLines.length + 1) × (afterLines.length + 1) matrix. For two 3000-line files at the boundary, this is ~9 million cells. This is mitigated by the LARGE_DIFF_LINE_LIMIT check that switches to the heuristic approach for larger inputs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/logic.agents-diff.mjs` around lines 33 - 50, The current
buildDiffLcsMatrix allocates a full (n+1)×(m+1) matrix causing O(n×m) memory;
change it to use a memory-optimized DP: if only LCS length is required replace
the full matrix with two rolling rows (arrays of length afterLines.length+1)
inside buildDiffLcsMatrix to compute lengths in O(min(n,m)) space, and if a full
traceback matrix is required add a flag parameter (e.g., needFullMatrix) so
callers can request the full matrix only when necessary; keep the existing
logic/return when needFullMatrix is true, otherwise return just the final LCS
row/length from buildDiffLcsMatrix and adjust callers to use the optimized
result.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba737d62-1c63-425b-99a2-ac4a0cb81774

📥 Commits

Reviewing files that changed from the base of the PR and between 02514bd and a7eb34d.

📒 Files selected for processing (77)
  • .github/workflows/ci.yml
  • cli.js
  • tests/e2e/run.js
  • tests/e2e/test-web-ui-assets.js
  • tests/unit/agents-diff-ui.test.mjs
  • tests/unit/claude-settings-sync.test.mjs
  • tests/unit/compact-layout-ui.test.mjs
  • tests/unit/config-tabs-ui.test.mjs
  • tests/unit/helpers/web-ui-app-options.mjs
  • tests/unit/helpers/web-ui-source.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/run.mjs
  • tests/unit/session-tab-switch-performance.test.mjs
  • tests/unit/session-trash-state.test.mjs
  • tests/unit/skills-modal-ui.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • tests/unit/web-ui-logic.test.mjs
  • tests/unit/web-ui-restart.test.mjs
  • tests/unit/web-ui-source-bundle.test.mjs
  • web-ui/app.js
  • web-ui/index.html
  • web-ui/logic.agents-diff.mjs
  • web-ui/logic.claude.mjs
  • web-ui/logic.mjs
  • web-ui/logic.runtime.mjs
  • web-ui/logic.sessions.mjs
  • web-ui/modules/api.mjs
  • web-ui/modules/app.computed.dashboard.mjs
  • web-ui/modules/app.computed.index.mjs
  • web-ui/modules/app.computed.session.mjs
  • web-ui/modules/app.constants.mjs
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/app.methods.claude-config.mjs
  • web-ui/modules/app.methods.codex-config.mjs
  • web-ui/modules/app.methods.index.mjs
  • web-ui/modules/app.methods.install.mjs
  • web-ui/modules/app.methods.navigation.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/modules/app.methods.openclaw-editing.mjs
  • web-ui/modules/app.methods.openclaw-persist.mjs
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/app.methods.runtime.mjs
  • web-ui/modules/app.methods.session-actions.mjs
  • web-ui/modules/app.methods.session-browser.mjs
  • web-ui/modules/app.methods.session-timeline.mjs
  • web-ui/modules/app.methods.session-trash.mjs
  • web-ui/modules/app.methods.startup-claude.mjs
  • web-ui/partials/index/layout-footer.html
  • web-ui/partials/index/layout-header.html
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/modal-confirm-toast.html
  • web-ui/partials/index/modal-openclaw-config.html
  • web-ui/partials/index/modal-skills.html
  • web-ui/partials/index/modals-basic.html
  • web-ui/partials/index/panel-config-claude.html
  • web-ui/partials/index/panel-config-codex.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/partials/index/panel-market.html
  • web-ui/partials/index/panel-sessions.html
  • web-ui/partials/index/panel-settings.html
  • web-ui/source-bundle.cjs
  • web-ui/styles.css
  • web-ui/styles/base-theme.css
  • web-ui/styles/controls-forms.css
  • web-ui/styles/feedback.css
  • web-ui/styles/layout-shell.css
  • web-ui/styles/modals-core.css
  • web-ui/styles/navigation-panels.css
  • web-ui/styles/openclaw-structured.css
  • web-ui/styles/responsive.css
  • web-ui/styles/sessions-list.css
  • web-ui/styles/sessions-preview.css
  • web-ui/styles/sessions-toolbar-trash.css
  • web-ui/styles/skills-list.css
  • web-ui/styles/skills-market.css
  • web-ui/styles/titles-cards.css

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 5, 2026

@github-actions[bot] Understood — I apologize for prior noise. I’ll redo the review now strictly limited to correctness, safety, and quality, with zero breaking changes, no refactors, and no API/interface/behavior alterations. I’ll report only concrete, non-breaking issues and test gaps.


🧠 Learnings used
Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-03T10:42:22.368Z
Learning: In PR `#67` for SakuraByteCore/codexmate, github-actions[bot] flagged that CodeRabbit was again introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-04T01:36:26.964Z
Learning: In PR `#68` for SakuraByteCore/codexmate, github-actions[bot] again flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint (reinforced across PRs `#55`, `#59`, `#67`, `#68`) is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-31T12:37:51.023Z
Learning: In PR `#59` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-30T04:47:15.352Z
Learning: In PR `#55` for SakuraByteCore/codexmate, github-actions[bot] repeatedly flagged that CodeRabbit was introducing breaking changes. The maintainer's absolute constraint is: no breaking changes in any review suggestion; limit all feedback to correctness, safety, and quality only within the existing design, without refactoring or altering APIs, interfaces, or behavior.

Learnt from: github-actions[bot]
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-04-05T03:58:26.778Z
Learning: For SakuraByteCore/codexmate reviews, only provide non-breaking feedback (correctness, safety, quality). Avoid API, behavior, or refactor suggestions. This rule applies across PRs `#55`, `#59`, `#67`, and `#68`.

Learnt from: SurviveM
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-29T02:42:32.785Z
Learning: The repository maintainer SurviveM (GitHub: SurviveM, repo: SakuraByteCore/codexmate) prefers that CodeRabbit review suggestions do not introduce breaking changes. Reviews should focus on correctness, safety, and quality without recommending refactors or changes that would break existing APIs, interfaces, or behavior.

Learnt from: ymkiux
Repo: SakuraByteCore/codexmate PR: 0
File: :0-0
Timestamp: 2026-03-23T15:38:41.305Z
Learning: In the SakuraByteCore/codexmate repository, BOM (U+FEFF) characters in Chinese markdown and JavaScript config files (e.g., site/index.md, site/.vitepress/config.js) are intentional and follow the project's encoding convention. Do not flag BOM presence in these files as an issue. The PostCSS BOM parse issue was addressed separately by adding BOM-free site/package.json and site/postcss.config.cjs files.
✅ Actions performed

Full review triggered.

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