chenbhao commited on
Commit
7728560
1 Parent(s): 9607e98

feat: ctx line

Browse files
src/components/CtxProgressBar.tsx ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Box, Text } from '../ink.js'
2
+
3
+ const CONTEXT_BAR_WIDTH = 20
4
+
5
+ type ContextFillBand = 'green' | 'orange' | 'red'
6
+
7
+ interface ContextBarCell {
8
+ ch: string
9
+ isFill: boolean
10
+ isMarker: boolean
11
+ }
12
+
13
+ interface CtxProgressBarProps {
14
+ currentTokens: number
15
+ contextWindowTokens: number
16
+ compactionTargetTokens: number
17
+ utilizationPct: number
18
+ }
19
+
20
+ function contextFillBand(utilizationPct: number): ContextFillBand {
21
+ const pct = Math.min(100, utilizationPct)
22
+ if (pct < 40) return 'green'
23
+ if (pct < 60) return 'orange'
24
+ return 'red'
25
+ }
26
+
27
+ function contextBarCells(
28
+ currentTokens: number,
29
+ contextWindowTokens: number,
30
+ compactionTargetTokens: number,
31
+ ): ContextBarCell[] {
32
+ const filled = Math.min(
33
+ CONTEXT_BAR_WIDTH,
34
+ Math.floor(
35
+ (currentTokens * CONTEXT_BAR_WIDTH) / Math.max(1, contextWindowTokens),
36
+ ),
37
+ )
38
+
39
+ const markerIndex = Math.max(
40
+ 0,
41
+ Math.min(
42
+ CONTEXT_BAR_WIDTH - 1,
43
+ Math.ceil(
44
+ (compactionTargetTokens * CONTEXT_BAR_WIDTH) /
45
+ Math.max(1, contextWindowTokens),
46
+ ) - 1,
47
+ ),
48
+ )
49
+
50
+ const cells: ContextBarCell[] = []
51
+ for (let i = 0; i < CONTEXT_BAR_WIDTH; i++) {
52
+ if (i === markerIndex) {
53
+ cells.push({ ch: '|', isFill: false, isMarker: true })
54
+ } else if (i < filled) {
55
+ cells.push({ ch: '=', isFill: true, isMarker: false })
56
+ } else {
57
+ cells.push({ ch: '-', isFill: false, isMarker: false })
58
+ }
59
+ }
60
+ return cells
61
+ }
62
+
63
+ function formatTokenCount(tokens: number): string {
64
+ if (tokens >= 1_000_000) {
65
+ return `${(tokens / 1_000_000).toFixed(1)}M`
66
+ }
67
+ if (tokens >= 1_000) {
68
+ return `${(tokens / 1_000).toFixed(1)}k`
69
+ }
70
+ return `${tokens}`
71
+ }
72
+
73
+ function bandColor(band: ContextFillBand): string {
74
+ switch (band) {
75
+ case 'green':
76
+ return '#22c55e'
77
+ case 'orange':
78
+ return '#f59e0b'
79
+ case 'red':
80
+ return '#ef4444'
81
+ }
82
+ }
83
+
84
+ export function CtxProgressBar({
85
+ currentTokens,
86
+ contextWindowTokens,
87
+ compactionTargetTokens,
88
+ utilizationPct,
89
+ }: CtxProgressBarProps) {
90
+ if (contextWindowTokens <= 0) return null
91
+
92
+ const band = contextFillBand(utilizationPct)
93
+ const color = bandColor(band)
94
+ const pct = Math.min(100, Math.max(0, Math.round(utilizationPct)))
95
+ const cells = contextBarCells(
96
+ currentTokens,
97
+ contextWindowTokens,
98
+ compactionTargetTokens,
99
+ )
100
+ const formattedWindow = formatTokenCount(contextWindowTokens)
101
+
102
+ return (
103
+ <Box>
104
+ <Text dimColor>ctx [</Text>
105
+ {cells.map((cell, i) => {
106
+ let cellColor: string
107
+ if (cell.isMarker) {
108
+ cellColor = '#a78bfa'
109
+ } else if (cell.isFill) {
110
+ cellColor = color
111
+ } else {
112
+ cellColor = undefined
113
+ }
114
+ return (
115
+ <Text key={i} color={cellColor} dimColor={!cellColor}>
116
+ {cell.ch}
117
+ </Text>
118
+ )
119
+ })}
120
+ <Text dimColor>] </Text>
121
+ <Text color={color}>{pct}%</Text>
122
+ <Text dimColor> [{formattedWindow}]</Text>
123
+ </Box>
124
+ )
125
+ }
src/components/LogoV2/LogoV2.tsx CHANGED
@@ -43,6 +43,8 @@ import { useAppState, useSetAppState } from '../../state/AppState.js';
43
  import { getEffortSuffix } from '../../utils/effort.js';
