Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
564d27b
feat(web-ui): enable claude share command button
ymkiux May 17, 2026
a9e567a
feat(web-ui): move version to brand header and simplify layout
ymkiux May 17, 2026
7e13a4e
fix(web-ui): make language switcher always visible at sidebar bottom
ymkiux May 17, 2026
2701016
fix(web-ui): fix language switcher visibility at sidebar bottom
ymkiux May 17, 2026
ae446ef
feat(web-ui): add context window utilization warning for sessions
ymkiux May 17, 2026
12b3038
test: update test to match enabled share command button
ymkiux May 17, 2026
b839fc4
test: allow sessionContextUtilization in parity test
ymkiux May 17, 2026
3d896b2
fix(search): CJK token matching + E2E stability fixes
ymkiux May 17, 2026
4ca5164
feat(web-ui): Codex provider UI improvements + edit key eye toggle
ymkiux May 18, 2026
707462f
feat(web-ui): add eye toggle for Claude edit config API key
ymkiux May 18, 2026
cde92c3
test(web-ui): update parity and share command tests for provider UI c…
ymkiux May 18, 2026
c746613
feat(claude): add local bridge load balancing for Claude providers
ymkiux May 18, 2026
9c3e6c9
fix(claude): add local provider card to Claude config panel
ymkiux May 18, 2026
55322e5
feat(cli): add Windows system tray icon for running indicator
ymkiux May 18, 2026
db99a4f
fix(claude): remove provider health check section from config panel
ymkiux May 18, 2026
2bac697
fix(dashboard): remove providers health section from Doctor panel
ymkiux May 18, 2026
3ea1e0d
fix(claude): remove provider health check section from Claude config …
ymkiux May 18, 2026
b43af44
fix(claude): remove health check results display, keep button and hint
ymkiux May 18, 2026
82123ea
fix(codex): remove health check results display, keep button and hint
ymkiux May 18, 2026
dec373a
feat(cli): add update command
ymkiux May 18, 2026
58999a5
fix(tray): fix pid variable bug and improve robustness
ymkiux May 18, 2026
75ad600
fix(tray): fix powershell heredoc indentation syntax error
ymkiux May 18, 2026
e3854bf
fix(tray): improve UI compatibility and add fallback icon
ymkiux May 18, 2026
3f54c4b
refactor(tray): use pure powershell with STA mode and hidden form for…
ymkiux May 18, 2026
f9011e0
refactor(cli): remove experimental windows tray icon feature
ymkiux May 18, 2026
6a5b08f
docs(readme): modernize style and fix consistency tests
ymkiux May 18, 2026
655b04e
docs(readme): remove sub-projects and ecosystem section
ymkiux May 18, 2026
32489f5
docs(readme): fix heading spacing and remove sub-projects section
ymkiux May 18, 2026
5b4ee20
chore: bump version to 0.0.33
ymkiux May 18, 2026
dd28a6b
docs(readme): restore missing badges with flat-square style
ymkiux May 18, 2026
52d577f
docs(readme): add Live Agent Sync to progress table
ymkiux May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/cli-sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,9 @@ function matchTokensInText(text, tokens, mode) {
return false;
}
var boundaryChars = "\\s_\\-.,;:!?(){}[\\]<>/\"'`";
var hasCjk = function(token) { return /[⺀-鿿\u{20000}-\u{2fa1f}]/u.test(token); };
var matchToken = function(token) {
if (/^\d+$/.test(token)) {
if (/^\d+$/.test(token) || hasCjk(token)) {
return haystack.indexOf(token) !== -1;
}
var escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/test-install-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { assert } = require('./helpers');

module.exports = async function testInstallStatus(ctx) {
const { api } = ctx;
const report = await api('install-status');
const report = await api('install-status', undefined, 10000);
assert(report && Array.isArray(report.targets), 'install-status should return targets');
const claude = report.targets.find((item) => item && item.id === 'claude') || null;
assert(claude, 'install-status should include claude target');
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(html, /<button[\s\S]*?@click="deleteProvider\(provider\.name\)"[\s\S]*?:aria-label="t\('config\.provider\.delete\.aria', \{ name: provider\.name \}\)"[\s\S]*?:title="shouldShowProviderDelete\(provider\) \? t\('common\.delete'\) : t\('common\.notDeletable'\)">/);
assert.match(html, /<button class="card-action-btn"[^>]*@click="openEditConfigModal\(name\)"[^>]*:aria-label="t\('claude\.action\.editAria', \{ name \}\)"[^>]*:title="t\('claude\.action\.edit'\)">/);
assert.match(html, /<button class="card-action-btn delete"[^>]*@click="deleteClaudeConfig\(name\)"[^>]*:aria-label="t\('claude\.action\.deleteAria', \{ name \}\)"[^>]*:title="t\('claude\.action\.delete'\)">/);
assert.match(html, /<button class="card-action-btn"[^>]*@click="copyClaudeShareCommand\(name\)"[^>]*disabled[^>]*>/);
assert.match(html, /<button class="card-action-btn"[^>]*@click="copyClaudeShareCommand\(name\)"[^>]*>/);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Strengthen this assertion to prove the button is actually enabled.

This regex still passes when disabled/:disabled is present, so it doesn’t validate the PR’s core behavior (interactive share button). Add a targeted negative check on the matched markup (and ideally assert updated aria/title bindings too).

Suggested test hardening
-    assert.match(html, /<button class="card-action-btn"[^>]*`@click`="copyClaudeShareCommand\(name\)"[^>]*>/);
+    const claudeShareButton = html.match(
+        /<button class="card-action-btn"[^>]*`@click`="copyClaudeShareCommand\(name\)"[^>]*>/
+    );
+    assert(claudeShareButton, 'Claude share button should exist');
+    assert.doesNotMatch(claudeShareButton[0], /\s(?:disabled|:disabled)=/);
+    assert.match(claudeShareButton[0], /:aria-label="t\('config\.shareCommand\.aria'\)"/);
+    assert.match(claudeShareButton[0], /:title="t\('config\.shareCommand'\)"/);
📝 Committable suggestion

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

Suggested change
assert.match(html, /<button class="card-action-btn"[^>]*@click="copyClaudeShareCommand\(name\)"[^>]*>/);
const claudeShareButton = html.match(
/<button class="card-action-btn"[^>]*`@click`="copyClaudeShareCommand\(name\)"[^>]*>/
);
assert(claudeShareButton, 'Claude share button should exist');
assert.doesNotMatch(claudeShareButton[0], /\s(?:disabled|:disabled)=/);
assert.match(claudeShareButton[0], /:aria-label="t\('config\.shareCommand\.aria'\)"/);
assert.match(claudeShareButton[0], /:title="t\('config\.shareCommand'\)"/);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/config-tabs-ui.test.mjs` at line 331, The current assertion using
assert.match(html, /<button
class="card-action-btn"[^>]*`@click`="copyClaudeShareCommand\(name\)"[^>]*>/) only
checks for presence but not that the button is enabled; update the test in
tests/unit/config-tabs-ui.test.mjs to (1) assert the matched button markup does
NOT contain disabled or :disabled (e.g., assert.notMatch against
/\s(?:disabled|:disabled)(?:=|>)/), (2) assert aria-disabled is not "true"
(e.g., assert.notMatch html against /aria-disabled=["']\s*true\s*["']/), and (3)
assert the title/aria-label used for tooltip does not indicate a disabled state
(e.g., assert.notMatch html against
/(title|aria-label)=["'][^"']*disabled[^"']*["']/); keep the original
`@click/name` presence check (copyClaudeShareCommand and name) to ensure
interactivity.

assert.match(html, /<button class="card-action-btn"[^>]*@click="openOpenclawEditModal\(name\)"[^>]*:aria-label="t\('openclaw\.action\.editAria', \{ name \}\)"[^>]*:title="t\('openclaw\.action\.edit'\)">/);
assert.match(
html,
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/web-ui-behavior-parity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus
'codexModelHasList',
'codexProviderTemplates',
'providersHealthSummary',
'providersHealthTone'
'providersHealthTone',
'sessionContextUtilization'
];
const allowedMissingCurrentComputedKeys = [
'hasLocalAndProxy',
Expand Down
42 changes: 42 additions & 0 deletions web-ui/modules/app.computed.session.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ function formatCompactUsageSummaryNumber(value) {
}).format(Math.floor(numeric));
}

function formatSignedUsageSummaryNumber(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return '0';
}
const abs = Math.floor(Math.abs(numeric));
const formatted = abs.toLocaleString('en-US');
if (numeric > 0) return `+${formatted}`;
if (numeric < 0) return `-${formatted}`;
return '0';
}

function formatUsageRangeLabel(range, t) {
const normalized = typeof range === 'string' ? range.trim().toLowerCase() : '7d';
if (typeof t === 'function') {
Expand Down Expand Up @@ -571,6 +583,7 @@ export function createSessionComputed() {
tokenPercent: maxTokens > 0 ? Math.round((row.tokenTotal / maxTokens) * 1000) / 10 : 0,
prevTokenPercent: row.compareEnabled && maxTokens > 0 ? Math.round(((row.prevTokenTotal || 0) / maxTokens) * 1000) / 10 : 0,
prevTokenTitle: row.compareEnabled ? formatUsageSummaryNumber(row.prevTokenTotal || 0) : '',
prevCostTitle: row.compareEnabled ? formatUsageSummaryNumber(row.prevTokenTotal || 0) : '',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for usage of prevCostTitle to understand its purpose

# Search for prevCostTitle usage in templates and code
rg -n 'prevCostTitle' --type=html --type=js --type=vue -C3

Repository: SakuraByteCore/codexmate

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Search for prevCostTitle without vue type
rg -n 'prevCostTitle' -C3

# Also search for the file to confirm path
find . -name "app.computed.session.mjs" -type f

Repository: SakuraByteCore/codexmate

Length of output: 1909


🏁 Script executed:

#!/bin/bash
# Read the file around line 586 to see context
# First, get file info
file_path=$(find . -name "app.computed.session.mjs" -type f)
if [ -n "$file_path" ]; then
  # Show lines 570-600 to get context
  sed -n '570,600p' "$file_path" | cat -n
fi

Repository: SakuraByteCore/codexmate

Length of output: 1887


🏁 Script executed:

#!/bin/bash
# Search for how prevCostTitle and prevTokenTitle are used in templates
rg -n 'prevToken|prevCost' -t html -t js -C2

Repository: SakuraByteCore/codexmate

Length of output: 2888


🏁 Script executed:

#!/bin/bash
# Search for prevCostTitle in all files, especially templates
rg -n 'prevCostTitle' -C5

# Also search for cost-related fields to see if actual cost calculation exists
rg -n 'prevCost|costTotal|cost' --type=js --type=html -i | head -50

Repository: SakuraByteCore/codexmate

Length of output: 3298


🏁 Script executed:

#!/bin/bash
# Search for tooltip usage patterns to understand how prevCostTitle is consumed
rg -n 'tooltip|title=' web-ui/partials/ -C2 | head -80

Repository: SakuraByteCore/codexmate

Length of output: 11257


🏁 Script executed:

#!/bin/bash
# Search for where prevTokenTitle is referenced/used
rg -n 'prevTokenTitle|prevCostTitle' -t js -t html -A2 -B2

# Also search for usage of day.tokenTitle or similar to understand the pattern
rg -n '\.tokenTitle|day\.token' web-ui/partials/ -C3

Repository: SakuraByteCore/codexmate

Length of output: 2017


Remove or rename prevCostTitle—field duplicates prevTokenTitle and has misleading naming.

Line 586 defines prevCostTitle with identical logic to prevTokenTitle (line 585), formatting prevTokenTotal using formatUsageSummaryNumber. The field name suggests cost data, but it actually formats token counts. This appears to be either:

  • Unused duplicate code (field is tested but not referenced in templates)
  • Incomplete implementation where actual cost calculation should replace token formatting
  • Placeholder for future tooltip support never completed

Either remove the field or correct the naming to prevTokenLabel if token display is intended.

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

In `@web-ui/modules/app.computed.session.mjs` at line 586, The field prevCostTitle
duplicates prevTokenTitle and misleads by implying cost; either remove
prevCostTitle entirely from the computed object or rename it to prevTokenLabel
and keep the same logic, i.e. replace prevCostTitle: ... with prevTokenLabel:
row.compareEnabled ? formatUsageSummaryNumber(row.prevTokenTotal || 0) : ''; if
you actually intend to show cost instead, implement the proper cost calculation
and formatting in prevCostTitle using the appropriate cost-calculation helper
rather than formatUsageSummaryNumber (update any template references from
prevCostTitle to the new name as needed).

})),
maxTokens,
};
Expand Down Expand Up @@ -688,6 +701,35 @@ export function createSessionComputed() {
return Math.max(0, Math.floor(totalCount));
}
return Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems.length : 0;
},

sessionContextUtilization() {
const list = Array.isArray(this.sessionsList) ? this.sessionsList : [];
const utilizationMap = {};
for (const session of list) {
if (!session || typeof session !== 'object') continue;
const key = this.getSessionExportKey(session);
if (!key) continue;
const totalTokens = Number.isFinite(Number(session.totalTokens))
? Math.max(0, Math.floor(Number(session.totalTokens)))
: 0;
const contextWindow = Number.isFinite(Number(session.contextWindow))
? Math.max(0, Math.floor(Number(session.contextWindow)))
: 0;
if (contextWindow <= 0) {
utilizationMap[key] = { percent: 0, level: 'normal' };
continue;
}
const percent = Math.min(100, Math.round((totalTokens / contextWindow) * 100));
let level = 'normal';
if (percent >= 95) {
level = 'critical';
} else if (percent >= 80) {
level = 'warning';
}
utilizationMap[key] = { percent, level };
}
return utilizationMap;
}
};
}
6 changes: 3 additions & 3 deletions web-ui/modules/i18n.dict.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,7 @@ const DICT = Object.freeze({
'claude.notConfigured': '未配置',
'claude.action.edit': '编辑',
'claude.action.delete': '删除',
'claude.action.shareDisabled': '分享导入命令(暂时禁用)',
'claude.action.shareDisabled': '分享导入命令',
'claude.action.editAria': '编辑 Claude 配置:{name}',
'claude.action.deleteAria': '删除 Claude 配置:{name}',
'claude.action.clone': '克隆',
Expand Down Expand Up @@ -2087,7 +2087,7 @@ const DICT = Object.freeze({
'claude.notConfigured': '未設定',
'claude.action.edit': '編集',
'claude.action.delete': '削除',
'claude.action.shareDisabled': 'インポートコマンド共有(一時無効)',
'claude.action.shareDisabled': 'インポートコマンド共有',
'claude.action.editAria': 'Claude 設定を編集:{name}',
'claude.action.deleteAria': 'Claude 設定を削除:{name}',
'claude.action.clone': 'クローン',
Expand Down Expand Up @@ -3147,7 +3147,7 @@ const DICT = Object.freeze({
'claude.notConfigured': 'Not configured',
'claude.action.edit': 'Edit',
'claude.action.delete': 'Delete',
'claude.action.shareDisabled': 'Share import command (disabled)',
'claude.action.shareDisabled': 'Share import command',
'claude.action.editAria': 'Edit Claude config: {name}',
'claude.action.deleteAria': 'Delete Claude config: {name}',
'claude.action.clone': 'Clone',
Expand Down
6 changes: 1 addition & 5 deletions web-ui/partials/index/layout-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,7 @@
<div class="brand-head">
<img class="brand-logo" src="/res/logo-pack.webp" alt="Codex Mate logo">
<div class="brand-copy">
<div class="brand-kicker">{{ t('brand.kicker.workspace') }}</div>
<div class="brand-title">Codex Mate</div>
<div class="brand-kicker">Codex Mate <span v-if="appVersion" class="brand-version">v{{ appVersion }}</span></div>
</div>
</div>
<div class="brand-subtitle">{{ t('brand.subtitle.localConfigSessionsWorkspace') }}</div>
Expand Down Expand Up @@ -340,9 +339,6 @@
:class="{ active: (lang || 'zh') === 'ja' }"
@click="setLang('ja')">日本語</button>
</div>
<div class="side-rail-version" v-if="appVersion">
<span class="side-rail-version-text">v{{ appVersion }}</span>
</div>
</div>
</aside>
<main class="main-panel">
Expand Down
2 changes: 1 addition & 1 deletion web-ui/partials/index/panel-config-claude.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
<button class="card-action-btn" @click="openCloneClaudeConfigModal(name, config)" :aria-label="t('claude.action.cloneAria', { name })" :title="t('claude.action.clone')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
<button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" disabled :title="t('claude.action.shareDisabled')" :aria-label="t('config.shareCommand.aria')">
<button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" :title="t('config.shareCommand')" :aria-label="t('config.shareCommand.aria')">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Disable the share button while loading to prevent duplicate actions.

Line 125 applies a loading class but still allows repeated clicks while claudeShareLoading[name] is true, which can trigger duplicate copy attempts.

Proposed fix
-<button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" `@click`="copyClaudeShareCommand(name)" :title="t('config.shareCommand')" :aria-label="t('config.shareCommand.aria')">
+<button
+  class="card-action-btn"
+  :class="{ loading: claudeShareLoading[name] }"
+  :disabled="claudeShareLoading[name]"
+  `@click`="copyClaudeShareCommand(name)"
+  :title="t('config.shareCommand')"
+  :aria-label="t('config.shareCommand.aria')">
📝 Committable suggestion

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

Suggested change
<button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" :title="t('config.shareCommand')" :aria-label="t('config.shareCommand.aria')">
<button
class="card-action-btn"
:class="{ loading: claudeShareLoading[name] }"
:disabled="claudeShareLoading[name]"
`@click`="copyClaudeShareCommand(name)"
:title="t('config.shareCommand')"
:aria-label="t('config.shareCommand.aria')">
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-config-claude.html` at line 125, The share button
currently only toggles a loading class but remains clickable; prevent duplicate
actions by disabling it while loading: add a disabled binding (e.g.,
:disabled="claudeShareLoading[name]") to the button and guard the click handler
so it only invokes copyClaudeShareCommand(name) when claudeShareLoading[name] is
false (for example change the `@click` to a short conditional call or add an
early-return in copyClaudeShareCommand to ignore calls when
claudeShareLoading[name] is true); also consider setting aria-disabled for
accessibility.

<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/><path d="M16 6l-4-4-4 4"/><path d="M12 2v14"/></svg>
</button>
<button class="card-action-btn delete" @click="deleteClaudeConfig(name)" :aria-label="t('claude.action.deleteAria', { name })" :title="t('claude.action.delete')">
Expand Down
6 changes: 6 additions & 0 deletions web-ui/partials/index/panel-sessions.html
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@
<div class="session-item-title">{{ session.title || session.sessionId }}</div>
<span class="session-count-badge">{{ session.messageCount == null ? '...' : session.messageCount }}</span>
<span v-if="session.match && session.match.hit" class="session-match-badge">{{ session.match.count }}</span>
<span
v-if="sessionContextUtilization[getSessionExportKey(session)] && sessionContextUtilization[getSessionExportKey(session)].percent > 0"
:class="['session-context-badge', 'session-context-' + sessionContextUtilization[getSessionExportKey(session)].level]"
:title="'Context: ' + sessionContextUtilization[getSessionExportKey(session)].percent + '%'">
{{ sessionContextUtilization[getSessionExportKey(session)].percent }}%
</span>
</div>
<div class="session-item-actions">
<button
Expand Down
56 changes: 23 additions & 33 deletions web-ui/styles/layout-shell.css
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ body::after {
border-radius: 0;
box-shadow: 8px 0 28px rgba(92, 68, 52, 0.05), 1px 0 0 rgba(255, 255, 255, 0.5);
min-height: 100vh;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
backdrop-filter: blur(18px) saturate(130%);
Expand All @@ -65,11 +64,6 @@ body::after {
display: none;
}

.side-rail .brand-title {
font-size: 22px;
margin-bottom: 0;
}

.side-section {
display: flex;
flex-direction: column;
Expand All @@ -84,8 +78,15 @@ body::after {
.side-rail-nav {
display: flex;
flex-direction: column;
gap: 0;
flex: 1 1 auto;
gap: 0;
overflow-y: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}

.side-rail-nav::-webkit-scrollbar {
display: none;
}

.lang-fab {
Expand Down Expand Up @@ -205,10 +206,11 @@ body::after {
}

.side-rail-lang {
position: sticky;
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
margin-top: auto;
padding: 10px 4px 6px;
background: linear-gradient(180deg, rgba(255, 248, 241, 0), rgba(255, 248, 241, 0.94) 28%);
border-top: 1px solid rgba(137, 111, 94, 0.08);
Expand All @@ -225,17 +227,6 @@ body::after {
backdrop-filter: none;
}

.side-rail-version {
padding: 4px 8px 8px;
text-align: center;
}

.side-rail-version-text {
font-size: 11px;
color: rgba(137, 111, 94, 0.45);
letter-spacing: 0.02em;
}

.lang-switch {
width: 100%;
border: none;
Expand Down Expand Up @@ -465,12 +456,20 @@ body::after {
}

.brand-kicker {
font-size: 18px;
line-height: 1.15;
font-family: var(--font-family-display);
color: var(--color-text-primary);
letter-spacing: -0.02em;
font-weight: 700;
}

.brand-version {
font-size: 11px;
line-height: 1;
letter-spacing: 0.06em;
text-transform: uppercase;
font-weight: 500;
color: var(--color-text-muted);
font-weight: 700;
vertical-align: middle;
margin-left: 4px;
}

.brand-subtitle {
Expand All @@ -480,15 +479,6 @@ body::after {
max-width: 22ch;
}

.brand-title {
font-size: 18px;
line-height: 1.15;
font-family: var(--font-family-display);
color: var(--color-text-primary);
letter-spacing: -0.02em;
font-weight: 700;
}

.github-badge {
display: inline-flex;
align-items: center;
Expand Down
36 changes: 36 additions & 0 deletions web-ui/styles/sessions-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -645,3 +645,39 @@
position: relative;
transition: box-shadow var(--transition-normal) var(--ease-spring-soft);
}

.session-context-badge {
font-size: 10px;
padding: 1px 6px;
border-radius: 999px;
font-weight: 600;
line-height: 1.4;
white-space: nowrap;
}

.session-context-normal {
background: rgba(75, 139, 106, 0.12);
color: #4b8b6a;
}

.session-context-warning {
background: rgba(217, 119, 6, 0.15);
color: #b45309;
animation: context-warning-pulse 2s ease-in-out infinite;
}

.session-context-critical {
background: rgba(196, 69, 54, 0.18);
color: #c2410c;
animation: context-critical-pulse 1s ease-in-out infinite;
}

@keyframes context-warning-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}

@keyframes context-critical-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
Loading