Skip to content

Commit 16181cc

Browse files
outsourc-eoutsourc-e
andauthored
feat: activate context bar, fix token counting, port 3002 (#32)
- Render ContextBar in chat screen (was imported but never placed) - Pass sessionId to context-usage API for accurate token data - Count all tokens (cached + uncached) for real context window usage - Model-aware max tokens (200k for Claude, 128k for GPT) - Return model name in context-usage response - Remove header border-b (context bar replaces separator) - Remove context bar own border-b for clean look - Change dev server port from 3000 to 3002 Co-authored-by: outsourc-e <eric@outsourc.e>
1 parent d1ab68b commit 16181cc

5 files changed

Lines changed: 60 additions & 24 deletions

File tree

src/routes/api/context-usage.ts

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,67 @@ export const Route = createFileRoute('/api/context-usage')({
1717
try {
1818
// Try to get session token usage from Hermes
1919
let usedTokens = 0
20-
let maxTokens = 128000 // default context window
20+
let maxTokens = 200000 // default context window
21+
let model = ''
22+
23+
// Known context window sizes for common models
24+
const MODEL_CONTEXT: Record<string, number> = {
25+
'claude-opus-4-6': 200000,
26+
'claude-sonnet-4-6': 200000,
27+
'claude-sonnet-4': 200000,
28+
'claude-opus-4': 200000,
29+
'claude-3-5-sonnet': 200000,
30+
'claude-3-opus': 200000,
31+
'gpt-5.4': 128000,
32+
'gpt-4o': 128000,
33+
'gpt-4-turbo': 128000,
34+
}
2135

2236
if (sessionId) {
2337
const res = await fetch(`${HERMES_API}/api/sessions/${sessionId}`, {
2438
signal: AbortSignal.timeout(3000),
2539
})
2640
if (res.ok) {
27-
const data = (await res.json()) as { session?: { input_tokens?: number; output_tokens?: number } }
41+
const data = (await res.json()) as { session?: {
42+
input_tokens?: number
43+
output_tokens?: number
44+
cache_read_tokens?: number
45+
cache_write_tokens?: number
46+
model?: string
47+
} }
2848
const session = data.session
2949
if (session) {
30-
usedTokens = (session.input_tokens || 0) + (session.output_tokens || 0)
50+
// Total context = all tokens in the window (cached + uncached)
51+
usedTokens = (session.input_tokens || 0)
52+
+ (session.output_tokens || 0)
53+
+ (session.cache_read_tokens || 0)
54+
+ (session.cache_write_tokens || 0)
55+
model = session.model || ''
56+
57+
// Set max based on model
58+
const modelKey = Object.keys(MODEL_CONTEXT).find(
59+
(k) => model.toLowerCase().includes(k.toLowerCase()),
60+
)
61+
if (modelKey) maxTokens = MODEL_CONTEXT[modelKey]
3162
}
3263
}
3364
}
3465

35-
// Try to get model context window from /v1/models
36-
try {
37-
const modelsRes = await fetch(`${HERMES_API}/v1/models`, {
38-
signal: AbortSignal.timeout(3000),
39-
})
40-
if (modelsRes.ok) {
41-
const modelsData = (await modelsRes.json()) as { data?: Array<{ context_length?: number }> }
42-
const firstModel = modelsData.data?.[0]
43-
if (firstModel?.context_length) {
44-
maxTokens = firstModel.context_length
66+
// Fallback: try /v1/models for context_length
67+
if (maxTokens === 200000 && !model) {
68+
try {
69+
const modelsRes = await fetch(`${HERMES_API}/v1/models`, {
70+
signal: AbortSignal.timeout(3000),
71+
})
72+
if (modelsRes.ok) {
73+
const modelsData = (await modelsRes.json()) as { data?: Array<{ context_length?: number }> }
74+
const firstModel = modelsData.data?.[0]
75+
if (firstModel?.context_length) {
76+
maxTokens = firstModel.context_length
77+
}
4578
}
46-
}
47-
} catch { /* use default */ }
79+
} catch { /* use default */ }
80+
}
4881

4982
const contextPercent = maxTokens > 0 ? Math.round((usedTokens / maxTokens) * 100) : 0
5083

@@ -53,7 +86,7 @@ export const Route = createFileRoute('/api/context-usage')({
5386
contextPercent,
5487
maxTokens,
5588
usedTokens,
56-
model: '',
89+
model,
5790
staticTokens: 0,
5891
conversationTokens: usedTokens,
5992
})

src/screens/chat/chat-screen.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2399,6 +2399,8 @@ export function ChatScreen({
23992399
</div>
24002400
)}
24012401

2402+
{hideUi ? null : <ContextBar sessionId={activeSession?.key || activeSessionKey || resolvedSessionKey} />}
2403+
24022404
{hideUi ? null : (
24032405
<ChatMessageList
24042406
messages={finalDisplayMessages}

src/screens/chat/components/chat-header.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ function ChatHeaderComponent({
229229
return (
230230
<div
231231
ref={wrapperRef}
232-
className="shrink-0 border-b border-primary-200 bg-surface transition-transform"
232+
className="shrink-0 bg-surface transition-transform"
233233
style={pullOffset > 0 ? { transform: `translateY(${pullOffset}px)` } : undefined}
234234
>
235235
<div className="px-3 h-12 flex items-center gap-0">
@@ -271,7 +271,7 @@ function ChatHeaderComponent({
271271
return (
272272
<div
273273
ref={wrapperRef}
274-
className="shrink-0 border-b border-primary-200 bg-surface"
274+
className="shrink-0 bg-surface"
275275
>
276276
<div className="px-4 h-12 flex items-center">
277277
{showFileExplorerButton ? (

src/screens/chat/components/context-bar.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function formatTokens(n: number): string {
3030
return String(n)
3131
}
3232

33-
function ContextBarComponent({ compact: _compact }: { compact?: boolean }) {
33+
function ContextBarComponent({ compact: _compact, sessionId }: { compact?: boolean; sessionId?: string }) {
3434
const [ctx, setCtx] = useState<ContextData>(EMPTY)
3535
const [showLabel, setShowLabel] = useState(false)
3636
const [isMobile, setIsMobile] = useState(false)
@@ -45,7 +45,8 @@ function ContextBarComponent({ compact: _compact }: { compact?: boolean }) {
4545

4646
const refresh = useCallback(async () => {
4747
try {
48-
const res = await fetch('/api/context-usage')
48+
const params = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : ''
49+
const res = await fetch(`/api/context-usage${params}`)
4950
if (!res.ok) return
5051
const data = await res.json()
5152
if (data.ok) {
@@ -59,7 +60,7 @@ function ContextBarComponent({ compact: _compact }: { compact?: boolean }) {
5960
} catch {
6061
/* ignore */
6162
}
62-
}, [])
63+
}, [sessionId])
6364

6465
useEffect(() => {
6566
void refresh()
@@ -143,7 +144,7 @@ function ContextBarComponent({ compact: _compact }: { compact?: boolean }) {
143144
<PreviewCardTrigger className="block w-full cursor-pointer">
144145
<div
145146
className={cn(
146-
'shrink-0 w-full h-2 border-b border-primary-200/50 dark:border-primary-700/30 transition-colors duration-300 relative',
147+
'shrink-0 w-full h-2 transition-colors duration-300 relative',
147148
barBg,
148149
)}
149150
>

vite.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,8 @@ const config = defineConfig(({ mode, command }) => {
388388
server: {
389389
// Force IPv4 — 'localhost' resolves to ::1 (IPv6) on Windows, breaking connectivity
390390
host: '0.0.0.0',
391-
port: 3000,
392-
strictPort: false, // allow fallback if 3000 is taken, but log clearly
391+
port: 3002,
392+
strictPort: false, // allow fallback if 3002 is taken, but log clearly
393393
allowedHosts: true,
394394
watch: {
395395
// Exclude generated route tree — TanStack Router's file watcher

0 commit comments

Comments
 (0)