|
| 1 | +## Context |
| 2 | + |
| 3 | +The WebUI currently has a simple header with "DSCode" branding on the left, a model name label, and a theme toggle + connection status on the right. The center of the header is empty. Users have no visibility into their context window budget — how much of the model's token limit is occupied vs. free, or what categories of content consume tokens. |
| 4 | + |
| 5 | +The `ContextManager` (`src/context/manager.ts`) already estimates total message tokens via `estimateMessagesTokens()` and uses that for compaction (`drop-oldest`, `sliding-window`). However, this estimation is aggregate-only; no per-category breakdown exists. The `WebUiBackend` tracks tool calls in `currentAssistant.tools` but doesn't expose token counts to the frontend. |
| 6 | + |
| 7 | +The Cline extension popularized the "context window bar" UX — a compact horizontal bar showing per-category token usage with color coding. We adopt the same pattern but integrate it into dscode's warm flat-design aesthetic and the existing WebSocket event model. |
| 8 | + |
| 9 | +**Constraints:** |
| 10 | +- Must work within the existing warm design system (no new design language) |
| 11 | +- Must not add external dependencies |
| 12 | +- Must reuse existing `estimateTokens` infrastructure |
| 13 | +- Must handle both light and dark themes via CSS custom properties |
| 14 | +- Bar must fit comfortably in the header center without crowding other elements |
| 15 | + |
| 16 | +## Goals / Non-Goals |
| 17 | + |
| 18 | +**Goals:** |
| 19 | +- Show a real-time, segmented horizontal bar in the WebUI header center representing context window usage |
| 20 | +- Color-code segments by content category: system prompt, user messages, thinking, tool calls (file read, file edit, terminal, browser), and free space |
| 21 | +- Display a compact numerical summary (e.g. `8.2k / 128k tokens`) |
| 22 | +- Update the bar after each turn completes and periodically during long streaming turns |
| 23 | +- Use the warm design system's existing CSS custom property infrastructure |
| 24 | + |
| 25 | +**Non-Goals:** |
| 26 | +- Adding context window usage to the TUI (this is WebUI-only) |
| 27 | +- Historical context window charts or analytics |
| 28 | +- Customizable category colors via settings |
| 29 | +- Per-session context window history |
| 30 | +- Compaction trigger recommendations or warnings (just visualization) |
| 31 | + |
| 32 | +## Decisions |
| 33 | + |
| 34 | +### 1. Per-category token estimation lives in `ContextManager` |
| 35 | + |
| 36 | +**Decision:** Extend `ContextManager` with a `getCategoryBreakdown(messages, tools)` method that returns estimates per category. The method iterates raw messages plus the current turn's tool calls and classifies each into a category enum. |
| 37 | + |
| 38 | +**Rationale:** Token estimation logic already lives here. Keeping it centralized avoids duplicating the CJK-aware estimation formula. The method accepts tool calls as a separate parameter because tool calls are tracked in `WebUiBackend.currentAssistant.tools`, not in the raw message array. |
| 39 | + |
| 40 | +**Alternatives considered:** |
| 41 | +- Putting estimation in `WebUiBackend`: Rejected — would duplicate estimator logic and create a second source of truth. |
| 42 | +- Putting estimation in the frontend: Rejected — the frontend doesn't have raw message content after server-side formatting, and estimating on the frontend would be inaccurate for CJK text. |
| 43 | + |
| 44 | +### 2. New `context_window` ServerEvent |
| 45 | + |
| 46 | +**Decision:** Add a new `context_window` event to the `ServerEvent` union: |
| 47 | + |
| 48 | +```typescript |
| 49 | +{ type: "context_window"; total: number; used: number; free: number; |
| 50 | + categories: { system: number; user: number; thinking: number; |
| 51 | + fileRead: number; fileEdit: number; terminal: number; |
| 52 | + browser: number; other: number } } |
| 53 | +``` |
| 54 | + |
| 55 | +**Rationale:** A dedicated event type is cleaner than piggybacking on `ready` or `sessions`. It's sent independently and can be throttled without affecting other event streams. The category breakdown uses fixed keys so the frontend can reference them directly without string parsing. |
| 56 | + |
| 57 | +**Alternatives considered:** |
| 58 | +- Piggyback on `sessions` event: Rejected — `sessions` is already data-heavy and sent for session list changes, not context changes. |
| 59 | +- Piggyback on `assistant_end`: Rejected — we also want updates during long turns (at each tool_end). |
| 60 | +- Generic payload like `Record<string, number>`: Rejected — fixed keys give TypeScript type safety. |
| 61 | + |
| 62 | +### 3. Broadcast timing: throttled, not on every event |
| 63 | + |
| 64 | +**Decision:** The backend broadcasts `context_window` on these triggers, throttled to at most once per 500ms: |
| 65 | +- On `ready` (initial connect / re-sync) |
| 66 | +- After each `tool_end` (tool call accumulated), throttled |
| 67 | +- After `assistant_end` (turn complete), always sent |
| 68 | +- After `clear_conversation`, always sent |
| 69 | + |
| 70 | +**Rationale:** Sending on every `text_delta` would flood the WS connection (streaming can produce hundreds of deltas per second). 500ms throttle during streaming + always-on-turn-end ensures the bar is responsive but not wasteful. The bar's visual update is smooth enough at 2 FPS. |
| 71 | + |
| 72 | +### 4. Frontend component: `ContextWindowBar` |
| 73 | + |
| 74 | +**Decision:** A standalone React component that receives the `context_window` event data as props and renders a horizontal segmented bar. The component: |
| 75 | +- Stores the latest `ContextWindowData` in state (set via `App.tsx` event handler) |
| 76 | +- Renders nothing (returns null) when no data is available yet |
| 77 | +- Renders the bar using inline `style` props referencing CSS custom properties (matching the existing warm design pattern) |
| 78 | +- Shows a tooltip on hover with the full per-category breakdown |
| 79 | + |
| 80 | +**Rationale:** Keeping it as a single component with a focused concern (context visualization) follows the project's "one file, one responsibility" rule. Using inline styles with CSS custom properties maintains consistency with the rest of the codebase. |
| 81 | + |
| 82 | +### 5. Color palette for categories |
| 83 | + |
| 84 | +**Decision:** |
| 85 | +| Category | Light theme | Dark theme | Meaning | |
| 86 | +|----------|------------|------------|---------| |
| 87 | +| system | `#8a8580` | `#8a8580` | var(--color-text-muted) | |
| 88 | +| user | `#ca8a04` | `#d49708` | var(--color-accent) | |
| 89 | +| thinking | `#a78bfa` | `#7c6bb0` | muted violet | |
| 90 | +| fileRead | `#eab308` | `#ca8a04` | amber-yellow | |
| 91 | +| fileEdit | `#3b82f6` | `#60a5fa` | blue | |
| 92 | +| terminal | `#ef4444` | `#f87171` | red | |
| 93 | +| browser | `#8b5cf6` | `#a78bfa` | purple | |
| 94 | +| other | `#6b7280` | `#9ca3af` | gray | |
| 95 | +| free | transparent | transparent | background shows through | |
| 96 | + |
| 97 | +Defined as new CSS custom properties on `:root` and `.dark`: `--cw-system`, `--cw-user`, etc. This maintains the warm design system pattern and ensures theme transitions work automatically. |
| 98 | + |
| 99 | +### 6. Header layout: flexbox with three zones |
| 100 | + |
| 101 | +**Decision:** Modify the `<header>` in `App.tsx` from a two-zone layout (justify-between left/right) to a three-zone layout: left (branding + model + sidebar toggle), center (ContextWindowBar), right (theme + connection status). The center zone uses `flex: 1` with `justify-content: center` so the bar is naturally centered regardless of left/right content width. On small screens (`<768px`), the bar hides entirely (too narrow to be useful). |
| 102 | + |
| 103 | +## Risks / Trade-offs |
| 104 | + |
| 105 | +- **Token estimation is approximate**: `estimateTokens()` uses character-count heuristics, not actual tokenization. → Mitigation: Bar display labels include a tooltip noting "Estimated" and the numbers are good enough for budget awareness. |
| 106 | +- **Bar too wide on narrow viewports**: The header already has several elements. → Mitigation: Hide bar below 768px (matching the existing `md:` breakpoint pattern). The bar only needs ~240px minimum. |
| 107 | +- **Category classification is heuristic**: We classify tool calls by name prefix (`read_file`, `write_file`, `bash`, `browser_*`). Future tool names might not match. → Mitigation: Fall back to `other` category for unrecognized tools; easy to extend the pattern matcher. |
| 108 | +- **Extra WS traffic**: Each `context_window` event adds ~150 bytes. → Mitigation: Throttled to 500ms during streaming (~300 bytes/sec worst case), negligible vs. the existing delta stream. |
0 commit comments