44
  import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
45
  import { renderModelSetting } from '../../utils/model/model.js';
 
 
46
  const LEFT_PANEL_MAX_WIDTH = 50;
47
  export function LogoV2() {
48
  const $ = _c(94);
@@ -158,6 +160,7 @@ export function LogoV2() {
158
  useEffect(t7, t8);
159
  const model = useMainLoopModel();
160
  const fullModelDisplayName = renderModelSetting(model);
 
161
  const setAppState = useSetAppState();
162
 
163
  // Poll for OpenRouter models when using 'default' model
@@ -381,7 +384,7 @@ export function LogoV2() {
381
  t18 = $[42];
382
  t19 = $[43];
383
  }
384
- return <><OffscreenFreeze><Box flexDirection="column" borderStyle="round" borderColor="startupAccent" borderText={t11} paddingX={1} paddingY={1} alignItems="center" width={columns}><Text bold={true}>{welcomeMessage}</Text>{t12}{t13}<Text dimColor={true}>{billingType}</Text><Text dimColor={true}>{agentName ? `@${agentName} 路 ${truncatedCwd}` : truncatedCwd}</Text></Box></OffscreenFreeze>{t14}{t15}{t16}{t17}{t18}{t19}</>;
385
  }
386
  const welcomeMessage_0 = formatWelcomeMessage(username);
387
  const modelLine = !process.env.IS_DEMO && config.oauthAccount?.organizationName ? `${modelDisplayName} 路 ${billingType} 路 ${config.oauthAccount.organizationName}` : `${modelDisplayName} 路 ${billingType}`;
@@ -448,7 +451,7 @@ export function LogoV2() {
448
  }
449
  let t22;
450
  if ($[53] !== t20 || $[54] !== t21) {
451
- t22 = <Box flexDirection="column" alignItems="center">{t20}{t21}</Box>;
452
  $[53] = t20;
453
  $[54] = t21;
454
  $[55] = t22;
 
43
  import { getEffortSuffix } from '../../utils/effort.js';
44
  import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
45
  import { renderModelSetting } from '../../utils/model/model.js';
46
+ import { CtxProgressBar } from '../CtxProgressBar.js';
47
+ import { useContextUsage } from '../../hooks/useContextUsage.js';
48
  const LEFT_PANEL_MAX_WIDTH = 50;
49
  export function LogoV2() {
50
  const $ = _c(94);
 
160
  useEffect(t7, t8);
161
  const model = useMainLoopModel();
162
  const fullModelDisplayName = renderModelSetting(model);
163
+ const contextUsage = useContextUsage();
164
  const setAppState = useSetAppState();
165
 
166
  // Poll for OpenRouter models when using 'default' model
 
384
  t18 = $[42];
385
  t19 = $[43];
386
  }
387
+ return <><OffscreenFreeze><Box flexDirection="column" borderStyle="round" borderColor="startupAccent" borderText={t11} paddingX={1} paddingY={1} alignItems="center" width={columns}><Text bold={true}>{welcomeMessage}</Text>{t12}{t13}{contextUsage.hasData && <CtxProgressBar currentTokens={contextUsage.currentTokens} contextWindowTokens={contextUsage.contextWindowTokens} compactionTargetTokens={contextUsage.compactionTargetTokens} utilizationPct={contextUsage.utilizationPct} />}<Text dimColor={true}>{billingType}</Text><Text dimColor={true}>{agentName ? `@${agentName} 路 ${truncatedCwd}` : truncatedCwd}</Text></Box></OffscreenFreeze>{t14}{t15}{t16}{t17}{t18}{t19}</>;
388
  }
389
  const welcomeMessage_0 = formatWelcomeMessage(username);
390
  const modelLine = !process.env.IS_DEMO && config.oauthAccount?.organizationName ? `${modelDisplayName} 路 ${billingType} 路 ${config.oauthAccount.organizationName}` : `${modelDisplayName} 路 ${billingType}`;
 
451
  }
