refactor: split web ui modules and harden provider switching#68
Conversation
|
@coderabbitai re-review |
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (89)
📝 WalkthroughWalkthroughServe 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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
Redoing the full review strictly focused on correctness, safety, and quality — no breaking changes will be suggested. ✏️ Learnings added
🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
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 | 🟡 MinorTrailing 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 | 🟡 MinorAdd 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 useimport foo from './bar'orimport * 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 | 🟡 MinorUnhandled error from
selectSessionmay leave UI in inconsistent state.If
selectSessionthrows (line 205), the error propagates after the session has already been removed fromsessionsList. 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 | 🟡 MinorKeep 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 | 🟡 MinorAdd 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 | 🟡 MinorWrap
localStorage.setItemin try-catch to handle quota/access errors.
localStoragecan throwQuotaExceededErrororSecurityError(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 | 🟡 MinorDuplicate logic in else branches.
Lines 432-438 and 439-446 contain identical code. The
else ifandelsebranches 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 | 🟡 MinorMissing try/catch in applyOpenclawConfig.
Unlike
saveAndApplyOpenclawConfigwhich wraps the API call in try/catch,applyOpenclawConfigdoesn't catch network/runtime errors from theapi()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 | 🟡 MinorModal closed before API call completes.
updateProvidercallscloseEditModal()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 | 🟡 MinorMissing try/catch in deleteProvider.
Unlike
addProviderwhich wraps the API call in try/catch,deleteProviderdoesn'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, andprojectRootvariables 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 constantfalse— verify intent or add TODO.
hasLocalAndProxy()always returnsfalse. 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
disabledHTML 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
closeClaudeConfigModalresetsnewClaudeConfigwith hardcoded values forbaseUrlandmodel. 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.
copyResumeCommandtriesfallbackCopyTextfirst thennavigator.clipboard, whilecopyInstallCommanddoes 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
📒 Files selected for processing (75)
cli.jstests/e2e/run.jstests/e2e/test-web-ui-assets.jstests/unit/agents-diff-ui.test.mjstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/helpers/web-ui-source.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/run.mjstests/unit/session-tab-switch-performance.test.mjstests/unit/session-trash-state.test.mjstests/unit/skills-modal-ui.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/index.htmlweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.mjsweb-ui/logic.runtime.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.index.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.constants.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.index.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/layout-header.htmlweb-ui/partials/index/modal-config-template-agents.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modal-openclaw-config.htmlweb-ui/partials/index/modal-skills.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-claude.htmlweb-ui/partials/index/panel-config-codex.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-market.htmlweb-ui/partials/index/panel-sessions.htmlweb-ui/partials/index/panel-settings.htmlweb-ui/source-bundle.cjsweb-ui/styles.cssweb-ui/styles/base-theme.cssweb-ui/styles/controls-forms.cssweb-ui/styles/feedback.cssweb-ui/styles/layout-shell.cssweb-ui/styles/modals-core.cssweb-ui/styles/navigation-panels.cssweb-ui/styles/openclaw-structured.cssweb-ui/styles/responsive.cssweb-ui/styles/sessions-list.cssweb-ui/styles/sessions-preview.cssweb-ui/styles/sessions-toolbar-trash.cssweb-ui/styles/skills-list.cssweb-ui/styles/skills-market.cssweb-ui/styles/titles-cards.css
|
🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 19
♻️ Duplicate comments (3)
cli.js (3)
10023-10037:⚠️ Potential issue | 🟠 MajorAdd an
'error'listener for eachFSWatcher.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 | 🔴 CriticalBundled dynamic asset readers are still gated behind disk existence checks.
Line 10709 returns
404before Line 10714 checksPUBLIC_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 | 🟠 MajorFile-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 thev-elseblock ofv-if="sessionStandalone"(line 8). Since we're already in the else branch,sessionStandaloneis 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
#fff6f5instead 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
📒 Files selected for processing (75)
cli.jstests/e2e/run.jstests/e2e/test-web-ui-assets.jstests/unit/agents-diff-ui.test.mjstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/helpers/web-ui-source.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/run.mjstests/unit/session-tab-switch-performance.test.mjstests/unit/session-trash-state.test.mjstests/unit/skills-modal-ui.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/index.htmlweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.mjsweb-ui/logic.runtime.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.index.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.constants.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.index.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/layout-header.htmlweb-ui/partials/index/modal-config-template-agents.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modal-openclaw-config.htmlweb-ui/partials/index/modal-skills.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-claude.htmlweb-ui/partials/index/panel-config-codex.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-market.htmlweb-ui/partials/index/panel-sessions.htmlweb-ui/partials/index/panel-settings.htmlweb-ui/source-bundle.cjsweb-ui/styles.cssweb-ui/styles/base-theme.cssweb-ui/styles/controls-forms.cssweb-ui/styles/feedback.cssweb-ui/styles/layout-shell.cssweb-ui/styles/modals-core.cssweb-ui/styles/navigation-panels.cssweb-ui/styles/openclaw-structured.cssweb-ui/styles/responsive.cssweb-ui/styles/sessions-list.cssweb-ui/styles/sessions-preview.cssweb-ui/styles/sessions-toolbar-trash.cssweb-ui/styles/skills-list.cssweb-ui/styles/skills-market.cssweb-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
testWebUiAssetshere 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(), andreadBundledWebUiScript()aligns with the new bundling infrastructure while preservingreadProjectFile()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 conditionalaria-labelledby/aria-labelhandling. 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 tosource-bundle.cjsmethods 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. TheslideDownkeyframe properly maintains the centeredtranslateX(-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.watchto 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
renameevent 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 forpreserveLoading.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-uiand 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.jsimports (e.g.,app.computed.index.mjs,app.methods.index.mjs, and their transitive dependencies). Without these files,createHeadWebUiFixturewill produce an incomplete fixture that cannot properly bundle and execute.
40-53: LGTM - Clean global override pattern.The
withGlobalOverridesfunction 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", dynamicaria-selected,aria-controlsreferences, and rovingtabindexmanagement.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-pressedon toggle buttons)- Consistent disabled state management across all interactive elements
- Appropriate loading/empty/list state handling
- Unique
:keybindings for allv-foriterationsweb-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.
sessionPathRequestSeqis 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-flightsession-plainresponse to still apply after the selection was cleared. Additionally, Lines 399-400 dereferencethis.activeSessionafter 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
finallyblocks 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.stopfor 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.sessionTrashCountat line 385 is a computed property (defined inweb-ui/modules/app.computed.session.mjs) that normalizes the raw state propertysessionTrashTotalCount. Using the computed property inclearSessionTrash()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
providerSwitchDisplayTargetis set immediately whilecurrentProviderremains 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 fromweb-ui/modules/.
318-335: Well-structured parity test for Vue app skeleton.The test properly validates that
data(),methods,computed,mounted, andbeforeUnmountstructures match between current and HEAD versions, with allowance for the newproviderSwitchDisplayTargetkey.web-ui/modules/app.methods.navigation.mjs (2)
276-283: Solid fallback pattern forscheduleAfterFrame.The implementation correctly uses
requestAnimationFramewhen available and falls back tosetTimeoutwith 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
applyOpenclawStructuredToTextmethod 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 inmatchClaudeConfigFromSettings.The logic properly prioritizes exact
apiKeymatch (lines 60-62), then falls back toexternalCredentialTypematch only when settings lack anapiKeyand 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: transformhint is appropriate for hover animations.
380-382: The@keyframes spinanimation is properly defined. It's located inweb-ui/styles/feedback.cssat line 94, confirming the.card-action-btn.loading svganimation reference is valid.web-ui/modules/app.methods.session-actions.mjs (2)
143-153: Correct shell argument quoting.The
quoteShellArghelper 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 thefinallyblock, 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
parseandstringifyfunctions onwindow.JSON5before using it, falling back to nativeJSONwhen unavailable. This allows JSON5 to be an optional enhancement.
332-337: Good defensive masking for sensitive values.The
maskProviderValuehelper 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.
parseOptionalNumberproperly handles empty strings (returningnull), 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 (settingmodelsSourceto'error'),currentProvideris already updated butcurrentModelmay be stale from the previous provider. Lines 84-87 then callapplyCodexConfigDirectwith this potentially invalid provider/model combination.
228-249: HandleprovidersListas string array.If
providersListcontains provider names as strings (as suggested by other parts of the PR), the filterprovider && provider.namewill drop all entries since strings don't have a.nameproperty. The speed-test loop would never execute.
12-21: LGTM: Download helper with proper cleanup.The
downloadTextFilemethod 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
buildLineDifffunction 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: GuardimportBackupFileagainst re-entry.
claudeImportLoading/codexImportLoadingis set but never checked before proceeding. Re-selecting a file while a restore is in progress can fire overlappingrestore-*-dirwrites against the same config directory.
74-114: LGTM: Download methods with proper concurrency guards.Both
downloadClaudeDirectoryanddownloadCodexDirectorycorrectly check their loading flags at the start and return early if already in progress. The progress ramp viasetIntervalwith cleanup infinallyis properly managed.web-ui/styles/base-theme.css (2)
1-1: External Google Fonts import creates runtime dependency.The
@importfrom 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: Treatapi('list')failures as startup failures.
loadAll()only checksstatusRes.error. Ifapi('list')returns{ error: ... }, the code still enters the success path and setsprovidersList = listRes.providers, which would beundefined. Downstream code expecting an array would fail.
185-204: LGTM: Config import with duplicate detection.
ensureClaudeConfigFromSettingsproperly validates required fields, checks for existing duplicates, and generates unique sequential names when the preferred name is taken. The persistence viasaveClaudeConfigs()is correctly triggered after mutation.
274-330: LGTM: Claude model loading with external credential awareness.
loadClaudeModelscorrectly handles the edge case whereexternalCredentialTypeis set butapiKeyis 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
externalCredentialTypeawareness). 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
prepareAgentsDiffimplementation 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
applyAgentsContentmethod 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.
|
@coderabbitai re-review |
|
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 🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 13
♻️ Duplicate comments (20)
web-ui/logic.sessions.mjs (1)
27-49:⚠️ Potential issue | 🟡 MinorReturn normalized filters from
buildSessionListParams().This issue was flagged in a previous review. Lines 38-39 return the raw
sourceandpathFiltervalues, but Line 36 already usesisSessionQueryEnabled(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 | 🟠 MajorExternal 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 | 🔴 CriticalRegistry 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 | 🟠 MajorBundled dynamic assets are still blocked when source files are absent.
Line 10709 returns
404before checkingPUBLIC_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 | 🟠 MajorFile-level watchers can miss edits after atomic-save rename patterns.
addWatcherwatches 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 | 🟠 MajorHandle
FSWatchererrors 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 | 🟡 MinorKeep 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. BindtabindextosettingsTabso 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 | 🟠 MajorTrash 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 whensessionTrashCountsays 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 | 🟡 MinorHandle rejected deletes the same way as the other provider mutations.
A transport failure from
api('delete-provider')orloadAll()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 | 🟡 MinorCatch API failures in the model add/remove actions.
Both handlers still let rejected
api(...)orloadAll()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 | 🔴 CriticalHEAD 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 missingmodules/app.constants.mjs. Please make that helper copy the full transitiveweb-ui/modules/dependency set, or just stage the wholeweb-ui/modules/directory, before evaluatingapp.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 | 🟠 MajorDon't silently re-apply the previous model after a provider load error.
currentProvideris updated beforeloadModelsForProvider(name)finishes. If that loader returns withthis.modelsSource === 'error', Line 86 still pushes the oldcurrentModelback throughapplyCodexConfigDirect({ 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 | 🟡 MinorHandle 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 | 🟠 MajorInvalidate stale standalone loads when the active session is cleared.
The early return on Lines 371-376 does not advance
sessionStandaloneRequestSeq, so an oldersession-plainresponse can still win after the selection was cleared. The success path on Lines 399-400 then dereferencesthis.activeSessionafterawait, which can throw if the selection becamenullin 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 | 🟠 MajorKeep session-path request tokens scoped per source.
Lines 95-117 still share
this.sessionPathRequestSeqacrosscodex,claude, andall. 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 | 🟡 MinorHeader 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 | 🟠 MajorGuard
importBackupFile()against re-entry.
claudeImportLoading/codexImportLoadingis set here but never checked. Selecting another backup while a restore is still running can start overlappingrestore-*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 | 🟠 MajorHandle rejected
apply-openclaw-configrequests here too.
applyOpenclawConfig()still awaits the API call without atry/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 | 🟠 MajorPreserve trailing-blank-line edits in
splitDiffLines().Stripping the final
\nbefore 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 | 🟠 MajorHandle
api('list')failures in the startup gate as well.
loadAll()currently only checksstatusRes.error. IflistRes.erroris present, the code still enters success flow and assignsprovidersListfrom 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-warncolor is hardcoded as#8d5b31while 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
📒 Files selected for processing (75)
cli.jstests/e2e/run.jstests/e2e/test-web-ui-assets.jstests/unit/agents-diff-ui.test.mjstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/helpers/web-ui-source.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/run.mjstests/unit/session-tab-switch-performance.test.mjstests/unit/session-trash-state.test.mjstests/unit/skills-modal-ui.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/index.htmlweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.mjsweb-ui/logic.runtime.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.index.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.constants.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.index.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/layout-header.htmlweb-ui/partials/index/modal-config-template-agents.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modal-openclaw-config.htmlweb-ui/partials/index/modal-skills.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-claude.htmlweb-ui/partials/index/panel-config-codex.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-market.htmlweb-ui/partials/index/panel-sessions.htmlweb-ui/partials/index/panel-settings.htmlweb-ui/source-bundle.cjsweb-ui/styles.cssweb-ui/styles/base-theme.cssweb-ui/styles/controls-forms.cssweb-ui/styles/feedback.cssweb-ui/styles/layout-shell.cssweb-ui/styles/modals-core.cssweb-ui/styles/navigation-panels.cssweb-ui/styles/openclaw-structured.cssweb-ui/styles/responsive.cssweb-ui/styles/sessions-list.cssweb-ui/styles/sessions-preview.cssweb-ui/styles/sessions-toolbar-trash.cssweb-ui/styles/skills-list.cssweb-ui/styles/skills-market.cssweb-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)
web-ui/styles/modals-core.css
Outdated
| 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); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for fadeIn and slideUp keyframe definitions
rg -n '@keyframes\s+(fadeIn|slideUp)' --type=cssRepository: 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 || trueRepository: 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 -10Repository: 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 -20Repository: 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" || trueRepository: 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 -10Repository: 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 -20Repository: 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).
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
web-ui/source-bundle.cjs (1)
218-219:⚠️ Potential issue | 🟠 MajorCanonicalize
entryPathbefore 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 onprocess.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.mjsre-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 directweb-ui/logic.mjsnamespace instead of checking onlynormalizeClaudeValueandbuildSessionTimelineNodes.
🤖 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
📒 Files selected for processing (4)
tests/unit/helpers/web-ui-app-options.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-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.
|
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 🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (26)
web-ui/styles/controls-forms.css (1)
317-322:⚠️ Potential issue | 🟡 MinorPrevent hover styling from applying to disabled tool buttons.
Disabled
.btn-tool/.btn-tool-compactcan 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 | 🟠 MajorGuard
locationat import time to prevent non-browser crashes.Line 1 dereferences bare
location; importing this module in Node/test contexts can throwReferenceErrorbefore 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 | 🟠 MajorAvoid third-party font
@importin 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 | 🟠 MajorDynamic bundled assets are still blocked by early disk existence check.
Line 10709 returns
404before 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.htmlunavailable 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 | 🔴 CriticalAttach an
errorlistener to eachfs.watchwatcher.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 | 🔴 CriticalFile-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 | 🟠 MajorMask 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 | 🟠 MajorAdd dialog semantics to every modal container.
These
.modalelements still lackrole="dialog",aria-modal="true", and anaria-labelledbylink 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 | 🟡 MinorPreserve 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 | 🟠 MajorMake 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 explicitaria-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 | 🟡 MinorMissing error handling in
deleteProvider.Unlike
addProvider,updateProvider, andresetConfig, thedeleteProvidermethod lackstry/catcharound theapi()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 | 🟡 MinorMissing error handling in
addModelandremoveModel.Both methods call
api()withouttry/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 | 🟠 MajorConfig normalization loses credential type inference for stored configs.
normalizeClaudeConfig()preservesexternalCredentialTypeas-is but doesn't infer it fromauthToken/useKeywhen missing. In contrast,normalizeClaudeSettingsEnv()does inferexternalCredentialTypefrom credential presence.When a stored config has
authTokenbut no persistedexternalCredentialType,normalizeClaudeConfig()returnsexternalCredentialType: '', causingmatchClaudeConfigFromSettings()andfindDuplicateClaudeConfigName()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 | 🟠 MajorProvider cards lack keyboard accessibility.
The provider card
divat line 178-180 only has a@clickhandler. Withouttabindex="0"and keyboard event handlers (@keydown.enter,@keydown.space), keyboard users cannot navigate to and activate provider cards to switch providers. The nested.card-actionsbuttons 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 | 🟡 MinorHeader resize doesn't invalidate measurement cache.
The
ResizeObservercallback only callsupdateSessionTimelineOffset()to update the CSS variable. When the sticky header wraps or changes height, cached message top positions in__sessionTimelineMeasurementCachebecome 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 | 🟠 MajorBase 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 whensessionTrashTotalCountsays 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 | 🟠 MajorSpan the full row here to avoid overflow on one-column layouts.
grid-column: span 2creates 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 | 🟡 MinorReturn normalized filters from
buildSessionListParams().
queryValuealready uses normalized source semantics, but the returned object still sends rawsourceandpathFilter. 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 | 🟠 MajorCatch rejected
apply-claude-configcalls 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 | 🟡 MinorMissing exception handling around API call.
Unlike
saveAndApplyOpenclawConfigwhich wraps the API call in try-catch,applyOpenclawConfigcallsapi('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 | 🟠 MajorEdit modal loads file content instead of preset content.
The modal is titled as editing preset
name, but it clearsopenclawEditing.contentand 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 | 🟠 MajorStale standalone loads can apply after session is cleared.
The early return on Lines 371-377 doesn't increment
sessionStandaloneRequestSeq, so an oldersession-plainresponse can still apply. Additionally, Lines 399-400 dereferencethis.activeSessionwhich may benullif 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 | 🟠 MajorShared request token can invalidate concurrent source requests.
sessionPathRequestSeqis 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 | 🟠 MajorSpeed test loop drops string provider names.
The filter on Line 230 checks
provider && provider.name, butprovidersListmay 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 | 🟠 MajorConfig re-apply proceeds even when model load fails.
After
loadModelsForProvider(name)fails,currentProvideris already switched butcurrentModelmay 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 | 🟡 MinorIdentical branches for trash count update.
Lines 432-438 (
sessionTrashCountLoadedOncetrue) 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
📒 Files selected for processing (75)
cli.jstests/e2e/run.jstests/e2e/test-web-ui-assets.jstests/unit/agents-diff-ui.test.mjstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/helpers/web-ui-source.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/run.mjstests/unit/session-tab-switch-performance.test.mjstests/unit/session-trash-state.test.mjstests/unit/skills-modal-ui.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/index.htmlweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.mjsweb-ui/logic.runtime.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.index.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.constants.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.index.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/layout-header.htmlweb-ui/partials/index/modal-config-template-agents.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modal-openclaw-config.htmlweb-ui/partials/index/modal-skills.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-claude.htmlweb-ui/partials/index/panel-config-codex.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-market.htmlweb-ui/partials/index/panel-sessions.htmlweb-ui/partials/index/panel-settings.htmlweb-ui/source-bundle.cjsweb-ui/styles.cssweb-ui/styles/base-theme.cssweb-ui/styles/controls-forms.cssweb-ui/styles/feedback.cssweb-ui/styles/layout-shell.cssweb-ui/styles/modals-core.cssweb-ui/styles/navigation-panels.cssweb-ui/styles/openclaw-structured.cssweb-ui/styles/responsive.cssweb-ui/styles/sessions-list.cssweb-ui/styles/sessions-preview.cssweb-ui/styles/sessions-toolbar-trash.cssweb-ui/styles/skills-list.cssweb-ui/styles/skills-market.cssweb-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)
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (36)
cli.js (3)
10023-10037:⚠️ Potential issue | 🟠 MajorHandle
FSWatchererrors locally.
fs.watch()can emit'error'when a watched path disappears or becomes inaccessible. With no listener onwatcher, a directory rename/delete can terminatecodexmate runinstead 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 | 🟠 MajorConsult bundled readers before returning
404.Lines 10709-10713 return
404beforePUBLIC_WEB_UI_DYNAMIC_ASSETSis checked. In source-less runs that makes/web-ui/index.html,/web-ui/app.js,/web-ui/logic.mjs, and/web-ui/styles.cssunreachable 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 | 🔴 CriticalDirect file watches still break after atomic-save replaces the file.
When
targetis a file,addWatcher()still usesfs.watch(target, ...). Editors that save via temp-file + rename replace that inode, so the watcher onlegacyHtmlPathcan stop seeing subsequent edits until the process restarts. Watching the parent directory and filtering onpath.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 | 🟡 MinorLet
claudeModelHasList()mirror the fallback option logic.Line 13 only checks
claudeModels.length, but Lines 16-20 already prependcurrentClaudeModelwhen 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 | 🟡 MinorResolve relative module entry paths before bundling.
Line 219 forwards
entryPathunchanged. If a caller passes a relative module path here, nested imports resolve fromprocess.cwd()instead ofweb-ui/, which is the same failure mode that produced/modules/app.constants.mjsENOENT 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 | 🟡 MinorRe-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, theheadfixture and thecurrentfixture 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 | 🟠 MajorGuard
locationbefore readingAPI_BASEto 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 | 🟠 MajorHidden 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 | 🟠 MajorCard selection and icon actions are still not keyboard/screen-reader accessible.
Line 54 creates a clickable
divwithout keyboard semantics. Lines 72 and 85 rely ontitlefor 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 | 🟠 MajorUse a real button for model removal.
span+@clickis 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 | 🟠 MajorMask the provider and Claude API key inputs.
These fields are still plain-text inputs. Switch them to
type="password"and addautocomplete="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 | 🟠 MajorAdd dialog semantics to every modal container.
Each
.modalblock still lacksrole="dialog",aria-modal="true", and a title reference viaaria-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 | 🟠 MajorLock the template editor while apply is running.
configTemplateApplyingdisables 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 | 🟠 MajorAdd dialog semantics to these modal containers.
Both
.modalblocks lackrole="dialog",aria-modal="true", and a title reference viaaria-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 | 🟡 MinorKeep
.session-item-copyat 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 | 🟡 MinorImplement roving tabindex for the settings subtabs.
Both buttons stay in the tab order because
tabindexis hard-coded to0. 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 | 🟠 MajorKeep “导入选中” disabled while the import list is still scanning.
This footer action still ignores
skillsScanningImports, soimportSelectedSkillscan 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 | 🟠 MajorBundled UI still hard-depends on Google Fonts.
This
@importkeeps/web-uifrom 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 2can 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 | 🟡 MinorDon'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"andaria-controlshere 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 | 🟡 MinorRejected provider/model requests still escape these handlers.
deleteProvider,addModel, andremoveModelawaitapi()without a localtry/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 | 🟠 MajorProvider switching is still mouse-only.
The active-provider card is a plain
divwith 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 | 🟠 MajorPreserve trailing blank-line edits in
splitDiffLines().Dropping the final
\nmakes 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 | 🟠 MajorHandle string entries in
providersListhere.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 | 🟠 MajorDo not re-apply Codex config after a model-load failure.
If
loadModelsForProvider()leavesmodelsSource === 'error', the silent re-apply still uses the previous provider’scurrentModel.🛠️ 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 | 🟠 MajorCatch rejected
apply-claude-configcalls 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 | 🟠 MajorOpen 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 | 🟠 MajorCatch
apply-openclaw-configfailures inapplyOpenclawConfig().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 | 🟠 MajorScope 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 | 🟠 MajorInvalidate stale standalone loads when
activeSessionis cleared.Clearing the selection does not advance
sessionStandaloneRequestSeq, so an oldersession-plainresponse can still apply. The later fallback tothis.activeSessionalso dereferencesnullin 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 | 🟠 MajorUse
sessionTrashCountfor trash pagination.These getters stop at the number of loaded entries, so the UI can show “no more” while
sessionTrashTotalCountstill 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 | 🟡 MinorReturn normalized
sourceandpathFilterfrombuildSessionListParams().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 | 🟠 MajorAbort
loadAll()when either startup call fails.Only
statusRes.erroris handled, and that failure path still falls through to the later model/profile loads. Ifapi('list')fails,providersListbecomesundefined; ifapi('status')fails, the method still continues with stale state afterfinally.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 startuptry. IflocalStorageis unavailable or throws, startup jumps to the catch block and setsinitErrorto 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 | 🟠 MajorPrevent overlapping restore requests for the same target directory.
claudeImportLoading/codexImportLoadingare set here but never checked before a new import starts. Re-selecting a file while a restore is still in flight can issue concurrentrestore-*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
📒 Files selected for processing (75)
cli.jstests/e2e/run.jstests/e2e/test-web-ui-assets.jstests/unit/agents-diff-ui.test.mjstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/helpers/web-ui-source.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/run.mjstests/unit/session-tab-switch-performance.test.mjstests/unit/session-trash-state.test.mjstests/unit/skills-modal-ui.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/index.htmlweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.mjsweb-ui/logic.runtime.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.index.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.constants.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.index.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/layout-header.htmlweb-ui/partials/index/modal-config-template-agents.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modal-openclaw-config.htmlweb-ui/partials/index/modal-skills.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-claude.htmlweb-ui/partials/index/panel-config-codex.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-market.htmlweb-ui/partials/index/panel-sessions.htmlweb-ui/partials/index/panel-settings.htmlweb-ui/source-bundle.cjsweb-ui/styles.cssweb-ui/styles/base-theme.cssweb-ui/styles/controls-forms.cssweb-ui/styles/feedback.cssweb-ui/styles/layout-shell.cssweb-ui/styles/modals-core.cssweb-ui/styles/navigation-panels.cssweb-ui/styles/openclaw-structured.cssweb-ui/styles/responsive.cssweb-ui/styles/sessions-list.cssweb-ui/styles/sessions-preview.cssweb-ui/styles/sessions-toolbar-trash.cssweb-ui/styles/skills-list.cssweb-ui/styles/skills-market.cssweb-ui/styles/titles-cards.css
|
@coderabbitai re-review |
|
This constraint has been reinforced across PRs 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 🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
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 | 🟠 MajorGuard allowlisted static assets against edit-time TOCTOU races.
Line 10738 performs
fs.existsSync()followed byfs.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, orfs.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 | 🟠 MajorClear
claudeModelsLoadingon the unlimited early-return path.Line 315 returns after advancing
claudeModelsRequestSeq, but before Line 322 ever reinitializes the loading lifecycle. If a previousmodels-by-urlrequest was still in flight, itsfinallywill 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 | 🟠 MajorIgnore stale failures in both model loaders.
Line 139 and Line 363 only guard the post-
awaitsuccess path. If an older request rejects after the provider/config has already changed, thesecatchblocks still toast the stale error and overwrite the newer selection witherrorstate.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 | 🟠 MajorBuild the HEAD fixture from a real baseline ref, not the checked-out PR commit.
git show HEAD:...on Line 49 andcollectJavaScriptFiles(entryPath)on Line 58 both resolve against the current checkout, so in PR CIcaptureHeadBundledAppOptions()rebundles the PR snapshot instead of an older baseline. That makes the parity suite self-comparing; the failure intests/unit/web-ui-behavior-parity.test.mjson 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),updateProvidercallscloseEditModal()at line 169 before entering thetryblock. If the network request fails, the user's edits are discarded without recovery.Consider moving
closeEditModal()inside thetryblock after the success path, matching the pattern inaddProvider:♻️ 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
📒 Files selected for processing (29)
cli.jstests/unit/claude-settings-sync.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-logic.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-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
parseOpenclawContentmethod 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!
fillOpenclawQuickFromConfigcorrectly handles both modern (config.agents.defaults.model) and legacy (config.agent.model) config shapes, with proper type guards throughout.
182-197: LGTM!
syncOpenclawQuickFromTextproperly parses withallowEmpty: trueand handles both success and failure paths with appropriate messaging.
199-213: LGTM!
mergeOpenclawModelEntrycorrectly handles the merge vs overwrite semantics with proper null/undefined/empty checks.
215-277: LGTM!
fillOpenclawStructurednow correctly handles legacyconfig.agent.model.fallbacksvia thefallbackSourcelogic on lines 226-230. The previous review concern about legacy fallbacks not reaching the structured form has been addressed.
279-298: LGTM!
syncOpenclawStructuredFromTextproperly orchestrates parsing, filling structured/quick state, and refreshing providers/agents list.
300-348: LGTM!
getOpenclawActiveProvidersnow correctly handles legacyconfig.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!
maskProviderValueandformatProviderValuecorrectly mask sensitive fields and truncate long values.
383-429: LGTM!
collectOpenclawProvidersandrefreshOpenclawProviderscorrectly build the providers list from bothmodels.providersand rootproviders, tracking active and missing providers.
431-459: LGTM!
refreshOpenclawAgentsListcorrectly 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!
openOpenclawAddModalcorrectly initializes the editor state and loads from file with template fallback.
22-33: LGTM!
openOpenclawEditModalnow correctly loads the preset's stored content (line 27:this.openclawHasContent(existing) ? existing.content : '') and usesforce: falseto avoid overwriting with the on-disk config. The previous review concern has been addressed.
35-42: LGTM!
closeOpenclawConfigModalproperly resets all editing state including structured and quick forms.
44-78: LGTM!
loadOpenclawConfigFromFilehas proper try-catch-finally, correctly handles theforceandfallbackToTemplateoptions, and always clears the loading flag infinally.
80-104: LGTM!
persistOpenclawConfigcorrectly validates name and content, prevents duplicates when not locked, and handles the close modal option.
106-115: LGTM!
saveOpenclawConfigproperly manages the saving flag with finally block.
117-141: LGTM!
saveAndApplyOpenclawConfighas proper try-catch-finally, handles API errors, and only updates state/closes modal on success.
143-161: LGTM!
deleteOpenclawConfigcorrectly prevents deletion of the last config, uses confirmation dialog, and updates state appropriately.
163-185: LGTM!
applyOpenclawConfignow 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!
applyOpenclawStructuredToTextproperly parses withallowEmpty: trueand 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!
applyOpenclawQuickToTextproperly 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
setPrimarylogic correctly updates bothagents.defaults.model.primaryand legacyconfig.agent.modelfor 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
openclawHasContentandopenclawSubtitleare straightforward and correct.
327-333: LGTM!
saveOpenclawConfigsnow 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/catchnow present.The missing error handling flagged in a previous review has been addressed with proper
try/catchwrapping.
207-237: Past issues resolved —try/catchnow present in both methods.The missing error handling flagged in a previous review has been addressed in both
addModelandremoveModel.
5-39: LGTM — past URL validation issue addressed.The whitespace-only URL validation issue from a previous review is now fixed:
rawUrlis trimmed on line 7 before the emptiness check on line 8, andrawNameis validated both before and after trimming. Error handling withtry/catchis 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
resetConfigmethod correctly uses a loading guard withtry/catch/finallyto ensureresetConfigLoadingis always reset.
249-260: LGTM — key masking handles edge cases.The
formatKeymethod 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')andreadBundledWebUiScript()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 exactpreserveLoadingassertions 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
initErroris 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()andsessionTrashHiddenCount()onsessionTrashCountkeeps the UI aligned withsessionTrashTotalCount, 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
normalizedSourceandnormalizedPathFilter(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
Setfor 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
|
Triggering a new full review now with the following hard limits:
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 🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 12
♻️ Duplicate comments (15)
web-ui/modules/app.methods.navigation.mjs (1)
150-168:⚠️ Potential issue | 🟡 MinorTouch 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-shotpointerup/pointercancelcleanup 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 | 🟠 MajorScope the
.top-tabshide to the shell container.This selector hides every
.top-tabsinstance oncelayout-shell.cssloads, 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 | 🟠 MajorMake 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 ontitlealone for the icon button names. Use a semantic button or equivalent keyboard handling for the card, and add explicitaria-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 | 🟡 MinorKeep
.session-item-copyat the mobile tap-target floor.Lines 179-197 set 44px mobile minimums, but Lines 216-220 immediately shrink
.session-item-copyback 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 | 🟡 MinorDisabled 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 | 🟠 MajorThe 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 | 🟠 MajorIcon-only buttons need aria-label attributes for screen readers.
The edit button (line 72) and delete button (line 85) use
titleattributes, but screen readers benefit fromaria-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 | 🟡 MinorThe grid column mismatch identified in the previous review remains unfixed.
The
.top-tabsgrid is set torepeat(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 | 🟠 MajorThe keyboard accessibility issue for card actions remains unaddressed.
The
.card-actionscontainer usesopacity: 0withoutvisibility: hiddenorpointer-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 | 🟠 MajorRoot-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 whenlistDirectoryTree(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 | 🟠 MajorKeep the edit modal open until the update succeeds.
Line 169 clears
editingProviderbefore the API call finishes. Onres.erroror 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 | 🟠 MajorMake the session row keyboard-accessible.
The selectable row is a plain
divwith 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:...andcollectJavaScriptFiles(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 | 🟠 MajorMixed configs still drop legacy fallback models.
fallbackSourceprefersdefaults.model.fallbacksand only falls back toconfig.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 | 🟠 MajorTrailing-newline buffers now get an extra phantom row.
normalized.split('\n')makes"a\n"become["a", ""]and"\n"become["", ""], so the preview andoldLineCount/newLineCountare 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 unusedhasLocalAndProxy()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) triesnavigator.clipboardfirst then falls back tofallbackCopyText, whilecopyResumeCommand(line 237-248) triesfallbackCopyTextfirst thennavigator.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 !importantto 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, andremoveOpenclawToolsDenymethods don't validate thatindexis within the array bounds. Whilesplicehandles 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
spliceis 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
📒 Files selected for processing (76)
cli.jstests/e2e/run.jstests/e2e/test-web-ui-assets.jstests/unit/agents-diff-ui.test.mjstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/helpers/web-ui-source.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/run.mjstests/unit/session-tab-switch-performance.test.mjstests/unit/session-trash-state.test.mjstests/unit/skills-modal-ui.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-logic.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/index.htmlweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.mjsweb-ui/logic.runtime.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.index.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.constants.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.index.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/layout-header.htmlweb-ui/partials/index/modal-config-template-agents.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modal-openclaw-config.htmlweb-ui/partials/index/modal-skills.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-claude.htmlweb-ui/partials/index/panel-config-codex.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-market.htmlweb-ui/partials/index/panel-sessions.htmlweb-ui/partials/index/panel-settings.htmlweb-ui/source-bundle.cjsweb-ui/styles.cssweb-ui/styles/base-theme.cssweb-ui/styles/controls-forms.cssweb-ui/styles/feedback.cssweb-ui/styles/layout-shell.cssweb-ui/styles/modals-core.cssweb-ui/styles/navigation-panels.cssweb-ui/styles/openclaw-structured.cssweb-ui/styles/responsive.cssweb-ui/styles/sessions-list.cssweb-ui/styles/sessions-preview.cssweb-ui/styles/sessions-toolbar-trash.cssweb-ui/styles/skills-list.cssweb-ui/styles/skills-market.cssweb-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)
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (11)
web-ui/styles/controls-forms.css (1)
317-322:⚠️ Potential issue | 🟡 MinorPrevent 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 | 🟠 MajorAssociate 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 | 🟠 MajorMake 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 ontitleonly 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 | 🟠 MajorKeep 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, recreatingweb-ui/cannot trigger another resync, so frontend restart watching stays disabled untilcodexmate runis 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 onlypath.dirname(...)usage is the file-watch setup at Line 10024; there is no recovery branch that keeps a watcher on the parent directory whentarget/rootDiris 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 | 🟠 MajorEditing a provider URL may silently wipe the API key.
openEditModalinitializeskey: ''(line 149), andupdateProvidersends 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
keyfield 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 | 🟠 MajorMake the session row keyboard-operable.
Line 114 still binds only
@clickon a plaindiv, 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 | 🟡 MinorDon't reuse standalone-route errors in the browse empty state.
Lines 278-280 render only when
sessionStandaloneis false, sosessionStandaloneErrorcan leak a stale/sessionerror 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 | 🟠 MajorScope the
.top-tabsoverride to the shell layout.Line 154 hides every
.top-tabsinstance 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 | 🟠 MajorKeep
currentProvideron the last successful value until model loading succeeds.
providerSwitchDisplayTargetalready covers the optimistic label. Line 78 updatescurrentProviderbeforeloadModelsForProvider(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 | 🟡 MinorGuard
window.JSON5before dereferencing it.Line 4 touches
windowunconditionally. IfgetOpenclawParser()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 | 🟡 MinorStale progress-reset timer can interfere with subsequent backups.
The
setTimeoutat 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,
sourcePathreceives the result oftypeof source === 'string' ? source : String(source || ''), but this variable actually represents a file path passed toreadUtf8Texton Line 169, not source code content. The logic works correctly sincesourceis 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_REpattern on Line 6 doesn't match pure side-effect imports likeimport './side-effect.mjs';(withoutfrom). 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@supportsfor 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
📒 Files selected for processing (76)
cli.jstests/e2e/run.jstests/e2e/test-web-ui-assets.jstests/unit/agents-diff-ui.test.mjstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/helpers/web-ui-source.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/run.mjstests/unit/session-tab-switch-performance.test.mjstests/unit/session-trash-state.test.mjstests/unit/skills-modal-ui.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-logic.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/index.htmlweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.mjsweb-ui/logic.runtime.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.index.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.constants.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.index.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/layout-header.htmlweb-ui/partials/index/modal-config-template-agents.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modal-openclaw-config.htmlweb-ui/partials/index/modal-skills.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-claude.htmlweb-ui/partials/index/panel-config-codex.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-market.htmlweb-ui/partials/index/panel-sessions.htmlweb-ui/partials/index/panel-settings.htmlweb-ui/source-bundle.cjsweb-ui/styles.cssweb-ui/styles/base-theme.cssweb-ui/styles/controls-forms.cssweb-ui/styles/feedback.cssweb-ui/styles/layout-shell.cssweb-ui/styles/modals-core.cssweb-ui/styles/navigation-panels.cssweb-ui/styles/openclaw-structured.cssweb-ui/styles/responsive.cssweb-ui/styles/sessions-list.cssweb-ui/styles/sessions-preview.cssweb-ui/styles/sessions-toolbar-trash.cssweb-ui/styles/skills-list.cssweb-ui/styles/skills-market.cssweb-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.cssweb-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)
|
@coderabbitai re-review |
|
Triggering a full re-review now with the following hard limits:
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 🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
web-ui/modules/app.methods.session-actions.mjs (1)
380-387:⚠️ Potential issue | 🟡 MinorGuard 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 genericcatchtoast 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 | 🟠 MajorMask 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 | 🟠 MajorMake 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 ontitle, 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
resolveBehaviorParityBaselineReffunction returns theWEB_UI_PARITY_BASE_REFoverride directly without callinggitRefExists()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
📒 Files selected for processing (13)
.github/workflows/ci.ymltests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/web-ui-behavior-parity.test.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-sessions.htmlweb-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: 0correctly enables access to git history needed bygit show <ref>:pathandgit ls-treecommands in the parity test helpers.
28-29: VerifyHEAD^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 helperresolveBehaviorParityBaselineRef()returns the override directly without validating it exists. This could causegit 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.mjstests/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
finallyblock ensures cleanup even when the callback throws.
54-113: LGTM!The git and filesystem utilities are well-implemented. Using
-zfor 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
requestAnimationFramecorrectly usessetTimeoutfor async behavior- Cleanup is properly handled in the
finallyblock- The error thrown when
appOptionsis null provides a clear failure message
320-344: LGTM!The exported functions correctly implement the parity testing pattern:
captureCurrentBundledAppOptionsreads from the working treecaptureGitBundledAppOptionscreates an isolated fixture from git history and bundles from therecaptureBehaviorParityBaselineAppOptionsreturns the expected{ ref, options }structure that tests consumeThe fixture cleanup in the
finallyblock 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.errorguard and the single total-count update path close the earlier null-response and duplicated-branch hazards here.
|
This constraint is enforced across PRs 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 🧠 Learnings used✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 17
♻️ Duplicate comments (22)
tests/unit/run.mjs (1)
20-32:⚠️ Potential issue | 🟠 MajorPipeline 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 | 🟠 MajorMask 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-copyoverrides the 44px mobile tap-target floor.Lines 194-197 set
min-width: 44pxandmin-height: 44pxfor touch accessibility, but lines 217-220 immediately shrink.session-item-copyto 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 | 🟡 MinorScheme-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 | 🟠 MajorConfig 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 ontitleattributes, which screen readers don't consistently announce for icon-only buttons.Consider adding
tabindex="0"and@keydown.enter/@keydown.spacehandlers to the card div, or converting it to a<button>. Addaria-labelattributes 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 | 🟡 MinorGrid column count may not accommodate all tabs.
The
.top-tabsgrid usesrepeat(4, 1fr)but if 6 tabs exist inlayout-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 | 🟠 MajorOutside
.mode-cards, the hidden action buttons are still tabbable.The default hidden state only changes
opacityandtransform, 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 | 🟠 MajorClaude 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 ontitle, which is not a reliable accessible name. Please add focus + Enter/Space support on the card andaria-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 | 🟠 MajorThe new tab navigation needs a real keyboard path.
Only the active item is tabbable, but this template wires the nav with
@pointerdown/@clickonly. 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 ARIAtablistfor 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 | 🟠 MajorProvider switching here is still mouse-only.
The selectable card is a clickable
divwith no focus target or key handler, so keyboard users cannot switch providers from this list. The icon-only action buttons also still depend ontitlefor naming. Please mirror the existing click behavior on Enter/Space and addaria-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 | 🟠 MajorWatcher 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 whentargetalready exists. After the root directory is deleted, nothing is left watching its parent, so recreatingweb-uiwill 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 | 🟡 MinorClamp non-finite latencies to
0ms.
typeof === 'number'still letsNaNandInfinitythrough, so this can renderNaNmsorInfinityms.🤖 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 | 🟠 MajorValidate 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, ornotes.txtcan 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 | 🟠 MajorKeep the unload warning active while a save is running.
handleBeforeUnload()relies onhasPendingAgentsDraft(), and this helper returnsfalseas soon asagentsSavingflips 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 | 🟠 MajorDon't short-circuit external-credential configs before
applyClaudeConfig().Both guards only check
apiKey, so profiles backed byexternalCredentialTypecan save model edits or click “保存并应用” without ever reaching the existing external-auth path inapplyClaudeConfig(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 | 🟡 MinorDon't show standalone-route errors in the normal sessions pane.
This branch only renders in browse mode, so reading
sessionStandaloneErrorhere 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 | 🟠 MajorDrop 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 andoldLineCount/newLineCountoff 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 | 🟠 MajorStale failing model requests still clobber the latest selection.
The success path checks
isLatestRequest(), but thecatchblocks 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 | 🟠 MajorFailed provider loads still leave
currentProviderswitched.Line 78 updates
currentProviderbeforeloadModelsForProvider(name)finishes. When that load ends inmodelsSource === '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 andcurrentModelmay 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 | 🟡 MinorPrevious 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 | 🟡 MinorTouch cancellations still leave transient nav state behind.
Lines 157-165 and Lines 177-185 apply
nav-intent-*classes andsession-panel-fast-hiddenbefore 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 | 🟠 MajorThis 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-tabsloses 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-okand.tone-erroruse CSS variables (--color-success,--color-error), but.tone-warnhardcodes#8d5b31. Consider defining a--color-warnvariable 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
📒 Files selected for processing (77)
.github/workflows/ci.ymlcli.jstests/e2e/run.jstests/e2e/test-web-ui-assets.jstests/unit/agents-diff-ui.test.mjstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/helpers/web-ui-source.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/run.mjstests/unit/session-tab-switch-performance.test.mjstests/unit/session-trash-state.test.mjstests/unit/skills-modal-ui.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-logic.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/index.htmlweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.mjsweb-ui/logic.runtime.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.index.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.constants.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.index.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/layout-header.htmlweb-ui/partials/index/modal-config-template-agents.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modal-openclaw-config.htmlweb-ui/partials/index/modal-skills.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-claude.htmlweb-ui/partials/index/panel-config-codex.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-market.htmlweb-ui/partials/index/panel-sessions.htmlweb-ui/partials/index/panel-settings.htmlweb-ui/source-bundle.cjsweb-ui/styles.cssweb-ui/styles/base-theme.cssweb-ui/styles/controls-forms.cssweb-ui/styles/feedback.cssweb-ui/styles/layout-shell.cssweb-ui/styles/modals-core.cssweb-ui/styles/navigation-panels.cssweb-ui/styles/openclaw-structured.cssweb-ui/styles/responsive.cssweb-ui/styles/sessions-list.cssweb-ui/styles/sessions-preview.cssweb-ui/styles/sessions-toolbar-trash.cssweb-ui/styles/skills-list.cssweb-ui/styles/skills-market.cssweb-ui/styles/titles-cards.css
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (33)
web-ui/modules/api.mjs (1)
15-18:⚠️ Potential issue | 🟡 MinorMake
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 | 🟡 MinorVerify the hard-coded four-column tab grid still matches the header.
Line 263 still fixes
.top-tabsat four columns. Iflayout-header.htmlstill 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 | 🟡 MinorHandle
/web-ui/the same as/web-ui.A normal trailing-slash visit falls into the asset branch,
relativePathbecomes'', 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 | 🟡 MinorInclude the new
.cjshelper in restart detection.
web-ui/source-bundle.cjsis 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 | 🟠 MajorCatch bundled reader failures and return
500instead 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); NODEExpected:
UNCAUGHT bundled reader failed, which is why these branches need localtry/catchand a500response.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 fallbackreadBundledWebUiHtml(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 | 🟠 MajorRoot-directory recreation can still disable restart watching until this process is restarted.
When a watched root disappears, the recursive path drops the watcher if
targetno longer exists, and the plain-tree fallback closes every nested watcher whenlistDirectoryTree(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); NODEExpected: 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 | 🟠 MajorLegacy fallback entries still disappear once
defaults.model.fallbacksexists.Line 226 picks either
defaults.model.fallbacksorlegacyAgent.model.fallbacks, never both. If a config carries legacy-only fallback entries alongside the newer defaults block, those entries never reachagentFallbacks, 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
0still collapses to “empty” here.Line 496 uses
String(value || ''), so numeric0becomes''and this helper returnsnullinstead of0. 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 | 🟠 MajorSpan the full row instead of hard-coding two tracks.
Line 18 still uses
grid-column: span 2. Once the responsive layout collapses.session-toolbarto 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 | 🟠 MajorKeep the edit state until the provider update actually succeeds.
Line 169 closes the modal before the request returns, so any
res.erroror thrown request drops the user's draft. Line 171 also still submitskeyeven when the edit form left it untouched; if the currentupdate-providerhandler 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 omitkeywhen untouched). Any unconditional write on emptykeymeans 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 | 🟡 MinorCoerce the standalone URL base before interpolating it.
When
window.location.originis unusable andapiBaseisundefined, Line 94 buildsundefined/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 | 🟠 MajorKeep 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(), orremoveSessionFromCurrentList()still falls into the outercatch, 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 | 🟠 MajorPoint 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 makesWEB_UI_PARITY_BASE_REFdrift 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 | 🟡 MinorDon't override the 44px tap-target floor here.
This block resets
.session-item-copyback to20x20after the same media query already raised it to44px, 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 | 🟠 MajorTreat 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 | 🟡 MinorMake the JSON5 editor read-only during writes.
Users can still type while
openclawSavingoropenclawApplyingis 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 | 🟠 MajorRestore 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 ontitle, 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 | 🟠 MajorKeep hidden card actions out of the keyboard path.
opacity: 0only 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 | 🟠 MajorDon'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 | 🟠 MajorVerify both modal close handlers reject in-flight dismissals.
The overlays still call
closeConfigTemplateModal/closeAgentsModaldirectly, while the AGENTS footer already blocks cancellation duringagentsSaving || agentsDiffLoading. Please confirm those close handlers also bail out onconfigTemplateApplying,agentsSaving, andagentsDiffLoading; 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/modulesAlso 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 | 🟡 MinorFreeze
AGENTS.mdedits during save.The textarea is readonly only while loading. During
agentsSaving, users can still changeagentsContent, 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 | 🟡 MinorLock the config template editor while apply is running.
Users can keep editing
configTemplateContentafterapplyConfigTemplatestarts, 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 | 🟡 MinorKeep 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 | 🟠 MajorProvider 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 | 🟡 MinorDon't expose the side-rail shortcuts as a second tab widget.
panel-marketandpanel-settingsare already labelled bytab-market/tab-settings. Keepingrole="tab"andaria-controlson 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 | 🟠 MajorAvoid a third-party font fetch in the local Web UI.
This
@importmakes 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 | 🟠 MajorKeep “导入选中” 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 | 🟠 MajorExpose 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 | 🟡 MinorClamp non-finite latency values before rendering.
typeof === 'number'still allowsNaNandInfinity, which renders asNaNms/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 | 🟠 MajorDon't hide the preview header classes.
This selector still removes
.session-preview-titleand.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 | 🟠 MajorDon't short-circuit externally-authenticated profiles.
Both branches return on missing
apiKeybefore the existing external-credential flow can run, so profiles backed byexternalCredentialTypesave 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 | 🟠 MajorUnsequenced modal loads can still clobber the current draft.
Both
openOpenclawAddModalandopenOpenclawEditModalfire-and-forgetloadOpenclawConfigFromFile()usingvoid. If the user quickly opens a different config or starts typing before the async load completes, lines 62-66 will overwriteopenclawEditing.contentwith 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 | 🟠 MajorPersistence 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:scheduleAfterFramecallback is invoked synchronously on fallback path.When
requestAnimationFrameis unavailable,setTimeout(callback, 16)is used. However, the callback receives no timestamp argument, which differs fromrequestAnimationFrame'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
_pendingCodexApplyOptionsis set during an in-flight apply, lines 428-430 recursively callapplyCodexConfigDirect. 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 theLARGE_DIFF_LINE_LIMITcheck 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
📒 Files selected for processing (77)
.github/workflows/ci.ymlcli.jstests/e2e/run.jstests/e2e/test-web-ui-assets.jstests/unit/agents-diff-ui.test.mjstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/helpers/web-ui-app-options.mjstests/unit/helpers/web-ui-source.mjstests/unit/provider-share-command.test.mjstests/unit/provider-switch-regression.test.mjstests/unit/run.mjstests/unit/session-tab-switch-performance.test.mjstests/unit/session-trash-state.test.mjstests/unit/skills-modal-ui.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-logic.test.mjstests/unit/web-ui-restart.test.mjstests/unit/web-ui-source-bundle.test.mjsweb-ui/app.jsweb-ui/index.htmlweb-ui/logic.agents-diff.mjsweb-ui/logic.claude.mjsweb-ui/logic.mjsweb-ui/logic.runtime.mjsweb-ui/logic.sessions.mjsweb-ui/modules/api.mjsweb-ui/modules/app.computed.dashboard.mjsweb-ui/modules/app.computed.index.mjsweb-ui/modules/app.computed.session.mjsweb-ui/modules/app.constants.mjsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.codex-config.mjsweb-ui/modules/app.methods.index.mjsweb-ui/modules/app.methods.install.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-core.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.openclaw-persist.mjsweb-ui/modules/app.methods.providers.mjsweb-ui/modules/app.methods.runtime.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-timeline.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/partials/index/layout-footer.htmlweb-ui/partials/index/layout-header.htmlweb-ui/partials/index/modal-config-template-agents.htmlweb-ui/partials/index/modal-confirm-toast.htmlweb-ui/partials/index/modal-openclaw-config.htmlweb-ui/partials/index/modal-skills.htmlweb-ui/partials/index/modals-basic.htmlweb-ui/partials/index/panel-config-claude.htmlweb-ui/partials/index/panel-config-codex.htmlweb-ui/partials/index/panel-config-openclaw.htmlweb-ui/partials/index/panel-market.htmlweb-ui/partials/index/panel-sessions.htmlweb-ui/partials/index/panel-settings.htmlweb-ui/source-bundle.cjsweb-ui/styles.cssweb-ui/styles/base-theme.cssweb-ui/styles/controls-forms.cssweb-ui/styles/feedback.cssweb-ui/styles/layout-shell.cssweb-ui/styles/modals-core.cssweb-ui/styles/navigation-panels.cssweb-ui/styles/openclaw-structured.cssweb-ui/styles/responsive.cssweb-ui/styles/sessions-list.cssweb-ui/styles/sessions-preview.cssweb-ui/styles/sessions-toolbar-trash.cssweb-ui/styles/skills-list.cssweb-ui/styles/skills-market.cssweb-ui/styles/titles-cards.css
Forced merge by repository maintainer.
|
🧠 Learnings used✅ Actions performedFull review triggered. |
Summary
Tests
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Tests