@tokentop/ttop 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -4
- package/src/agents/aggregator.ts +8 -1
- package/src/agents/types.ts +3 -0
- package/src/cli.ts +2 -2
- package/src/demo/simulator.ts +1 -1
- package/src/plugins/agents/index.ts +2 -0
- package/src/plugins/auth-sources.ts +2 -3
- package/src/plugins/lifecycle.ts +17 -13
- package/src/plugins/loader.test.ts +1 -1
- package/src/plugins/loader.ts +15 -7
- package/src/plugins/notification-bus.ts +5 -2
- package/src/plugins/notifications/terminal-bell.ts +9 -2
- package/src/plugins/notifications/visual-flash.ts +13 -3
- package/src/plugins/plugin-context-factory.ts +4 -19
- package/src/plugins/providers/anthropic.ts +2 -2
- package/src/plugins/providers/antigravity.ts +1 -1
- package/src/plugins/providers/chutes.ts +189 -0
- package/src/plugins/providers/codex.ts +1 -1
- package/src/plugins/providers/gemini.ts +1 -1
- package/src/plugins/providers/github-copilot.ts +1 -1
- package/src/plugins/providers/index.ts +1 -0
- package/src/plugins/providers/minimax.ts +1 -1
- package/src/plugins/providers/openai-api.ts +1 -1
- package/src/plugins/providers/opencode-zen.ts +1 -1
- package/src/plugins/providers/perplexity.ts +1 -1
- package/src/plugins/providers/zai.ts +4 -2
- package/src/plugins/registry.ts +51 -46
- package/src/plugins/sandbox-guard.test.ts +1 -1
- package/src/plugins/sandbox-guard.ts +1 -1
- package/src/plugins/sandbox.ts +1 -1
- package/src/plugins/themes/catppuccin-latte.ts +1 -1
- package/src/plugins/themes/catppuccin-mocha.ts +1 -1
- package/src/plugins/themes/claude-code.ts +1 -1
- package/src/plugins/themes/dracula.ts +1 -1
- package/src/plugins/themes/github-light.ts +1 -1
- package/src/plugins/themes/gruvbox-dark.ts +1 -1
- package/src/plugins/themes/gruvbox-light.ts +1 -1
- package/src/plugins/themes/kanagawa.ts +1 -1
- package/src/plugins/themes/nord.ts +1 -1
- package/src/plugins/themes/one-dark.ts +1 -1
- package/src/plugins/themes/opencode.ts +1 -1
- package/src/plugins/themes/rose-pine-dawn.ts +1 -1
- package/src/plugins/themes/rose-pine.ts +1 -1
- package/src/plugins/themes/solarized-light.ts +1 -1
- package/src/plugins/themes/tokyo-night.ts +1 -1
- package/src/plugins/update-checker.ts +2 -2
- package/src/pricing/estimator.test.ts +1 -1
- package/src/pricing/estimator.ts +1 -1
- package/src/pricing/fallback.ts +1 -1
- package/src/pricing/index.ts +1 -1
- package/src/pricing/models-dev.ts +1 -1
- package/src/storage/index.ts +6 -0
- package/src/storage/migrations/index.ts +17 -0
- package/src/storage/persistence-service.ts +206 -0
- package/src/storage/repos/pluginStorage.ts +35 -0
- package/src/storage/retention.ts +146 -0
- package/src/tui/App.tsx +1 -1
- package/src/tui/components/DebugPanel.tsx +23 -10
- package/src/tui/components/ProviderCard.tsx +1 -1
- package/src/tui/components/ProviderDetailPanel.tsx +1 -1
- package/src/tui/components/SessionDetailsDrawer.tsx +10 -0
- package/src/tui/components/SessionsTable.tsx +15 -4
- package/src/tui/components/SettingsModal.tsx +383 -76
- package/src/tui/components/ThemePicker.tsx +1 -1
- package/src/tui/contexts/AgentSessionContext.tsx +44 -80
- package/src/tui/contexts/PluginContext.tsx +13 -3
- package/src/tui/contexts/RealTimeActivityContext.tsx +33 -20
- package/src/tui/contexts/StorageContext.tsx +42 -130
- package/src/tui/contexts/ThemeContext.tsx +3 -3
- package/src/tui/hooks/useExitAnimation.ts +4 -0
- package/src/tui/hooks/useSessionEnrichedProviders.ts +1 -1
- package/src/tui/views/Dashboard.tsx +40 -3
- package/src/tui/views/ProjectsView.tsx +29 -6
- package/src/tui/views/RealTimeDashboard.tsx +29 -8
- package/src/tui/views/SettingsView.tsx +9 -4
- package/src/version.ts +1 -1
- package/src/plugins/types/agent.ts +0 -108
- package/src/plugins/types/base.ts +0 -278
- package/src/plugins/types/index.ts +0 -19
- package/src/plugins/types/notification.ts +0 -51
- package/src/plugins/types/provider.ts +0 -237
- package/src/plugins/types/theme.ts +0 -78
- package/src/storage/database.ts +0 -623
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data retention policy — prevents unbounded database growth.
|
|
3
|
+
*
|
|
4
|
+
* Detailed snapshot data (agent_session_snapshots, agent_session_stream_snapshots,
|
|
5
|
+
* provider_snapshots) is pruned after a retention window. Aggregated data in
|
|
6
|
+
* usage_events, hourly_aggregates, and daily_aggregates is kept longer since
|
|
7
|
+
* those tables grow much more slowly.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getDatabase, isDatabaseInitialized } from "./db.ts";
|
|
11
|
+
|
|
12
|
+
/** Keep detailed snapshots for 7 days */
|
|
13
|
+
const SNAPSHOT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
/** Keep provider snapshots for 7 days */
|
|
16
|
+
const PROVIDER_SNAPSHOT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
/** Keep usage events for 90 days */
|
|
19
|
+
const USAGE_EVENT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
/** Maximum rows to delete in a single batch (prevents long-running transactions) */
|
|
22
|
+
const PRUNE_BATCH_SIZE = 10_000;
|
|
23
|
+
|
|
24
|
+
export interface PruneResult {
|
|
25
|
+
agentSessionSnapshots: number;
|
|
26
|
+
providerSnapshots: number;
|
|
27
|
+
usageEvents: number;
|
|
28
|
+
durationMs: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Prune old data from the database. Deletes in batches to avoid
|
|
33
|
+
* holding a write lock for too long.
|
|
34
|
+
*/
|
|
35
|
+
export function pruneOldData(nowMs: number = Date.now()): PruneResult {
|
|
36
|
+
if (!isDatabaseInitialized()) {
|
|
37
|
+
return { agentSessionSnapshots: 0, providerSnapshots: 0, usageEvents: 0, durationMs: 0 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const db = getDatabase();
|
|
41
|
+
const start = performance.now();
|
|
42
|
+
|
|
43
|
+
let agentSessionSnapshots = 0;
|
|
44
|
+
let providerSnapshots = 0;
|
|
45
|
+
let usageEvents = 0;
|
|
46
|
+
|
|
47
|
+
const snapshotCutoff = nowMs - SNAPSHOT_RETENTION_MS;
|
|
48
|
+
const providerCutoff = nowMs - PROVIDER_SNAPSHOT_RETENTION_MS;
|
|
49
|
+
const usageEventCutoff = nowMs - USAGE_EVENT_RETENTION_MS;
|
|
50
|
+
|
|
51
|
+
// Prune agent_session_snapshots in batches.
|
|
52
|
+
// CASCADE delete handles agent_session_stream_snapshots automatically.
|
|
53
|
+
{
|
|
54
|
+
const stmt = db.prepare(`
|
|
55
|
+
DELETE FROM agent_session_snapshots
|
|
56
|
+
WHERE id IN (
|
|
57
|
+
SELECT id FROM agent_session_snapshots
|
|
58
|
+
WHERE timestamp < ?
|
|
59
|
+
LIMIT ?
|
|
60
|
+
)
|
|
61
|
+
`);
|
|
62
|
+
|
|
63
|
+
let deleted: number;
|
|
64
|
+
do {
|
|
65
|
+
const result = stmt.run(snapshotCutoff, PRUNE_BATCH_SIZE);
|
|
66
|
+
deleted = result.changes;
|
|
67
|
+
agentSessionSnapshots += deleted;
|
|
68
|
+
} while (deleted === PRUNE_BATCH_SIZE);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Prune old provider_snapshots in batches
|
|
72
|
+
{
|
|
73
|
+
const stmt = db.prepare(`
|
|
74
|
+
DELETE FROM provider_snapshots
|
|
75
|
+
WHERE id IN (
|
|
76
|
+
SELECT id FROM provider_snapshots
|
|
77
|
+
WHERE timestamp < ?
|
|
78
|
+
LIMIT ?
|
|
79
|
+
)
|
|
80
|
+
`);
|
|
81
|
+
|
|
82
|
+
let deleted: number;
|
|
83
|
+
do {
|
|
84
|
+
const result = stmt.run(providerCutoff, PRUNE_BATCH_SIZE);
|
|
85
|
+
deleted = result.changes;
|
|
86
|
+
providerSnapshots += deleted;
|
|
87
|
+
} while (deleted === PRUNE_BATCH_SIZE);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Prune old usage_events in batches
|
|
91
|
+
{
|
|
92
|
+
const stmt = db.prepare(`
|
|
93
|
+
DELETE FROM usage_events
|
|
94
|
+
WHERE id IN (
|
|
95
|
+
SELECT id FROM usage_events
|
|
96
|
+
WHERE timestamp < ?
|
|
97
|
+
LIMIT ?
|
|
98
|
+
)
|
|
99
|
+
`);
|
|
100
|
+
|
|
101
|
+
let deleted: number;
|
|
102
|
+
do {
|
|
103
|
+
const result = stmt.run(usageEventCutoff, PRUNE_BATCH_SIZE);
|
|
104
|
+
deleted = result.changes;
|
|
105
|
+
usageEvents += deleted;
|
|
106
|
+
} while (deleted === PRUNE_BATCH_SIZE);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const durationMs = Math.round(performance.now() - start);
|
|
110
|
+
|
|
111
|
+
return { agentSessionSnapshots, providerSnapshots, usageEvents, durationMs };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run incremental auto_vacuum if supported (WAL mode).
|
|
116
|
+
* This reclaims free pages without blocking readers.
|
|
117
|
+
*/
|
|
118
|
+
export function incrementalVacuum(pages = 1000): void {
|
|
119
|
+
if (!isDatabaseInitialized()) return;
|
|
120
|
+
|
|
121
|
+
const db = getDatabase();
|
|
122
|
+
try {
|
|
123
|
+
db.exec(`PRAGMA incremental_vacuum(${pages})`);
|
|
124
|
+
} catch {
|
|
125
|
+
// Not all builds support incremental vacuum — silently skip
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the database file size in bytes. Returns 0 if unable to determine.
|
|
131
|
+
*/
|
|
132
|
+
export function getDatabaseSizeBytes(): number {
|
|
133
|
+
if (!isDatabaseInitialized()) return 0;
|
|
134
|
+
|
|
135
|
+
const db = getDatabase();
|
|
136
|
+
try {
|
|
137
|
+
const row = db.prepare("PRAGMA page_count").get() as { page_count: number } | null;
|
|
138
|
+
const sizeRow = db.prepare("PRAGMA page_size").get() as { page_size: number } | null;
|
|
139
|
+
if (row && sizeRow) {
|
|
140
|
+
return row.page_count * sizeRow.page_size;
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// ignore
|
|
144
|
+
}
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
package/src/tui/App.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import type { ThemePlugin } from "@tokentop/plugin-sdk";
|
|
2
3
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
4
|
import * as builtinThemeExports from "@/plugins/themes/index.ts";
|
|
4
|
-
import type { ThemePlugin } from "@/plugins/types/theme.ts";
|
|
5
5
|
import { resolveTheme, ThemeProvider, useColors, useTheme } from "./contexts/ThemeContext.tsx";
|
|
6
6
|
import {
|
|
7
7
|
type BurstRecorder,
|
|
@@ -9,7 +9,7 @@ import { useColors } from "../contexts/ThemeContext.tsx";
|
|
|
9
9
|
import { ModalBackdrop, Z_INDEX } from "./ModalBackdrop.tsx";
|
|
10
10
|
|
|
11
11
|
type DebugTab = "logs" | "inspector" | "plugins";
|
|
12
|
-
type PluginSort = "name" | "type" | "
|
|
12
|
+
type PluginSort = "name" | "type" | "source";
|
|
13
13
|
|
|
14
14
|
interface DebugInspectorData {
|
|
15
15
|
sessions: Array<{
|
|
@@ -193,7 +193,7 @@ export function DebugPanel({ onClose, inspectorData }: DebugPanelProps) {
|
|
|
193
193
|
if (activeTab === "plugins") {
|
|
194
194
|
if (key.name === "s") {
|
|
195
195
|
setPluginSort((prev) => {
|
|
196
|
-
const order: PluginSort[] = ["name", "type", "
|
|
196
|
+
const order: PluginSort[] = ["name", "type", "source"];
|
|
197
197
|
return order[(order.indexOf(prev) + 1) % order.length] ?? "name";
|
|
198
198
|
});
|
|
199
199
|
return;
|
|
@@ -656,12 +656,12 @@ interface PluginsTabProps {
|
|
|
656
656
|
|
|
657
657
|
const TYPE_ORDER: Record<string, number> = { provider: 0, agent: 1, theme: 2, notification: 3 };
|
|
658
658
|
|
|
659
|
-
function
|
|
659
|
+
function getSourcePriority(pluginId: string, pluginType: string): number {
|
|
660
660
|
const source = pluginRegistry.getSource(
|
|
661
661
|
pluginType as "provider" | "agent" | "theme" | "notification",
|
|
662
662
|
pluginId,
|
|
663
663
|
);
|
|
664
|
-
return source === "
|
|
664
|
+
return source === "local" ? 0 : source === "npm" ? 1 : 2;
|
|
665
665
|
}
|
|
666
666
|
|
|
667
667
|
function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
|
|
@@ -671,10 +671,10 @@ function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
|
|
|
671
671
|
if (sortBy === "name") {
|
|
672
672
|
return a.name.localeCompare(b.name);
|
|
673
673
|
}
|
|
674
|
-
if (sortBy === "
|
|
675
|
-
const
|
|
676
|
-
const
|
|
677
|
-
if (
|
|
674
|
+
if (sortBy === "source") {
|
|
675
|
+
const aPriority = getSourcePriority(a.id, a.type);
|
|
676
|
+
const bPriority = getSourcePriority(b.id, b.type);
|
|
677
|
+
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
678
678
|
return a.name.localeCompare(b.name);
|
|
679
679
|
}
|
|
680
680
|
const typeA = TYPE_ORDER[a.type] ?? 99;
|
|
@@ -733,6 +733,9 @@ function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
|
|
|
733
733
|
<text width={14} fg={colors.textSubtle}>
|
|
734
734
|
{padRight("TYPE", 14)}
|
|
735
735
|
</text>
|
|
736
|
+
<text width={10} fg={colors.textSubtle}>
|
|
737
|
+
{padRight("SOURCE", 10)}
|
|
738
|
+
</text>
|
|
736
739
|
<text width={14} fg={colors.textSubtle}>
|
|
737
740
|
{padRight("VERSION", 14)}
|
|
738
741
|
</text>
|
|
@@ -743,7 +746,14 @@ function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
|
|
|
743
746
|
{allPlugins.map((plugin) => {
|
|
744
747
|
const key = `${plugin.type}-${plugin.id}`;
|
|
745
748
|
const updateInfo = updates.get(key);
|
|
746
|
-
const
|
|
749
|
+
const source = pluginRegistry.getSource(plugin.type, plugin.id) ?? "builtin";
|
|
750
|
+
const official = pluginRegistry.isOfficial(plugin.type, plugin.id);
|
|
751
|
+
|
|
752
|
+
const sourceColors: Record<string, string> = {
|
|
753
|
+
builtin: colors.textSubtle,
|
|
754
|
+
local: colors.success,
|
|
755
|
+
npm: colors.info,
|
|
756
|
+
};
|
|
747
757
|
|
|
748
758
|
let versionSuffix = "";
|
|
749
759
|
let versionColor = colors.textMuted;
|
|
@@ -758,12 +768,15 @@ function PluginsTab({ colors, scrollboxRef, sortBy }: PluginsTabProps) {
|
|
|
758
768
|
return (
|
|
759
769
|
<box key={key} flexDirection="row" height={1}>
|
|
760
770
|
<text width={22}>
|
|
761
|
-
{official && <span fg={colors.
|
|
771
|
+
{official && <span fg={colors.warning}>✦ </span>}
|
|
762
772
|
<span fg={colors.text}>{padRight(plugin.name, official ? 20 : 22)}</span>
|
|
763
773
|
</text>
|
|
764
774
|
<text width={14} fg={typeColors[plugin.type] ?? colors.textMuted}>
|
|
765
775
|
{padRight(plugin.type, 14)}
|
|
766
776
|
</text>
|
|
777
|
+
<text width={10} fg={sourceColors[source] ?? colors.textMuted}>
|
|
778
|
+
{padRight(source, 10)}
|
|
779
|
+
</text>
|
|
767
780
|
<text width={14} fg={versionColor}>
|
|
768
781
|
{padRight(plugin.version + versionSuffix, 14)}
|
|
769
782
|
</text>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BoxRenderable } from "@opentui/core";
|
|
2
2
|
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import type { ProviderUsageData } from "@tokentop/plugin-sdk";
|
|
3
4
|
import { forwardRef, useCallback, useEffect, useState } from "react";
|
|
4
|
-
import type { ProviderUsageData } from "@/plugins/types/provider.ts";
|
|
5
5
|
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
6
6
|
import { SkeletonProviderContent } from "./Skeleton.tsx";
|
|
7
7
|
import { useSpinner } from "./Spinner.tsx";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { UsageLimit } from "
|
|
1
|
+
import type { UsageLimit } from "@tokentop/plugin-sdk";
|
|
2
2
|
import type { ProviderState } from "../contexts/PluginContext.tsx";
|
|
3
3
|
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
4
4
|
import { Sparkline } from "./Sparkline.tsx";
|
|
@@ -314,6 +314,16 @@ export function SessionDetailsDrawer({ session, onClose: _onClose }: SessionDeta
|
|
|
314
314
|
</text>
|
|
315
315
|
</box>
|
|
316
316
|
|
|
317
|
+
{session.metadata?.isEstimated === true && (
|
|
318
|
+
<box flexDirection="row" height={1} marginBottom={0}>
|
|
319
|
+
<text height={1} overflow="hidden">
|
|
320
|
+
<span fg={colors.warning}>
|
|
321
|
+
≈ Token data is estimated — server enrichment pending
|
|
322
|
+
</span>
|
|
323
|
+
</text>
|
|
324
|
+
</box>
|
|
325
|
+
)}
|
|
326
|
+
|
|
317
327
|
{hasCacheData && (
|
|
318
328
|
<box flexDirection="row" height={1} marginBottom={0}>
|
|
319
329
|
<text height={1} overflow="hidden">
|
|
@@ -154,6 +154,7 @@ const SessionRow = memo(function SessionRow({
|
|
|
154
154
|
const effectiveTokens = session.totals.input + session.totals.output;
|
|
155
155
|
const cacheRead = session.totals.cacheRead ?? 0;
|
|
156
156
|
const hasCacheData = cacheRead > 0;
|
|
157
|
+
const isEstimated = session.metadata?.isEstimated === true;
|
|
157
158
|
const costUsd = session.totalCostUsd ?? 0;
|
|
158
159
|
|
|
159
160
|
const animatedTokens = useAnimatedValue(effectiveTokens, { durationMs: 300, precision: 0 });
|
|
@@ -260,7 +261,10 @@ const SessionRow = memo(function SessionRow({
|
|
|
260
261
|
{String(session.requestCount).padStart(4)}
|
|
261
262
|
</text>
|
|
262
263
|
<text width={14} height={1} {...bgProp}>
|
|
263
|
-
<span fg={tokenColor}>
|
|
264
|
+
<span fg={tokenColor}>
|
|
265
|
+
{isEstimated ? "≈" : " "}
|
|
266
|
+
{formatTokensVal(animatedTokens).padStart(7)}
|
|
267
|
+
</span>
|
|
264
268
|
<span fg={colors.textMuted}>
|
|
265
269
|
{hasCacheData ? ` ↯${formatCacheBadge(cacheRead)}` : ""}
|
|
266
270
|
</span>
|
|
@@ -315,7 +319,10 @@ const SessionRow = memo(function SessionRow({
|
|
|
315
319
|
: modelWithCount.padEnd(narrowModelMaxChars)}
|
|
316
320
|
</text>
|
|
317
321
|
<text width={narrowTokenW} height={1} {...bgProp}>
|
|
318
|
-
<span fg={tokenColor}>
|
|
322
|
+
<span fg={tokenColor}>
|
|
323
|
+
{isEstimated ? "≈" : " "}
|
|
324
|
+
{formatTokensVal(animatedTokens).padStart(7)}
|
|
325
|
+
</span>
|
|
319
326
|
<span fg={colors.textMuted}>
|
|
320
327
|
{hasCacheData && showCacheBadge ? ` ↯${formatCacheBadge(cacheRead)}` : ""}
|
|
321
328
|
</span>
|
|
@@ -386,6 +393,10 @@ export const SessionsTable = forwardRef(function SessionsTable(
|
|
|
386
393
|
getKey: getSessionKey,
|
|
387
394
|
});
|
|
388
395
|
|
|
396
|
+
// Track bulk changes to force scrollbox remount (clears ghost text)
|
|
397
|
+
const bulkCountRef = useRef(0);
|
|
398
|
+
if (isBulkChange) bulkCountRef.current++;
|
|
399
|
+
|
|
389
400
|
let activeIndex = 0;
|
|
390
401
|
const selectedSession = sessions[selectedRow] ?? null;
|
|
391
402
|
const inspector = selectedSession ? getInspectorData(selectedSession) : null;
|
|
@@ -489,8 +500,8 @@ export const SessionsTable = forwardRef(function SessionsTable(
|
|
|
489
500
|
</box>
|
|
490
501
|
)}
|
|
491
502
|
|
|
492
|
-
<scrollbox ref={ref} flexGrow={1}>
|
|
493
|
-
<box flexDirection="column">
|
|
503
|
+
<scrollbox key={`sessions-sb-${bulkCountRef.current}`} ref={ref} flexGrow={1}>
|
|
504
|
+
<box flexDirection="column" flexGrow={1}>
|
|
494
505
|
{sessions.length === 0 && animatedSessions.length === 0 && (
|
|
495
506
|
<box paddingLeft={2}>
|
|
496
507
|
<text fg={colors.textMuted}>
|