Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
187 changes: 106 additions & 81 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
--success: #2ec27e;
--warn: #f6d32d;
--danger: #e01b24;
/* Session context meter tones, indexed by the agent-advertised mode order
/* Session context ring tones, indexed by the agent-advertised mode order
(see getSessionContextTone). Values stay muted so any tone blends with the
warm gray surfaces; the dark theme swaps in lighter counterparts. */
--mode-tone-0: #c05621;
Expand Down Expand Up @@ -5502,12 +5502,6 @@ summary:focus-visible {
-webkit-backdrop-filter: var(--surface-blur);
}

/* Second header row: the full-width session context meter. */
.session-header-context {
grid-column: 1 / -1;
min-width: 0;
}

.session-header-back {
flex: 0 0 auto;
}
Expand All @@ -5530,8 +5524,11 @@ summary:focus-visible {

.session-header-meta {
display: flex;
width: max-content;
max-width: 100%;
min-width: 0;
align-items: center;
justify-content: flex-start;
gap: 0.35rem;
margin-top: 0.15rem;
overflow: hidden;
Expand All @@ -5545,17 +5542,53 @@ summary:focus-visible {
display: none;
}

.session-header-meta span:not([aria-hidden]) {
.session-header-meta span:not([aria-hidden]):not(.session-context-dial):not(.session-context-dial-figure) {
overflow: hidden;
text-overflow: ellipsis;
}

.session-header-meta > .session-header-session-id,
.session-header-meta > .session-header-context-slot,
.session-header-meta > .session-header-status-wrap,
.session-header-mobile-meta > .session-context-dial {
flex: 0 0 auto;
}

.session-header-context-slot {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 0.35rem;
}

.session-header-meta-desktop,
.session-header-session-id {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 0.35rem;
font-size: inherit;
line-height: inherit;
}

/* One line box for the whole meta row. Chips otherwise bring their own
font-size, line-height, and padding and the row grows to the tallest. */
.session-header-meta .session-id-chip,
.session-header-meta .copy-chip,
.session-header-meta .session-context-dial,
.session-header-meta .session-header-profile {
font-size: inherit;
line-height: inherit;
}

.session-header-meta .session-id-chip,
.session-header-meta .copy-chip {
padding-block: 0;
}

.session-header-meta .session-header-profile svg {
width: 1em;
height: 1em;
}

.session-header-controls,
Expand Down Expand Up @@ -6045,7 +6078,6 @@ summary:focus-visible {
}

.session-usage-bar {
--session-context-accent: var(--accent);
display: grid;
gap: 0.6rem;
margin-top: 0.7rem;
Expand All @@ -6054,51 +6086,76 @@ summary:focus-visible {
font-size: 0.7rem;
}

/* Mode tones: indexes follow the agent-advertised mode order (see
getSessionContextTone); the context meter picks the accent up from here. */
.session-context-tone-0 { --session-context-accent: var(--mode-tone-0); }
.session-context-tone-1 { --session-context-accent: var(--mode-tone-1); }
.session-context-tone-2 { --session-context-accent: var(--mode-tone-2); }
.session-context-tone-3 { --session-context-accent: var(--mode-tone-3); }
.session-context-tone-4 { --session-context-accent: var(--mode-tone-4); }
.session-context-tone-5 { --session-context-accent: var(--mode-tone-5); }
.session-context-tone-6 { --session-context-accent: var(--mode-tone-6); }
.session-context-tone-7 { --session-context-accent: var(--mode-tone-7); }

.session-usage-bar.session-usage-variant-context {
margin-top: 0;
/* Context is a closed window, not a task bar. The ring is fixed-size so a low
reading never stretches into an empty track. The arc stays on the accent hue;
pressure only changes how hard that hue reads, never the hue itself. */
.session-context-dial {
--session-context-accent: var(--accent);
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 0.3rem;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: inherit;
font-weight: 500;
font-variant-numeric: tabular-nums;
line-height: inherit;
white-space: nowrap;
}

.session-usage-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.7rem;
.session-context-dial-calm .session-context-dial-arc {
opacity: 0.72;
}

.session-usage-context {
display: flex;
align-items: center;
min-width: 0;
gap: 0.45rem;
.session-context-dial-warm {
color: var(--text);
}

.session-usage-icon,
.session-usage-stat > svg {
.session-context-dial-tight {
color: var(--status-warn);
}

.session-context-dial-full {
color: var(--status-danger);
}

.session-context-dial-ring {
width: 1lh;
height: 1lh;
flex: 0 0 auto;
overflow: visible;
}

.session-context-dial-ring circle {
fill: none;
stroke-width: 2.4;
stroke-linecap: round;
}

.session-context-dial-track {
stroke: color-mix(in srgb, var(--text-dim) 42%, transparent);
}

/* The gauge takes the mode tone; stat icons keep the application accent. */
.session-usage-icon {
color: var(--session-context-accent);
.session-context-dial-arc {
stroke: var(--session-context-accent);
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset 240ms ease, opacity 180ms ease;
}

.session-usage-meta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.7rem;
}

.session-usage-stat > svg {
flex: 0 0 auto;
color: var(--accent);
}

.session-usage-label,
.session-usage-stat > span {
color: var(--muted);
font-family: var(--font-sans);
Expand All @@ -6108,41 +6165,13 @@ summary:focus-visible {
text-transform: uppercase;
}

.session-usage-context strong,
.session-usage-stat strong {
color: var(--text);
font-size: 0.72rem;
font-weight: 650;
white-space: nowrap;
}

.session-usage-meter {
position: relative;
min-width: 3.75rem;
height: 0.28rem;
flex: 1 1 8rem;
overflow: hidden;
border-radius: 999px;
background: color-mix(in srgb, var(--session-context-accent) 14%, transparent);
}

.session-usage-meter span {
position: absolute;
inset: 0 auto 0 0;
width: var(--session-context-percent);
border-radius: inherit;
background: var(--session-context-accent);
box-shadow: 0 0 10px color-mix(in srgb, var(--session-context-accent) 35%, transparent);
transition: width 240ms ease;
}

.session-usage-percent {
min-width: 2.5rem;
color: var(--text);
font-weight: 650;
text-align: right;
}

.session-usage-stat {
display: flex;
align-items: center;
Expand Down Expand Up @@ -10413,6 +10442,10 @@ summary:focus-visible {
will-change: auto;
}

.session-context-dial-arc {
transition: none;
}

.session-activity-bar-dock,
.session-scroll-bottom-pill-host {
transition: none;
Expand Down Expand Up @@ -10813,10 +10846,15 @@ summary:focus-visible {
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 0.64rem;
line-height: 1.05;
line-height: 1.2;
white-space: nowrap;
}

.app-shell-active-session .session-header-mobile-meta .session-context-dial {
font-size: inherit;
line-height: inherit;
}

.app-shell-active-session .session-header-mobile-workspace,
.app-shell-active-session .session-header-mobile-id {
overflow: hidden;
Expand All @@ -10830,7 +10868,7 @@ summary:focus-visible {

.app-shell-active-session .session-header-mobile-id {
min-width: 0;
flex: 1 1 auto;
flex: 0 1 auto;
}

.app-shell-active-session .session-header-details-panel {
Expand Down Expand Up @@ -10899,23 +10937,10 @@ summary:focus-visible {
min-height: 1.25rem;
}

.app-shell-active-session .session-header-details-panel .session-usage-context {
gap: 0.35rem;
}

.app-shell-active-session .session-header-details-panel .session-usage-label,
.app-shell-active-session .session-header-details-panel .session-usage-stat > span {
font-size: 0.58rem;
}

.app-shell-active-session .session-header-details-panel .session-usage-meter {
min-width: 2.5rem;
}

.app-shell-active-session .session-header-details-panel .session-usage-percent {
min-width: 2rem;
}

.app-shell-active-session .session-page-content {
gap: 0.9rem;
padding: 0.85rem 0.75rem var(
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/primitives/SessionComposer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@
{/if}
</div>
</div>
{/snippet}
{/snippet}

<div
bind:this={dockElement}
Expand Down
64 changes: 64 additions & 0 deletions src/lib/components/session/SessionContextDial.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import type { SessionUsageStats } from '$lib/domain/types';
import {
formatTokenCount,
getContextPercent,
getContextPressure,
getContextRemaining
} from '$lib/domain/session-usage';

let { usage }: { usage: SessionUsageStats } = $props();

// Closed ring: the circle is the window, the arc is used, the gap is what's left.
// A fixed footprint so a low reading never stretches into an empty track.
const RING_RADIUS = 7;
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;

const percent = $derived(getContextPercent(usage));
const pressure = $derived(getContextPressure(percent));
const remaining = $derived(getContextRemaining(usage));
const roundedPercent = $derived(percent === null ? null : Math.round(percent));
const usedLabel = $derived(formatTokenCount(usage.contextUsed));
const limitLabel = $derived(formatTokenCount(usage.contextLimit));
const figure = $derived(roundedPercent !== null ? `${roundedPercent}%` : usedLabel);
const detail = $derived.by(() => {
if (usage.contextUsed === null) return '';
if (roundedPercent !== null && usage.contextLimit !== null) {
const remainingNote = remaining === null ? '' : `, ${formatTokenCount(remaining)} left`;
return `${usedLabel} of ${limitLabel} · ${roundedPercent}% of the context window${remainingNote}`;
}
return `${usedLabel} in the context window`;
});
const arcOffset = $derived(
percent === null ? RING_CIRCUMFERENCE : RING_CIRCUMFERENCE * (1 - percent / 100)
);
const rootClass = $derived(`session-context-dial session-context-dial-${pressure}`);
</script>

{#if usage.contextUsed !== null}
<span
class={rootClass}
title={detail}
aria-label="Context window"
aria-valuetext={detail}
role={roundedPercent !== null ? 'meter' : 'img'}
aria-valuemin={roundedPercent !== null ? 0 : undefined}
aria-valuemax={roundedPercent !== null ? 100 : undefined}
aria-valuenow={roundedPercent ?? undefined}
>
<svg class="session-context-dial-ring" viewBox="0 0 20 20" aria-hidden="true">
<circle class="session-context-dial-track" cx="10" cy="10" r={RING_RADIUS} />
{#if percent !== null && percent > 0}
<circle
class="session-context-dial-arc"
cx="10"
cy="10"
r={RING_RADIUS}
stroke-dasharray={RING_CIRCUMFERENCE}
stroke-dashoffset={arcOffset}
/>
{/if}
</svg>
<span class="session-context-dial-figure">{figure}</span>
</span>
{/if}
Loading
Loading