Skip to content

Commit 58a0935

Browse files
author
IM.codes
committed
Refine footer agent status icons
1 parent e279946 commit 58a0935

3 files changed

Lines changed: 33 additions & 13 deletions

File tree

web/src/components/UsageFooter.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export function UsageFooter({ usage, sessionName, sessionState, agentType, model
9595
const monthlyCost = sessionCost > 0 ? getMonthlyCost() : 0;
9696
const modelLabel = shortModelLabel(displayModel);
9797
const inlineQuotaText = displayQuotaLabel;
98+
const liveStatusMode = sessionState === 'running' ? 'running' : sessionState === 'idle' ? 'idle' : null;
9899
const liveStatusText = useMemo(() => {
99100
if (sessionState === 'running') {
100101
if (statusText) return statusText;
@@ -116,12 +117,6 @@ export function UsageFooter({ usage, sessionName, sessionState, agentType, model
116117
<div class="session-ctx-input" style={{ width: `${newPct}%`, left: `${cachePct}%` }} />
117118
</div>
118119
)}
119-
{showLiveStatus && liveStatusText && (
120-
<div class="session-live-status">
121-
{sessionState === 'running' && <span class="chat-thinking-dots">···</span>}
122-
<span>{liveStatusText}</span>
123-
</div>
124-
)}
125120
{codexQuotaLines.length > 0 && (
126121
<div class="session-usage-codex-quota">
127122
{codexQuotaLines.map((line) => (
@@ -130,6 +125,14 @@ export function UsageFooter({ usage, sessionName, sessionState, agentType, model
130125
</div>
131126
)}
132127
<div class="session-usage-stats">
128+
{showLiveStatus && liveStatusText && liveStatusMode && (
129+
<span class={`session-live-status-inline ${liveStatusMode}`} title={liveStatusText} aria-label={liveStatusText}>
130+
<span class="session-live-status-emoji robot">🤖</span>
131+
{liveStatusMode === 'running'
132+
? <span class="session-live-status-emoji gear">⚙️</span>
133+
: <span class="session-live-status-emoji sleep">💤</span>}
134+
</span>
135+
)}
133136
<span style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
134137
{modelLabel && <span class="session-usage-model">{modelLabel}</span>}
135138
{total > 0 && <span class="session-usage-tokens">{fmt(total)} / {fmt(ctx)} ({pctStr}%)</span>}

web/src/styles.css

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -852,18 +852,27 @@ body {
852852
/* Usage footer — between chat/terminal and input controls */
853853
.session-usage-footer { flex-shrink: 0; padding: 4px 10px 3px; background: #0f1117; border-top: 1px solid #1e293b; }
854854
.session-ctx-bar { position: relative; width: 100%; height: 5px; background: #1e293b; border-radius: 3px; overflow: hidden; margin-bottom: 3px; }
855-
.session-live-status { display: flex; align-items: center; gap: 6px; min-height: 16px; margin-bottom: 3px; color: #818cf8; font-size: 10px; }
856855
.session-usage-codex-row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 4px; align-items: center; }
857856
.session-usage-codex-quota { display: flex; flex-direction: column; align-items: flex-end; gap: 1px; margin-bottom: 2px; }
858857
.session-usage-codex-line { color: #64748b; font-size: 9px; line-height: 1.2; text-align: right; }
859858
.session-ctx-input { position: absolute; left: 0; top: 0; height: 100%; background: #34d399; border-radius: 3px; }
860859
.session-ctx-cache { position: absolute; left: 0; top: 0; height: 100%; background: #818cf8; border-radius: 3px; }
861860
.session-usage-stats { display: flex; justify-content: space-between; font-size: 10px; color: #475569; }
861+
.session-live-status-inline { display: inline-flex; align-items: center; justify-content: center; gap: 1px; min-width: 20px; color: #818cf8; }
862+
.session-live-status-emoji { display: inline-block; font-size: 12px; line-height: 1; filter: saturate(1.1); }
863+
.session-live-status-emoji.robot { transform: translateY(0.2px); }
864+
.session-live-status-inline.running .session-live-status-emoji.gear { animation: status-gear-spin 0.8s linear infinite; transform-origin: 50% 50%; }
865+
.session-live-status-inline.idle .session-live-status-emoji.sleep { font-size: 9px; transform: translateY(-3px) translateX(-1px); opacity: 0.9; animation: status-sleep-breathe 1.8s ease-in-out infinite; transform-origin: 50% 100%; }
862866
.session-usage-model { color: #a78bfa; font-size: 10px; font-weight: 500; margin-right: 6px; }
863867
.session-usage-tokens { color: #64748b; }
864868
.session-usage-badge { color: #93c5fd; border: 1px solid #1d4ed8; border-radius: 999px; padding: 1px 6px; line-height: 1.4; }
865869
.session-usage-quota-inline { color: #64748b; font-size: 9px; line-height: 1.4; white-space: nowrap; }
866870
.session-usage-cost { color: #94a3b8; }
871+
@keyframes status-gear-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
872+
@keyframes status-sleep-breathe {
873+
0%, 100% { transform: translateY(-3px) translateX(-1px) scale(0.88); opacity: 0.72; }
874+
50% { transform: translateY(-4px) translateX(-1px) scale(1.08); opacity: 1; }
875+
}
867876
.subsession-input-bar { display: flex; gap: 6px; padding: 6px 8px; background: #0d1117; border-top: 1px solid #1e293b; flex-shrink: 0; }
868877
.subsession-input { flex: 1; background: #1e293b; border: 1px solid #334155; border-radius: 6px; color: #e2e8f0; font-family: inherit; font-size: 13px; padding: 5px 10px; outline: none; }
869878
.subsession-input:focus { border-color: #3b82f6; }

web/test/usage-footer.test.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ afterEach(() => {
3434
});
3535

3636
describe('UsageFooter', () => {
37-
it('renders live status below the ctx bar and only animates while running', () => {
37+
it('renders emoji live status inline with footer stats and only animates while running', () => {
3838
const { container, rerender } = render(
3939
<UsageFooter
4040
usage={{
@@ -49,8 +49,12 @@ describe('UsageFooter', () => {
4949
/>,
5050
);
5151

52-
expect(container.querySelector('.session-live-status')?.textContent).toContain('Agent idle');
52+
const idleStatus = container.querySelector('.session-live-status-inline') as HTMLSpanElement | null;
53+
expect(idleStatus?.textContent).toContain('🤖');
54+
expect(idleStatus?.textContent).toContain('💤');
55+
expect(idleStatus?.getAttribute('aria-label')).toContain('Agent idle');
5356
expect(container.querySelector('.chat-thinking-dots')).toBeNull();
57+
expect(container.querySelector('.session-live-status-inline.idle .session-live-status-emoji.sleep')).toBeTruthy();
5458

5559
rerender(
5660
<UsageFooter
@@ -66,12 +70,16 @@ describe('UsageFooter', () => {
6670
/>,
6771
);
6872

69-
expect(container.querySelector('.session-live-status')?.textContent).toContain('thinking');
70-
expect(container.querySelector('.chat-thinking-dots')).toBeTruthy();
73+
const runningStatus = container.querySelector('.session-live-status-inline') as HTMLSpanElement | null;
74+
expect(runningStatus?.textContent).toContain('🤖');
75+
expect(runningStatus?.textContent).toContain('⚙️');
76+
expect(runningStatus?.getAttribute('aria-label')).toContain('thinking');
77+
expect(container.querySelector('.session-live-status-inline.running')).toBeTruthy();
78+
expect(container.querySelector('.session-live-status-inline.running .session-live-status-emoji.gear')).toBeTruthy();
7179
});
7280

7381
it('prefers explicit running status text in the live status row', () => {
74-
render(
82+
const { container } = render(
7583
<UsageFooter
7684
usage={{
7785
inputTokens: 0,
@@ -85,7 +93,7 @@ describe('UsageFooter', () => {
8593
/>,
8694
);
8795

88-
expect(screen.getByText('Reading file...')).toBeDefined();
96+
expect((container.querySelector('.session-live-status-inline') as HTMLSpanElement | null)?.getAttribute('aria-label')).toBe('Reading file...');
8997
});
9098

9199
it('renders explicit quota label inline in the ctx footer', () => {

0 commit comments

Comments
 (0)