fix(web-ui): persist preferences and refine sidebar collapse#211
Conversation
📝 WalkthroughWalkthroughThe PR moves Web UI preferences to server-backed persistence with legacy migration, updates navigation to treat disabled tabs as non-selectable, and adds a collapsible sidebar with disabled orchestration rendering, icons, and matching styling/localization. ChangesWeb UI Preferences Persistence & Sidebar/Orchestration UI Refactor
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant AppJS as web-ui/app.js
participant Prefs as app.methods.web-ui-preferences.mjs
participant Storage as localStorage
participant API as backend API
AppJS->>Prefs: loadWebUiPreferences()
Prefs->>API: get-web-ui-preferences
Prefs->>Storage: collect legacy preference keys
Prefs->>Prefs: merge and normalize snapshot
Prefs->>AppJS: applyWebUiPreferences(merged)
Prefs->>API: set-web-ui-preferences
AppJS->>Prefs: persistWebUiPreferences(overrides)
AppJS->>Prefs: flushWebUiPreferences()
Prefs->>API: set-web-ui-preferences (pending overrides)
sequenceDiagram
participant User
participant NavHandlers as onMainTabPointerDown/onMainTabClick
participant NavLogic as switchMainTab
participant Prefs as persistNavState
User->>NavHandlers: interact with main tab
NavHandlers->>NavLogic: check tab selectability
alt tab disabled
NavHandlers-->>User: cancel event
else tab selectable
NavHandlers->>NavLogic: switchMainTab()
NavLogic->>Prefs: persistNavState()
end
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web-ui/modules/app.methods.session-browser.mjs (1)
248-287: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winWait for web UI preferences before restoring session filters.
loadWebUiPreferences()is async andrestoreSessionFilterCache()runs immediately after the fire-and-forget call, so startup can read the defaultsessionFilterSource/sessionPathFilterinstead of the persisted values and skip the needed reload. Call restore after the preferences promise resolves or chain it fromloadWebUiPreferences().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-ui/modules/app.methods.session-browser.mjs` around lines 248 - 287, The session filter restore runs too early because `restoreSessionFilterCache()` is invoked before `loadWebUiPreferences()` finishes, so persisted `sessionFilterSource` and `sessionPathFilter` may not be available. Update the startup flow in `app.methods.session-browser.mjs` so restoration happens only after the async preferences load resolves, either by awaiting `loadWebUiPreferences()` or chaining the restore call from it. Keep the reload logic in `restoreSessionFilterCache()` and the `loadWebUiPreferences()` entry point aligned so `loadSessions()` sees the restored state.
🧹 Nitpick comments (1)
web-ui/modules/app.methods.navigation.mjs (1)
436-439: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueRedundant ternary — both branches are identical.
targetTab === 'config' ? this.configMode : this.configModealways evaluates tothis.configMode, so the condition has no effect. Either simplify toconfigMode: this.configMode, or if a different value was intended for non-configtargets (e.g., a cleared/default mode), fix the branch. Please confirm the intended behavior.♻️ Simplify (if no distinct non-config value was intended)
persistNavState(this, { mainTab: targetTab, - configMode: targetTab === 'config' ? this.configMode : this.configMode + configMode: this.configMode });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-ui/modules/app.methods.navigation.mjs` around lines 436 - 439, The persistNavState call in app.methods.navigation.mjs uses a redundant ternary for configMode, since both branches return this.configMode and the condition has no effect. Update the navigation logic around persistNavState to either pass configMode: this.configMode directly, or if mainTab targetTab was meant to change the value for non-config tabs, replace the identical branch with the intended alternate mode. Use targetTab and configMode in this method to confirm the behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web-ui/modules/app.methods.openclaw-editing.mjs`:
- Around line 362-367: saveOpenclawConfigs() is always returning success, which
prevents persistOpenclawConfig() from detecting a failed preference write and
rolling back the edit. Update saveOpenclawConfigs() in
app.methods.openclaw-editing.mjs to surface the real result of
persistWebUiPreferences instead of unconditionally returning true, and make
persistWebUiPreferences / set-web-ui-preferences propagate or expose write
failures so the caller can react. Keep the behavior aligned with
persistOpenclawConfig() by ensuring a failed OpenClaw config save returns a
failure signal rather than a success value.
---
Outside diff comments:
In `@web-ui/modules/app.methods.session-browser.mjs`:
- Around line 248-287: The session filter restore runs too early because
`restoreSessionFilterCache()` is invoked before `loadWebUiPreferences()`
finishes, so persisted `sessionFilterSource` and `sessionPathFilter` may not be
available. Update the startup flow in `app.methods.session-browser.mjs` so
restoration happens only after the async preferences load resolves, either by
awaiting `loadWebUiPreferences()` or chaining the restore call from it. Keep the
reload logic in `restoreSessionFilterCache()` and the `loadWebUiPreferences()`
entry point aligned so `loadSessions()` sees the restored state.
---
Nitpick comments:
In `@web-ui/modules/app.methods.navigation.mjs`:
- Around line 436-439: The persistNavState call in app.methods.navigation.mjs
uses a redundant ternary for configMode, since both branches return
this.configMode and the condition has no effect. Update the navigation logic
around persistNavState to either pass configMode: this.configMode directly, or
if mainTab targetTab was meant to change the value for non-config tabs, replace
the identical branch with the intended alternate mode. Use targetTab and
configMode in this method to confirm the behavior.
🪄 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: d5a82ec0-dbff-4829-958d-7820539ee382
📒 Files selected for processing (34)
cli.jstests/e2e/test-web-ui-session-tab.jstests/unit/claude-settings-sync.test.mjstests/unit/compact-layout-ui.test.mjstests/unit/config-tabs-ui.test.mjstests/unit/session-browser-timeline-regression.test.mjstests/unit/session-trash-state.test.mjstests/unit/startup-claude-star-prompt.test.mjstests/unit/web-ui-behavior-parity.test.mjstests/unit/web-ui-preferences.test.mjstests/unit/web-ui-runtime-navigation-regression.test.mjsweb-ui/app.jsweb-ui/modules/app.methods.agents.mjsweb-ui/modules/app.methods.claude-config.mjsweb-ui/modules/app.methods.navigation.mjsweb-ui/modules/app.methods.openclaw-editing.mjsweb-ui/modules/app.methods.session-actions.mjsweb-ui/modules/app.methods.session-browser.mjsweb-ui/modules/app.methods.session-trash.mjsweb-ui/modules/app.methods.startup-claude.mjsweb-ui/modules/app.methods.tool-config-permissions.mjsweb-ui/modules/app.methods.web-ui-preferences.mjsweb-ui/modules/config-template-confirm-pref.mjsweb-ui/modules/i18n.mjsweb-ui/modules/i18n/locales/en.mjsweb-ui/modules/i18n/locales/ja.mjsweb-ui/modules/i18n/locales/vi.mjsweb-ui/modules/i18n/locales/zh-tw.mjsweb-ui/modules/i18n/locales/zh.mjsweb-ui/modules/sessions-filters-url.mjsweb-ui/partials/index/layout-header.htmlweb-ui/res/web-ui-render.precompiled.jsweb-ui/styles/layout-shell.cssweb-ui/styles/navigation-panels.css
💤 Files with no reviewable changes (2)
- web-ui/modules/app.methods.session-trash.mjs
- web-ui/modules/app.methods.tool-config-permissions.mjs
📜 Review details
🧰 Additional context used
🪛 Stylelint (17.14.0)
web-ui/styles/layout-shell.css
[error] 407-407: Expected "currentColor" to be "currentcolor" (value-keyword-case)
(value-keyword-case)
🔇 Additional comments (49)
cli.js (2)
1003-1005: LGTM!Also applies to: 1013-1060, 1074-1085, 1114-1116
1007-1011: 🎯 Functional CorrectnessNo missing session sources The supported values are
all,codex,claude,gemini, andcodebuddy; invalid values are normalized back before persistence, so this whitelist matches the UI.> Likely an incorrect or invalid review comment.web-ui/modules/app.methods.web-ui-preferences.mjs (1)
1-273: LGTM!Also applies to: 274-339, 341-446, 448-506
tests/unit/web-ui-preferences.test.mjs (1)
18-114: LGTM!Also applies to: 126-177, 204-216
web-ui/app.js (1)
511-535: LGTM!Also applies to: 547-583, 700-747
web-ui/modules/app.methods.agents.mjs (1)
356-368: LGTM!tests/e2e/test-web-ui-session-tab.js (1)
91-94: LGTM!Also applies to: 136-137, 171-218
tests/unit/config-tabs-ui.test.mjs (1)
74-92: LGTM!tests/unit/web-ui-runtime-navigation-regression.test.mjs (1)
340-358: LGTM!tests/unit/web-ui-behavior-parity.test.mjs (1)
487-493: LGTM!Also applies to: 575-576, 793-801
web-ui/partials/index/layout-header.html (4)
125-135: 🩺 Stability & AvailabilityVerify collapse toggle behavior in compact/mobile layout.
The 64px collapse toggle is anchored to
.side-rail's edge via absolute positioning. Confirm the toggle doesn't visually conflict or become inaccessible underbody.force-compact/mobile layouts where the side rail's presentation changes.
167-167: LGTM!Also applies to: 184-184, 202-202, 217-217, 232-232, 247-247, 266-266, 280-280, 297-297, 311-311, 347-347, 365-365, 383-383, 396-396
322-335: LGTM on the disabled-state wiring for the sidebar entry (aria-disabled, disabled, static class, rebuilding copy).
66-74: 🎯 Functional CorrectnessIncorrect: orchestration is gated consistently.
taskOrchestrationTabEnableddefaults totrue, and both the top tab and sidebar orchestration entry use the samev-if, so there isn’t a top-tab-only visibility mismatch to fix.> Likely an incorrect or invalid review comment.web-ui/res/web-ui-render.precompiled.js (1)
106-115: LGTM! This precompiled output correctly mirrors the disabled-tab and sidebar-collapse markup added tolayout-header.html. (Note: the orchestration top-tab visibility gating issue flagged in that file's review will also need regenerating this precompiled output once fixed.)Also applies to: 181-197, 240-565
web-ui/styles/layout-shell.css (2)
397-408: 📐 Maintainability & Code Quality | 💤 Low valueStylelint: use lowercase
currentcolor.🔧 Suggested fix
.side-item-icon { display: none; width: 28px; height: 28px; border-radius: 10px; align-items: center; justify-content: center; font-size: 15px; font-weight: 700; line-height: 1; - color: currentColor; + color: currentcolor; }Source: Linters/SAST tools
22-99: LGTM on the collapsed-width variable, transitions, disabled-state visuals, and collapsed-mode overrides.Also applies to: 428-438, 503-560
web-ui/modules/i18n/locales/zh.mjs (1)
150-151: LGTM!Also applies to: 962-964
tests/unit/compact-layout-ui.test.mjs (1)
56-61: LGTM! Assertions correctly reflect the new--side-rail-width/--side-rail-collapsed-widthCSS variable contract.web-ui/styles/navigation-panels.css (1)
308-318: LGTM!web-ui/modules/i18n/locales/en.mjs (1)
150-151: LGTM!Also applies to: 963-965
web-ui/modules/i18n/locales/ja.mjs (1)
151-152: LGTM!Also applies to: 952-954
web-ui/modules/i18n/locales/vi.mjs (1)
111-112: LGTM!Also applies to: 1099-1101
web-ui/modules/i18n/locales/zh-tw.mjs (1)
150-151: LGTM!Also applies to: 962-964
web-ui/modules/app.methods.claude-config.mjs (5)
91-97: LGTM!
99-126: Solid sanitization/normalization helper.Clears masked keys and normalizes
targetApialiases correctly;changedflag logic is accurate.
155-182: LGTM!
263-274: LGTM!
497-499: LGTM!Also applies to: 613-615
web-ui/modules/app.methods.startup-claude.mjs (4)
17-27: LGTM!
267-267: LGTM!
370-419: 🗄️ Data Integrity & Integration | 💤 Low valueRedundant intermediate persist with stale value in the imported-config branch.
In the imported-name branch,
ensureClaudeConfigFromSettingsinternally callssaveClaudeConfigs()(persisting the oldcurrentClaudeConfig) before this code updatesthis.currentClaudeConfig = importedNameand persists again with the correct value. Functionally this self-corrects because the final persist call runs immediately after, but it produces an extra network round-trip and relies on the preferences layer applying writes in order/last-write-wins. Worth confirming that ordering guarantee holds in the persistence module.
602-608: LGTM!tests/unit/claude-settings-sync.test.mjs (5)
626-675: LGTM!
696-717: LGTM!
1525-1581: 📐 Maintainability & Code Quality | 💤 Low valueSecret-leak assertion is vacuous.
assert.doesNotMatch(persistedJson, /sk-secret/)will always pass since none of the test fixtures contain that string — the test data'sapiKeyis already empty. This doesn't actually verify that a real (unmasked) secret is excluded from persisted preferences.
1582-1639: LGTM!
1668-1704: LGTM!tests/unit/startup-claude-star-prompt.test.mjs (1)
10-55: LGTM!web-ui/modules/i18n.mjs (2)
52-56: 🎯 Functional Correctness | ⚡ Quick winVerify document language stays in sync after async preference load.
initI18n()now only normalizes/applies whateverthis.langcurrently holds instead of reading a persisted value itself; it relies entirely on an upstream layer settingthis.langfrom loaded Web UI preferences. If that upstream flow setsthis.langdirectly (e.g., viaapplyWebUiPreferences) without re-invokingapplyDocumentLanguage/initI18n, the<html lang>/dirattributes could remain stale relative to the restored language after the preferences fetch resolves.
88-95: LGTM!web-ui/modules/sessions-filters-url.mjs (1)
136-137: LGTM!web-ui/modules/app.methods.session-actions.mjs (1)
2-2: LGTM!Also applies to: 301-306, 309-314, 317-322, 330-335
web-ui/modules/app.methods.session-browser.mjs (3)
16-50: LGTM!
342-353: LGTM!Also applies to: 771-777
289-311: 🗄️ Data Integrity & IntegrationNo change needed for
onSessionSortChange
persistWebUiPreferences()deep-mergessessionFilters, so{ sortMode: normalized }updates only that field and preservessource,pathFilter,query,roleFilter, andtimePreset.> Likely an incorrect or invalid review comment.tests/unit/session-trash-state.test.mjs (1)
2107-2115: LGTM!Also applies to: 2396-2417
tests/unit/session-browser-timeline-regression.test.mjs (1)
98-126: LGTM!web-ui/modules/config-template-confirm-pref.mjs (1)
12-22: 📐 Maintainability & Code QualityDrop this concern No current call sites rely on the implicit
localStoragefallback.> Likely an incorrect or invalid review comment.
| saveOpenclawConfigs() { | ||
| try { | ||
| localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs)); | ||
| return true; | ||
| } catch (_) { | ||
| this.showMessage('保存本地 OpenClaw 配置失败', 'error'); | ||
| return false; | ||
| if (typeof this.persistWebUiPreferences === 'function') { | ||
| this.persistWebUiPreferences({ openclawConfigs: this.openclawConfigs }); | ||
| } | ||
| return true; | ||
| }, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n 'saveOpenclawConfigs' -B2 -A6 web-uiRepository: SakuraByteCore/codexmate
Length of output: 3356
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the persistence helper and related call sites.
sed -n '1,220p' web-ui/modules/app.methods.openclaw-persist.mjs
printf '\n---\n'
sed -n '1,220p' web-ui/modules/app.methods.web-ui-preferences.mjs
printf '\n---\n'
sed -n '220,380p' web-ui/modules/app.methods.openclaw-persist.mjs
printf '\n---\n'
sed -n '340,380p' web-ui/modules/app.methods.openclaw-editing.mjs
# Find any callers that depend on a boolean return.
printf '\n=== boolean-return callers ===\n'
rg -n 'saveOpenclawConfigs\(\)\s*===\s*false|saveOpenclawConfigs\(\)\s*===\s*true|if\s*\(\s*this\.saveOpenclawConfigs\(\)\s*\)' web-uiRepository: SakuraByteCore/codexmate
Length of output: 27676
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the web UI preferences persistence implementation and its error handling.
rg -n 'persistWebUiPreferences|flushWebUiPreferences|set-web-ui-preferences|catch\(\(\)\s*=>\s*\{\}\)' web-ui/modules/app.methods.web-ui-preferences.mjs web-ui/modules -A6 -B6
printf '\n--- targeted slice ---\n'
sed -n '220,380p' web-ui/modules/app.methods.web-ui-preferences.mjsRepository: SakuraByteCore/codexmate
Length of output: 50380
Restore a failure signal for OpenClaw saves saveOpenclawConfigs() now always returns true, so persistOpenclawConfig() can no longer roll back an edit when the preference write fails. The persistence path is fire-and-forget (set-web-ui-preferences is dispatched without awaiting and swallows rejections), so a save error leaves the UI showing success while the new OpenClaw config never reaches storage.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web-ui/modules/app.methods.openclaw-editing.mjs` around lines 362 - 367,
saveOpenclawConfigs() is always returning success, which prevents
persistOpenclawConfig() from detecting a failed preference write and rolling
back the edit. Update saveOpenclawConfigs() in app.methods.openclaw-editing.mjs
to surface the real result of persistWebUiPreferences instead of unconditionally
returning true, and make persistWebUiPreferences / set-web-ui-preferences
propagate or expose write failures so the caller can react. Keep the behavior
aligned with persistOpenclawConfig() by ensuring a failed OpenClaw config save
returns a failure signal rather than a success value.
|
Updated this PR with the collapsed-sidebar follow-up:
Validation run after the update:
Note: CodeRabbit was not checked or requested in this pass. |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
web-ui/styles/layout-shell.css (1)
547-554: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMissing overflow containment for collapsed section title.
white-space: nowrapwith nooverflow/text-overflowfallback risks the section title spilling past the 136px collapsed rail for longer localized strings (ja/vi/zh-tw labels). The sibling.language-settings-linkrule already usesoverflow: hidden; text-overflow: ellipsisfor the same scenario — worth mirroring here.💄 Proposed fix
.sidebar-collapsed .side-section-title { display: block; padding: 0 10px; margin: 4px 0 5px; text-align: left; white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web-ui/styles/layout-shell.css` around lines 547 - 554, The .sidebar-collapsed .side-section-title rule needs overflow containment for long localized labels. Update the side-section title styling in layout-shell.css to mirror the existing .language-settings-link behavior by adding clipping and ellipsis handling so nowrap text stays within the collapsed rail. Keep the fix scoped to the .sidebar-collapsed .side-section-title selector and preserve the current spacing/alignment while preventing spillover.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@web-ui/styles/layout-shell.css`:
- Around line 547-554: The .sidebar-collapsed .side-section-title rule needs
overflow containment for long localized labels. Update the side-section title
styling in layout-shell.css to mirror the existing .language-settings-link
behavior by adding clipping and ellipsis handling so nowrap text stays within
the collapsed rail. Keep the fix scoped to the .sidebar-collapsed
.side-section-title selector and preserve the current spacing/alignment while
preventing spillover.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 30baa4a4-de37-4773-8e6f-30f7e6d899cb
📒 Files selected for processing (2)
tests/unit/compact-layout-ui.test.mjsweb-ui/styles/layout-shell.css
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/unit/compact-layout-ui.test.mjs
📜 Review details
🔇 Additional comments (1)
web-ui/styles/layout-shell.css (1)
519-526: LGTM!Also applies to: 567-580
|
Completed a stricter local review/fix pass for the collapsed sidebar:
Validation run after the fix:
Note: CodeRabbit was not checked or requested in this pass. |
|
Fixed the collapsed-sidebar top brand regression:
Validation run after the fix:
Note: CodeRabbit was not checked or requested in this pass. |
|
Updated the PR for
Validation run after the version bump:
Note: CodeRabbit was not checked or requested in this pass. |
Summary
localStorageto the existing backendpreferences.jsonAPI.localStoragekeys for navigation, sidebar, session filters/trash, language, config selections, Claude/OpenClaw config, tool permissions, and related Web UI state.?tab=orchestrationor restored orchestration state.OpenClawhas more than 15px extra horizontal room.0.0.57and push annotated git tagv0.0.57to the current PR head commit.Validation
0.0.56/v0.0.56strings outside git metadata;0.0.57appears inpackage.jsonandpackage-lock.json.v0.0.57resolves to PR head commit3cadaca2d53493ada9c51ef9929cd87c197c7edb.git diff --checknpm run lint([codexmate] Lint passed for 593 file(s).)npm run test:unit(All 742 tests passed.)npm run test:e2enode -e "require('./tests/e2e/test-web-ui-session-tab.js')().then(()=>console.log('focused test passed')).catch((e)=>{console.error(e); process.exit(1);})"(focused test passed)?tab=orchestrationfalls back to dashboard.aria-disabled="true",disabled, andtabindex="-1".localStoragekeys are cleared.preferences.json.OpenClawtext measured 58px, item width 119px, leaving 61px extra horizontal room.Codex Mate, and version/update copy are visible above Doctor.语言:中文, measured 75px wide in the manual check).Trashhas 66px clearance from the language footer in the collapsed-bottom check.targetApialiases are normalized before being persisted.Screenshot evidence
Sent in the WeChat task thread:
/home/moltbot/clawd-wechat/tmp/codexmate-web-shots/orchestration-fallback-migrated-collapsed.png/home/moltbot/clawd-wechat/tmp/codexmate-web-shots/sidebar-expanded-after-toggle.png/home/moltbot/clawd-wechat/tmp/codexmate-web-shots/sidebar-collapsed-after-toggle.png/home/moltbot/clawd-wechat/tmp/codexmate-text-collapse-shots/collapsed-full-text.png/home/moltbot/clawd-wechat/tmp/codexmate-text-collapse-shots/expanded-after-text-collapse.png/home/moltbot/clawd-wechat/tmp/codexmate-collapse-section-language-shots/collapsed-section-titles-language-top.png/home/moltbot/clawd-wechat/tmp/codexmate-collapse-section-language-shots/collapsed-section-titles-language-bottom.png/home/moltbot/clawd-wechat/tmp/codexmate-collapse-brand-shots/collapsed-brand-top.png