452
  let t22;
453
  if ($[53] !== t20 || $[54] !== t21) {
454
+ t22 = <Box flexDirection="column" alignItems="center">{t20}{contextUsage.hasData && <CtxProgressBar currentTokens={contextUsage.currentTokens} contextWindowTokens={contextUsage.contextWindowTokens} compactionTargetTokens={contextUsage.compactionTargetTokens} utilizationPct={contextUsage.utilizationPct} />}{t21}</Box>;
455
  $[53] = t20;
456
  $[54] = t21;
457
  $[55] = t22;
src/hooks/useContextUsage.ts ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo } from 'react'
2
+ import { getSdkBetas, getTotalInputTokens } from '../bootstrap/state.js'
3
+ import { useMainLoopModel } from './useMainLoopModel.js'
4
+ import {
5
+ getContextWindowForModel,
6
+ calculateContextPercentages,
7
+ } from '../utils/context.js'
8
+ import { getEffectiveContextWindowSize } from '../services/compact/autoCompact.js'
9
+
10
+ export interface ContextUsageInfo {
11
+ currentTokens: number
12
+ contextWindowTokens: number
13
+ compactionTargetTokens: number
14
+ utilizationPct: number
15
+ hasData: boolean
16
+ }
17
+
18
+ export function useContextUsage(): ContextUsageInfo {
19
+ const model = useMainLoopModel()
20
+
21
+ const contextWindowTokens = getContextWindowForModel(
22
+ model,
23
+ getSdkBetas(),
24
+ )
25
+
26
+ const effectiveWindowSize = getEffectiveContextWindowSize(model)
27
+
28
+ const totalInputTokens = getTotalInputTokens()
29
+
30
+ const currentUsage = useMemo(() => {
31
+ // We don't have per-call cache breakdown in global state,
32
+ // so use total input tokens as approximation for current context usage.
33
+ // This works because totalInputTokens tracks cumulative input,
34
+ // but for a live progress bar we want the last API response's context size.
35
+ // For now, use totalInputTokens as a reasonable proxy.
36
+ return {
37
+ input_tokens: totalInputTokens,
38
+ cache_creation_input_tokens: 0,
39
+ cache_read_input_tokens: 0,
40
+ }
41
+ }, [totalInputTokens])
42
+
43
+ const percentages = calculateContextPercentages(
44
+ currentUsage,
45
+ contextWindowTokens,
46
+ )
47
+
48
+ const utilizationPct = percentages.used ?? 0
49
+ const hasData = percentages.used !== null
50
+
51
+ // Compaction target is the threshold at which auto-compact triggers
52
+ const compactionTargetTokens =
53
+ effectiveWindowSize - 13_000 // AUTOCOMPACT_BUFFER_TOKENS
54
+
55
+ return {
56
+ currentTokens: totalInputTokens,
57
+ contextWindowTokens,
58
+ compactionTargetTokens,
59
+ utilizationPct,
60
+ hasData,
61
+ }
62
+ }