Skip to content

Commit f6fe010

Browse files
committed
feat(tui): redesign sidebar layout with silent refresh and topic display
1 parent 87fdbb8 commit f6fe010

File tree

4 files changed

+110
-120
lines changed

4 files changed

+110
-120
lines changed

lib/ui/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,10 @@ export function formatStatsHeader(totalTokensSaved: number, pruneTokenCounter: n
143143
}
144144

145145
export function formatTokenCount(tokens: number): string {
146-
if (tokens >= 1000) {
146+
if (tokens >= 100_000) {
147+
return `${Math.round(tokens / 1000)}K tokens`
148+
}
149+
if (tokens >= 10_000) {
147150
return `${(tokens / 1000).toFixed(1)}K`.replace(".0K", "K") + " tokens"
148151
}
149152
return tokens.toString() + " tokens"

tui/data/context.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const createPlaceholderContextSnapshot = (
5858
available: false,
5959
activeBlockCount: 0,
6060
activeBlockTopics: [],
61+
activeBlockTopicTotal: 0,
6162
},
6263
notes,
6364
loadedAt: Date.now(),
@@ -245,12 +246,13 @@ export const loadContextSnapshot = async (
245246
const { state, persisted } = await buildState(sessionID, messages)
246247
const breakdown = analyzeTokens(state, messages)
247248

248-
const topics = Array.from(state.prune.messages.activeBlockIds)
249+
const allTopics = Array.from(state.prune.messages.activeBlockIds)
249250
.map((blockID) => state.prune.messages.blocksById.get(blockID))
250251
.filter((block): block is NonNullable<typeof block> => !!block)
251252
.map((block) => block.topic)
252253
.filter((topic) => !!topic)
253-
.slice(0, 3)
254+
const topics = allTopics.slice(0, 5)
255+
const topicTotal = allTopics.length
254256

255257
const notes: string[] = []
256258
if (persisted) {
@@ -269,6 +271,7 @@ export const loadContextSnapshot = async (
269271
available: !!persisted,
270272
activeBlockCount: state.prune.messages.activeBlockIds.size,
271273
activeBlockTopics: topics,
274+
activeBlockTopicTotal: topicTotal,
272275
lastUpdated: persisted?.lastUpdated,
273276
},
274277
notes,

tui/shared/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface DcpPersistedSummary {
4242
available: boolean
4343
activeBlockCount: number
4444
activeBlockTopics: string[]
45+
activeBlockTopicTotal: number
4546
lastUpdated?: string
4647
}
4748

tui/slots/sidebar-top.tsx

Lines changed: 100 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ import { getPalette, type DcpPalette } from "../shared/theme"
1414
import type { DcpRouteNames, DcpTuiClient, DcpTuiConfig } from "../shared/types"
1515

1616
const BAR_WIDTH = 12
17+
// Content width derived from graph row: label(9) + space(1) + percent(4) + " |"(2) + bar(12) + "| "(2) + tokens(~5)
18+
const CONTENT_WIDTH = 9 + 1 + 4 + 2 + BAR_WIDTH + 2 + 5
19+
20+
const truncate = (text: string, max: number) =>
21+
text.length > max ? text.slice(0, max - 3) + "..." : text
1722
const REFRESH_DEBOUNCE_MS = 100
1823

1924
const toneColor = (
@@ -40,17 +45,16 @@ const SummaryRow = (props: {
4045
label: string
4146
value: string
4247
tone?: "text" | "muted" | "accent" | "success" | "warning"
48+
marginTop?: number
4349
}) => {
4450
return (
4551
<box
4652
width="100%"
47-
backgroundColor={props.palette.surface}
48-
paddingLeft={1}
49-
paddingRight={1}
5053
flexDirection="row"
5154
justifyContent="space-between"
55+
marginTop={props.marginTop}
5256
>
53-
<text fg={props.palette.muted}>{props.label}</text>
57+
<text fg={props.palette.text}>{props.label}</text>
5458
<text fg={toneColor(props.palette, props.tone)}>
5559
<b>{props.value}</b>
5660
</text>
@@ -69,12 +73,15 @@ const SidebarContextBar = (props: {
6973
const percent = createMemo(() =>
7074
props.total > 0 ? `${Math.round((props.value / props.total) * 100)}%` : "0%",
7175
)
72-
const label = createMemo(() => props.label.padEnd(8, " "))
76+
const label = createMemo(() => props.label.padEnd(9, " "))
7377
const bar = createMemo(() => buildBar(props.value, props.total, props.char))
7478
return (
75-
<text
76-
fg={toneColor(props.palette, props.tone)}
77-
>{`${label()} ${percent().padStart(4, " ")} |${bar()}| ${compactTokenCount(props.value)}`}</text>
79+
<box flexDirection="row">
80+
<text fg={props.palette.text}>{label()}</text>
81+
<text fg={toneColor(props.palette, props.tone)}>
82+
{` ${percent().padStart(4, " ")} |${bar()}| ${compactTokenCount(props.value).padStart(5, " ")}`}
83+
</text>
84+
</box>
7885
)
7986
}
8087

@@ -149,6 +156,7 @@ const SidebarContext = (props: {
149156
}
150157

151158
const cached = peekContextSnapshot(sessionID)
159+
let silentRefresh = false
152160
if (cached) {
153161
void props.logger.debug("Sidebar using cached snapshot before reload", {
154162
sessionID,
@@ -159,18 +167,24 @@ const SidebarContext = (props: {
159167
setLoading(false)
160168
} else {
161169
const current = untrack(snapshot)
162-
if (!preserveSnapshot || current?.sessionID !== sessionID) {
170+
if (preserveSnapshot && current?.sessionID === sessionID) {
171+
silentRefresh = true
172+
void props.logger.debug("Sidebar silent refresh, keeping current snapshot", {
173+
sessionID,
174+
})
175+
} else {
163176
setSnapshot(createPlaceholderContextSnapshot(sessionID, ["Loading DCP context..."]))
177+
setLoading(true)
178+
void props.logger.debug("Sidebar entering loading state", {
179+
sessionID,
180+
hadCurrentSnapshot: !!current,
181+
})
164182
}
165-
setLoading(true)
166-
void props.logger.debug("Sidebar entering loading state", {
167-
sessionID,
168-
hadCurrentSnapshot: !!current,
169-
preservedSnapshot: preserveSnapshot && current?.sessionID === sessionID,
170-
})
171183
}
172184
setError(undefined)
173-
requestRender("refresh-start", { sessionID, reason })
185+
if (!silentRefresh) {
186+
requestRender("refresh-start", { sessionID, reason })
187+
}
174188

175189
const currentRequest = ++requestVersion
176190
void props.logger.debug("Sidebar refresh request issued", {
@@ -357,44 +371,14 @@ const SidebarContext = (props: {
357371
),
358372
)
359373

360-
const prunedItems = createMemo(() => {
361-
const value = snapshot()
362-
const parts: string[] = []
363-
if (value.breakdown.prunedToolCount > 0) {
364-
parts.push(
365-
`${value.breakdown.prunedToolCount} tool${value.breakdown.prunedToolCount === 1 ? "" : "s"}`,
366-
)
367-
}
368-
if (value.breakdown.prunedMessageCount > 0) {
369-
parts.push(
370-
`${value.breakdown.prunedMessageCount} msg${value.breakdown.prunedMessageCount === 1 ? "" : "s"}`,
371-
)
372-
}
373-
return parts.length > 0 ? `${parts.join(", ")} pruned` : "No pruned items"
374-
})
375-
376374
const blockSummary = createMemo(() => {
377375
return `${snapshot().persisted.activeBlockCount}`
378376
})
379377

380-
const topicLine = createMemo(() => {
381-
const value = snapshot()
382-
if (!value.persisted.activeBlockTopics.length) return ""
383-
return `Topics: ${value.persisted.activeBlockTopics.join(" | ")}`
384-
})
385-
386-
const noteLine = createMemo(() => {
387-
const topic = topicLine()
388-
if (topic) return topic
389-
return snapshot().notes[0] ?? ""
390-
})
391-
392-
const stateLine = createMemo(() => {
393-
if (error() && snapshot().breakdown.total === 0) return "DCP context failed to load."
394-
if (error()) return `Refresh failed: ${error()}`
395-
if (loading()) return "Loading DCP context..."
396-
return "DCP context loaded."
397-
})
378+
const topics = createMemo(() => snapshot().persisted.activeBlockTopics)
379+
const topicTotal = createMemo(() => snapshot().persisted.activeBlockTopicTotal)
380+
const topicOverflow = createMemo(() => topicTotal() - topics().length)
381+
const fallbackNote = createMemo(() => snapshot().notes[0] ?? "")
398382

399383
const status = createMemo(() => {
400384
if (error() && snapshot().breakdown.total > 0)
@@ -411,7 +395,7 @@ const SidebarContext = (props: {
411395
width="100%"
412396
flexDirection="column"
413397
gap={0}
414-
backgroundColor={props.palette.base}
398+
backgroundColor={props.palette.surface}
415399
border={{ type: "single" }}
416400
borderColor={props.palette.accent}
417401
paddingTop={1}
@@ -427,87 +411,86 @@ const SidebarContext = (props: {
427411
<b>{props.config.label}</b>
428412
</text>
429413
</box>
430-
<text fg={props.palette.muted}>click for more</text>
414+
<text fg={props.palette.text}>click for more</text>
431415
</box>
432416
<text fg={toneColor(props.palette, status().tone)}>{status().label}</text>
433417
</box>
434418

435419
<box flexDirection="row" justifyContent="space-between">
436-
<text fg={props.palette.muted}>session {props.sessionID().slice(0, 18)}</text>
437-
</box>
438-
439-
<box paddingTop={1}>
440-
<text fg={error() ? props.palette.warning : props.palette.muted}>
441-
{stateLine()}
420+
<text fg={props.palette.muted}>
421+
{props.sessionID().length > 27
422+
? props.sessionID().slice(0, 27) + "..."
423+
: props.sessionID()}
442424
</text>
443425
</box>
444426

427+
<SummaryRow
428+
palette={props.palette}
429+
label="Saved"
430+
value={`~${compactTokenCount(snapshot().breakdown.prunedTokens)}`}
431+
tone="accent"
432+
marginTop={1}
433+
/>
434+
<SummaryRow
435+
palette={props.palette}
436+
label="Compressions"
437+
value={blockSummary()}
438+
tone="accent"
439+
/>
440+
445441
<box width="100%" flexDirection="column" gap={0} paddingTop={1}>
446-
<box
447-
width="100%"
448-
flexDirection="row"
449-
justifyContent="space-between"
450-
backgroundColor={props.palette.surface}
451-
paddingLeft={1}
452-
paddingRight={1}
453-
>
454-
<text fg={props.palette.muted}>Current</text>
455-
<text fg={props.palette.accent}>
456-
<b>~{compactTokenCount(snapshot().breakdown.total)}</b>
457-
</text>
458-
</box>
459-
<SummaryRow
442+
<SidebarContextBar
460443
palette={props.palette}
461-
label="Saved"
462-
value={`~${compactTokenCount(snapshot().breakdown.prunedTokens)}`}
463-
tone="success"
444+
label="System"
445+
value={snapshot().breakdown.system}
446+
total={snapshot().breakdown.total}
447+
char="█"
448+
tone="accent"
464449
/>
465-
<SummaryRow
450+
<SidebarContextBar
466451
palette={props.palette}
467-
label="Compressions"
468-
value={blockSummary()}
452+
label="User"
453+
value={snapshot().breakdown.user}
454+
total={snapshot().breakdown.total}
455+
char="█"
469456
tone="accent"
470457
/>
458+
<SidebarContextBar
459+
palette={props.palette}
460+
label="Assistant"
461+
value={snapshot().breakdown.assistant}
462+
total={snapshot().breakdown.total}
463+
char="█"
464+
tone="accent"
465+
/>
466+
<SidebarContextBar
467+
palette={props.palette}
468+
label="Tools"
469+
value={snapshot().breakdown.tools}
470+
total={snapshot().breakdown.total}
471+
char="█"
472+
tone="accent"
473+
/>
474+
</box>
471475

472-
<box width="100%" flexDirection="column" gap={0} paddingTop={1}>
473-
<SidebarContextBar
474-
palette={props.palette}
475-
label="System"
476-
value={snapshot().breakdown.system}
477-
total={snapshot().breakdown.total}
478-
char="█"
479-
tone="accent"
480-
/>
481-
<SidebarContextBar
482-
palette={props.palette}
483-
label="User"
484-
value={snapshot().breakdown.user}
485-
total={snapshot().breakdown.total}
486-
char="▓"
487-
tone="text"
488-
/>
489-
<SidebarContextBar
490-
palette={props.palette}
491-
label="Assist"
492-
value={snapshot().breakdown.assistant}
493-
total={snapshot().breakdown.total}
494-
char="▒"
495-
tone="muted"
496-
/>
497-
<SidebarContextBar
498-
palette={props.palette}
499-
label="Tools"
500-
value={snapshot().breakdown.tools}
501-
total={snapshot().breakdown.total}
502-
char="░"
503-
tone="warning"
504-
/>
505-
</box>
506-
507-
<box width="100%" flexDirection="column" gap={0} paddingTop={1}>
508-
<text fg={props.palette.muted}>{prunedItems()}</text>
509-
<text fg={props.palette.muted}>{noteLine()}</text>
510-
</box>
476+
<box width="100%" flexDirection="column" gap={0} paddingTop={1}>
477+
{topics().length > 0 ? (
478+
<>
479+
<text fg={props.palette.text}>
480+
<b>Compressed Topics</b>
481+
</text>
482+
{topics().map((t) => (
483+
<text fg={props.palette.muted}>{truncate(t, CONTENT_WIDTH)}</text>
484+
))}
485+
{topicOverflow() > 0 ? (
486+
<text fg={props.palette.muted} dim>
487+
... {topicOverflow()} more topics
488+
</text>
489+
) : null}
490+
</>
491+
) : fallbackNote() ? (
492+
<text fg={props.palette.muted}>{fallbackNote()}</text>
493+
) : null}
511494
</box>
512495
</box>
513496
)

0 commit comments

Comments
 (0)