@tokentop/ttop 0.2.2 → 0.3.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/README.md +2 -5
- package/package.json +8 -5
- package/src/agents/aggregator.ts +39 -19
- package/src/agents/costing.ts +11 -9
- package/src/agents/index.ts +7 -9
- package/src/agents/types.ts +2 -2
- package/src/cli.ts +28 -23
- package/src/config/schema.test.ts +42 -40
- package/src/config/schema.ts +26 -26
- package/src/demo/simulator.ts +161 -122
- package/src/plugins/agents/index.ts +2 -2
- package/src/plugins/auth-sources.ts +42 -38
- package/src/plugins/lifecycle.ts +32 -32
- package/src/plugins/loader.test.ts +49 -49
- package/src/plugins/loader.ts +67 -63
- package/src/plugins/notification-bus.ts +21 -16
- package/src/plugins/notifications/index.ts +2 -2
- package/src/plugins/notifications/terminal-bell.ts +29 -23
- package/src/plugins/notifications/visual-flash.ts +21 -19
- package/src/plugins/npm-installer.ts +23 -25
- package/src/plugins/plugin-context-factory.ts +10 -10
- package/src/plugins/plugin-host.ts +12 -5
- package/src/plugins/providers/anthropic.ts +64 -57
- package/src/plugins/providers/antigravity.ts +81 -69
- package/src/plugins/providers/codex.ts +42 -42
- package/src/plugins/providers/gemini.ts +106 -87
- package/src/plugins/providers/github-copilot.ts +49 -45
- package/src/plugins/providers/index.ts +10 -12
- package/src/plugins/providers/minimax.ts +50 -44
- package/src/plugins/providers/openai-api.ts +82 -62
- package/src/plugins/providers/opencode-zen.ts +46 -39
- package/src/plugins/providers/perplexity.ts +55 -46
- package/src/plugins/providers/zai.ts +66 -57
- package/src/plugins/registry.ts +47 -43
- package/src/plugins/sandbox-guard.test.ts +45 -48
- package/src/plugins/sandbox-guard.ts +20 -25
- package/src/plugins/sandbox.ts +26 -48
- package/src/plugins/themes/catppuccin-latte.ts +29 -29
- package/src/plugins/themes/catppuccin-mocha.ts +29 -29
- package/src/plugins/themes/claude-code.ts +29 -29
- package/src/plugins/themes/dracula.ts +29 -29
- package/src/plugins/themes/github-light.ts +29 -29
- package/src/plugins/themes/gruvbox-dark.ts +29 -29
- package/src/plugins/themes/gruvbox-light.ts +29 -29
- package/src/plugins/themes/index.ts +15 -15
- package/src/plugins/themes/kanagawa.ts +29 -29
- package/src/plugins/themes/nord.ts +29 -29
- package/src/plugins/themes/one-dark.ts +29 -29
- package/src/plugins/themes/opencode.ts +31 -31
- package/src/plugins/themes/rose-pine-dawn.ts +29 -29
- package/src/plugins/themes/rose-pine.ts +29 -29
- package/src/plugins/themes/solarized-light.ts +29 -29
- package/src/plugins/themes/tokyo-night.ts +29 -29
- package/src/plugins/types/agent.ts +5 -5
- package/src/plugins/types/base.ts +35 -24
- package/src/plugins/types/index.ts +9 -9
- package/src/plugins/types/notification.ts +12 -12
- package/src/plugins/types/provider.ts +12 -8
- package/src/plugins/types/theme.ts +31 -23
- package/src/plugins/update-checker.ts +6 -6
- package/src/pricing/estimator.test.ts +39 -44
- package/src/pricing/estimator.ts +7 -13
- package/src/pricing/fallback.test.ts +30 -34
- package/src/pricing/fallback.ts +54 -27
- package/src/pricing/index.ts +18 -12
- package/src/pricing/models-dev.ts +6 -6
- package/src/storage/database.ts +56 -46
- package/src/storage/db.ts +12 -12
- package/src/storage/index.ts +44 -45
- package/src/storage/migrations/index.ts +2 -2
- package/src/storage/paths.ts +13 -13
- package/src/storage/repos/agentSessions.ts +33 -23
- package/src/storage/repos/providerSnapshots.ts +5 -5
- package/src/storage/repos/usageEvents.ts +40 -28
- package/src/storage/types.ts +5 -5
- package/src/tui/App.tsx +180 -137
- package/src/tui/components/CommandPalette.tsx +46 -34
- package/src/tui/components/DebugConsole.tsx +39 -33
- package/src/tui/components/DebugPanel.tsx +295 -177
- package/src/tui/components/FooterHints.tsx +2 -2
- package/src/tui/components/GhostProviderCard.tsx +37 -35
- package/src/tui/components/Header.tsx +68 -35
- package/src/tui/components/HelpOverlay.tsx +99 -29
- package/src/tui/components/InlineGauge.tsx +10 -7
- package/src/tui/components/InlineSparkline.tsx +17 -11
- package/src/tui/components/KpiStrip.tsx +128 -98
- package/src/tui/components/LimitGauge.tsx +96 -59
- package/src/tui/components/ModalBackdrop.tsx +4 -4
- package/src/tui/components/ProviderAggregateStrip.tsx +69 -38
- package/src/tui/components/ProviderCard.tsx +232 -226
- package/src/tui/components/ProviderDetailPanel.tsx +133 -58
- package/src/tui/components/ProviderLimitsPanel.tsx +126 -89
- package/src/tui/components/ProvidersList.tsx +215 -126
- package/src/tui/components/SessionDetailsDrawer.tsx +191 -94
- package/src/tui/components/SessionsTable.tsx +313 -166
- package/src/tui/components/SettingsModal.tsx +231 -214
- package/src/tui/components/SidebarBreakdown.tsx +30 -16
- package/src/tui/components/Skeleton.tsx +24 -17
- package/src/tui/components/SmartSidebar.tsx +360 -258
- package/src/tui/components/Sparkline.tsx +64 -52
- package/src/tui/components/Spinner.tsx +3 -3
- package/src/tui/components/StatusBar.tsx +27 -27
- package/src/tui/components/ThemePicker.tsx +67 -66
- package/src/tui/components/Toast.tsx +9 -6
- package/src/tui/components/UsageGauge.tsx +10 -13
- package/src/tui/components/index.ts +5 -5
- package/src/tui/contexts/AgentSessionContext.tsx +193 -144
- package/src/tui/contexts/ConfigContext.tsx +15 -13
- package/src/tui/contexts/DashboardRuntimeContext.tsx +26 -26
- package/src/tui/contexts/DemoModeContext.tsx +22 -15
- package/src/tui/contexts/DrawerContext.tsx +11 -9
- package/src/tui/contexts/InputContext.tsx +2 -2
- package/src/tui/contexts/LogContext.tsx +62 -47
- package/src/tui/contexts/PluginContext.tsx +232 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +151 -117
- package/src/tui/contexts/ThemeContext.tsx +21 -16
- package/src/tui/contexts/TimeWindowContext.tsx +72 -61
- package/src/tui/contexts/ToastContext.tsx +10 -7
- package/src/tui/contexts/index.ts +9 -5
- package/src/tui/createApp.tsx +8 -12
- package/src/tui/debug/captureFrame.ts +25 -28
- package/src/tui/debug/snapshot.tsx +179 -162
- package/src/tui/driver/assertions.ts +30 -22
- package/src/tui/driver/cli.ts +231 -221
- package/src/tui/driver/coverage.ts +27 -44
- package/src/tui/driver/demo-snapshot.ts +33 -33
- package/src/tui/driver/diff.ts +26 -32
- package/src/tui/driver/driver.ts +43 -48
- package/src/tui/driver/index.ts +36 -40
- package/src/tui/driver/recorder.ts +37 -48
- package/src/tui/driver/test-workflow.ts +51 -51
- package/src/tui/driver/test.ts +25 -25
- package/src/tui/hooks/index.ts +24 -20
- package/src/tui/hooks/useAnimatedValue.ts +13 -18
- package/src/tui/hooks/useDashboardKeyboard.ts +118 -98
- package/src/tui/hooks/useDashboardState.ts +26 -24
- package/src/tui/hooks/useEmaActivity.ts +82 -74
- package/src/tui/hooks/useEntranceAnimation.ts +11 -11
- package/src/tui/hooks/useExitAnimation.ts +3 -3
- package/src/tui/hooks/usePulse.ts +4 -4
- package/src/tui/hooks/useSafeRenderer.ts +5 -5
- package/src/tui/hooks/useSessionEnrichedProviders.ts +150 -0
- package/src/tui/hooks/useValueFlash.ts +10 -14
- package/src/tui/index.tsx +29 -6
- package/src/tui/shutdown.ts +19 -0
- package/src/tui/utils/providerColor.ts +1 -1
- package/src/tui/views/Dashboard.tsx +127 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +466 -237
- package/src/tui/views/RealTimeDashboard.tsx +150 -111
- package/src/tui/views/SettingsView.tsx +222 -191
- package/src/tui/views/index.ts +2 -2
- package/src/types/opentui-test-utils.d.ts +7 -7
- package/src/utils/clipboard.ts +29 -29
- package/src/utils/currency.test.ts +53 -53
- package/src/utils/currency.ts +35 -32
- package/src/version.ts +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import type { SparklineOrientation, SparklineStyle } from "@/config/schema.ts";
|
|
2
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
3
3
|
|
|
4
4
|
export interface SparklineProps {
|
|
5
5
|
data: number[];
|
|
@@ -12,29 +12,29 @@ export interface SparklineProps {
|
|
|
12
12
|
showBaseline?: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const BLOCK_CHARS_UP = [
|
|
16
|
-
const BLOCK_CHARS_DOWN = [
|
|
15
|
+
const BLOCK_CHARS_UP = ["▁", "▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
16
|
+
const BLOCK_CHARS_DOWN = ["▔", "▔", "🬂", "🬎", "▀", "🬩", "🬷", "🮂", "█"];
|
|
17
17
|
|
|
18
18
|
function valueToBraille(
|
|
19
|
-
left: number,
|
|
20
|
-
right: number,
|
|
19
|
+
left: number,
|
|
20
|
+
right: number,
|
|
21
21
|
orientation: SparklineOrientation,
|
|
22
|
-
includeBaseline: boolean
|
|
22
|
+
includeBaseline: boolean,
|
|
23
23
|
): string {
|
|
24
24
|
const leftDots = Math.min(4, Math.max(0, Math.floor(left)));
|
|
25
25
|
const rightDots = Math.min(4, Math.max(0, Math.floor(right)));
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
let code = 0x2800;
|
|
28
|
-
|
|
29
|
-
if (orientation ===
|
|
28
|
+
|
|
29
|
+
if (orientation === "up") {
|
|
30
30
|
// Fill from bottom to top: dot7, dot3, dot2, dot1 (left) / dot8, dot6, dot5, dot4 (right)
|
|
31
31
|
const leftBitsUp = [0x40, 0x04, 0x02, 0x01];
|
|
32
32
|
const rightBitsUp = [0x80, 0x20, 0x10, 0x08];
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
if (includeBaseline) {
|
|
35
35
|
code |= 0x40 | 0x80; // Always include bottom row when baseline is on
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
for (let i = 0; i < leftDots; i++) {
|
|
39
39
|
code |= leftBitsUp[i]!;
|
|
40
40
|
}
|
|
@@ -45,11 +45,11 @@ function valueToBraille(
|
|
|
45
45
|
// Fill from top to bottom: dot1, dot2, dot3, dot7 (left) / dot4, dot5, dot6, dot8 (right)
|
|
46
46
|
const leftBitsDown = [0x01, 0x02, 0x04, 0x40];
|
|
47
47
|
const rightBitsDown = [0x08, 0x10, 0x20, 0x80];
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
if (includeBaseline) {
|
|
50
50
|
code |= 0x01 | 0x08; // Always include top row when baseline is on
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
for (let i = 0; i < leftDots; i++) {
|
|
54
54
|
code |= leftBitsDown[i]!;
|
|
55
55
|
}
|
|
@@ -57,18 +57,18 @@ function valueToBraille(
|
|
|
57
57
|
code |= rightBitsDown[i]!;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
return String.fromCharCode(code);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function getBaselineBraille(orientation: SparklineOrientation): string {
|
|
65
|
-
return orientation ===
|
|
65
|
+
return orientation === "up" ? "⣀" : "⠉";
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
function getValueColor(
|
|
69
69
|
rawValue: number,
|
|
70
70
|
thresholds: { warning: number; error: number },
|
|
71
|
-
colors: { success: string; warning: string; error: string; textSubtle: string }
|
|
71
|
+
colors: { success: string; warning: string; error: string; textSubtle: string },
|
|
72
72
|
): string {
|
|
73
73
|
if (rawValue <= 0) return colors.textSubtle;
|
|
74
74
|
if (rawValue >= thresholds.error) return colors.error;
|
|
@@ -76,105 +76,117 @@ function getValueColor(
|
|
|
76
76
|
return colors.success;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export function Sparkline({
|
|
80
|
-
data,
|
|
81
|
-
width = 60,
|
|
82
|
-
label,
|
|
79
|
+
export function Sparkline({
|
|
80
|
+
data,
|
|
81
|
+
width = 60,
|
|
82
|
+
label,
|
|
83
83
|
fixedMax = 2000,
|
|
84
84
|
thresholds = { warning: 800, error: 2000 },
|
|
85
|
-
style =
|
|
86
|
-
orientation =
|
|
85
|
+
style = "braille",
|
|
86
|
+
orientation = "up",
|
|
87
87
|
showBaseline = true,
|
|
88
88
|
}: SparklineProps) {
|
|
89
89
|
const colors = useColors();
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
const peak = Math.max(...data, 0);
|
|
92
92
|
const sqrtMax = Math.sqrt(fixedMax);
|
|
93
|
-
|
|
93
|
+
|
|
94
94
|
const normalizeValue = (v: number): number => {
|
|
95
95
|
if (v <= 0) return 0;
|
|
96
96
|
const sqrtValue = Math.sqrt(Math.min(v, fixedMax));
|
|
97
|
-
return (sqrtValue / sqrtMax) * (style ===
|
|
97
|
+
return (sqrtValue / sqrtMax) * (style === "braille" ? 4 : 8);
|
|
98
98
|
};
|
|
99
|
-
|
|
100
|
-
const peakLabel = peak >= 1000 ? `${(peak/1000).toFixed(1)}k` : `${Math.round(peak)}`;
|
|
101
99
|
|
|
102
|
-
|
|
100
|
+
const peakLabel = peak >= 1000 ? `${(peak / 1000).toFixed(1)}k` : `${Math.round(peak)}`;
|
|
101
|
+
|
|
102
|
+
if (style === "braille") {
|
|
103
103
|
const effectiveWidth = width * 2;
|
|
104
104
|
const displayData = data.slice(-effectiveWidth);
|
|
105
105
|
const charCount = Math.ceil(displayData.length / 2);
|
|
106
106
|
const padding = width - charCount;
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
const groups: { color: string; chars: string }[] = [];
|
|
109
109
|
const baselineChar = getBaselineBraille(orientation);
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
for (let i = 0; i < displayData.length; i += 2) {
|
|
112
112
|
const leftRaw = displayData[i] ?? 0;
|
|
113
113
|
const rightRaw = displayData[i + 1] ?? leftRaw;
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
const leftNorm = normalizeValue(leftRaw);
|
|
116
116
|
const rightNorm = normalizeValue(rightRaw);
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
const dominantRaw = Math.max(leftRaw, rightRaw);
|
|
119
119
|
const color = getValueColor(dominantRaw, thresholds, colors);
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
let char: string;
|
|
122
122
|
if (dominantRaw <= 0) {
|
|
123
|
-
char = showBaseline ? baselineChar :
|
|
123
|
+
char = showBaseline ? baselineChar : " ";
|
|
124
124
|
} else {
|
|
125
125
|
char = valueToBraille(leftNorm, rightNorm, orientation, showBaseline);
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
|
|
128
128
|
if (groups.length > 0 && groups[groups.length - 1]!.color === color) {
|
|
129
129
|
groups[groups.length - 1]!.chars += char;
|
|
130
130
|
} else {
|
|
131
131
|
groups.push({ color, chars: char });
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
return (
|
|
136
136
|
<box flexDirection="column">
|
|
137
137
|
<text>
|
|
138
|
-
{padding > 0 && <span>{
|
|
138
|
+
{padding > 0 && <span>{" ".repeat(padding)}</span>}
|
|
139
139
|
{groups.map((group, i) => (
|
|
140
|
-
<span key={i} fg={group.color}>
|
|
140
|
+
<span key={i} fg={group.color}>
|
|
141
|
+
{group.chars}
|
|
142
|
+
</span>
|
|
141
143
|
))}
|
|
142
144
|
</text>
|
|
143
|
-
{label &&
|
|
145
|
+
{label && (
|
|
146
|
+
<text fg={colors.textMuted}>
|
|
147
|
+
{label} peak:{peakLabel}
|
|
148
|
+
</text>
|
|
149
|
+
)}
|
|
144
150
|
</box>
|
|
145
151
|
);
|
|
146
152
|
}
|
|
147
|
-
|
|
148
|
-
const blockChars = orientation ===
|
|
149
|
-
const baselineChar = showBaseline ? (orientation ===
|
|
150
|
-
|
|
153
|
+
|
|
154
|
+
const blockChars = orientation === "up" ? BLOCK_CHARS_UP : BLOCK_CHARS_DOWN;
|
|
155
|
+
const baselineChar = showBaseline ? (orientation === "up" ? "▁" : "▔") : "·";
|
|
156
|
+
|
|
151
157
|
const displayData = data.slice(-width);
|
|
152
158
|
const padding = width - displayData.length;
|
|
153
|
-
|
|
159
|
+
|
|
154
160
|
const groups: { color: string; chars: string }[] = [];
|
|
155
161
|
for (let i = 0; i < displayData.length; i++) {
|
|
156
162
|
const rawValue = displayData[i] ?? 0;
|
|
157
163
|
const v = Math.round(normalizeValue(rawValue));
|
|
158
|
-
|
|
164
|
+
|
|
159
165
|
const color = getValueColor(rawValue, thresholds, colors);
|
|
160
|
-
const char = rawValue <= 0 ? baselineChar : (blockChars[v] ?? blockChars[1] ??
|
|
161
|
-
|
|
166
|
+
const char = rawValue <= 0 ? baselineChar : (blockChars[v] ?? blockChars[1] ?? "▁");
|
|
167
|
+
|
|
162
168
|
if (groups.length > 0 && groups[groups.length - 1]!.color === color) {
|
|
163
169
|
groups[groups.length - 1]!.chars += char;
|
|
164
170
|
} else {
|
|
165
171
|
groups.push({ color, chars: char });
|
|
166
172
|
}
|
|
167
173
|
}
|
|
168
|
-
|
|
174
|
+
|
|
169
175
|
return (
|
|
170
176
|
<box flexDirection="column">
|
|
171
177
|
<text>
|
|
172
|
-
{padding > 0 && <span>{
|
|
178
|
+
{padding > 0 && <span>{" ".repeat(padding)}</span>}
|
|
173
179
|
{groups.map((group, i) => (
|
|
174
|
-
<span key={i} fg={group.color}>
|
|
180
|
+
<span key={i} fg={group.color}>
|
|
181
|
+
{group.chars}
|
|
182
|
+
</span>
|
|
175
183
|
))}
|
|
176
184
|
</text>
|
|
177
|
-
{label &&
|
|
185
|
+
{label && (
|
|
186
|
+
<text fg={colors.textMuted}>
|
|
187
|
+
{label} peak:{peakLabel}
|
|
188
|
+
</text>
|
|
189
|
+
)}
|
|
178
190
|
</box>
|
|
179
191
|
);
|
|
180
192
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
2
|
|
|
3
|
-
const SPINNER_FRAMES = [
|
|
3
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
4
4
|
const FRAME_INTERVAL = 80;
|
|
5
5
|
|
|
6
6
|
interface SpinnerProps {
|
|
@@ -32,5 +32,5 @@ export function useSpinner(): string {
|
|
|
32
32
|
return () => clearInterval(interval);
|
|
33
33
|
}, []);
|
|
34
34
|
|
|
35
|
-
return SPINNER_FRAMES[frameIndex] ??
|
|
35
|
+
return SPINNER_FRAMES[frameIndex] ?? "⠋";
|
|
36
36
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { useColors } from
|
|
4
|
-
import { useTimeWindow } from
|
|
1
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
4
|
+
import { useTimeWindow } from "../contexts/TimeWindowContext.tsx";
|
|
5
5
|
|
|
6
6
|
interface StatusBarProps {
|
|
7
7
|
lastRefresh?: number;
|
|
@@ -26,25 +26,25 @@ export function StatusBar({ lastRefresh, nextRefresh, demoMode = false }: Status
|
|
|
26
26
|
const isNarrow = termWidth < 100;
|
|
27
27
|
|
|
28
28
|
const lastRefreshText = lastRefresh
|
|
29
|
-
?
|
|
30
|
-
|
|
29
|
+
? isNarrow
|
|
30
|
+
? formatTimeShort(lastRefresh)
|
|
31
|
+
: formatTime(lastRefresh)
|
|
32
|
+
: "";
|
|
31
33
|
|
|
32
|
-
const nextRefreshText = nextRefresh
|
|
33
|
-
? `${formatCountdown(nextRefresh)}`
|
|
34
|
-
: '';
|
|
34
|
+
const nextRefreshText = nextRefresh ? `${formatCountdown(nextRefresh)}` : "";
|
|
35
35
|
|
|
36
36
|
const timeLabel = timeWindow.toUpperCase();
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
<box
|
|
40
|
+
flexDirection="row"
|
|
41
|
+
justifyContent="space-between"
|
|
42
|
+
paddingX={1}
|
|
43
|
+
backgroundColor={colors.foreground}
|
|
44
|
+
flexShrink={0}
|
|
45
|
+
height={1}
|
|
46
|
+
overflow="hidden"
|
|
47
|
+
>
|
|
48
48
|
<box flexDirection="row" gap={2} overflow="hidden">
|
|
49
49
|
<text fg={colors.textSubtle}>1-4 views</text>
|
|
50
50
|
<text fg={colors.text}>
|
|
@@ -55,7 +55,7 @@ export function StatusBar({ lastRefresh, nextRefresh, demoMode = false }: Status
|
|
|
55
55
|
<text fg={colors.textSubtle}>? help</text>
|
|
56
56
|
</box>
|
|
57
57
|
<box flexDirection="row" gap={2} overflow="hidden">
|
|
58
|
-
{demoMode && <text fg={colors.warning}>{isNarrow ?
|
|
58
|
+
{demoMode && <text fg={colors.warning}>{isNarrow ? "DEMO" : "DEMO MODE"}</text>}
|
|
59
59
|
{lastRefreshText && <text fg={colors.textSubtle}>{lastRefreshText}</text>}
|
|
60
60
|
{nextRefreshText && <text fg={colors.textSubtle}>{nextRefreshText}</text>}
|
|
61
61
|
</box>
|
|
@@ -65,19 +65,19 @@ export function StatusBar({ lastRefresh, nextRefresh, demoMode = false }: Status
|
|
|
65
65
|
|
|
66
66
|
function formatTimeShort(timestamp: number): string {
|
|
67
67
|
const date = new Date(timestamp);
|
|
68
|
-
return date.toLocaleTimeString(
|
|
69
|
-
hour:
|
|
70
|
-
minute:
|
|
68
|
+
return date.toLocaleTimeString("en-US", {
|
|
69
|
+
hour: "2-digit",
|
|
70
|
+
minute: "2-digit",
|
|
71
71
|
hour12: false,
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function formatTime(timestamp: number): string {
|
|
76
76
|
const date = new Date(timestamp);
|
|
77
|
-
return date.toLocaleTimeString(
|
|
78
|
-
hour:
|
|
79
|
-
minute:
|
|
80
|
-
second:
|
|
77
|
+
return date.toLocaleTimeString("en-US", {
|
|
78
|
+
hour: "2-digit",
|
|
79
|
+
minute: "2-digit",
|
|
80
|
+
second: "2-digit",
|
|
81
81
|
hour12: false,
|
|
82
82
|
});
|
|
83
83
|
}
|
|
@@ -86,7 +86,7 @@ function formatCountdown(timestamp: number): string {
|
|
|
86
86
|
const now = Date.now();
|
|
87
87
|
const diff = timestamp - now;
|
|
88
88
|
|
|
89
|
-
if (diff <= 0) return
|
|
89
|
+
if (diff <= 0) return "now";
|
|
90
90
|
|
|
91
91
|
const seconds = Math.floor(diff / 1000);
|
|
92
92
|
if (seconds < 60) return `${seconds}s`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
1
|
+
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import type { ColorSchemePreference, ThemePlugin } from "@/plugins/types/theme.ts";
|
|
4
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
5
5
|
|
|
6
6
|
interface ThemePickerProps {
|
|
7
7
|
themes: ThemePlugin[];
|
|
@@ -12,9 +12,7 @@ interface ThemePickerProps {
|
|
|
12
12
|
onCancel: () => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
type ThemeItem =
|
|
16
|
-
| { type: 'header'; label: string }
|
|
17
|
-
| { type: 'theme'; theme: ThemePlugin };
|
|
15
|
+
type ThemeItem = { type: "header"; label: string } | { type: "theme"; theme: ThemePlugin };
|
|
18
16
|
|
|
19
17
|
export function ThemePicker({
|
|
20
18
|
themes,
|
|
@@ -26,37 +24,39 @@ export function ThemePicker({
|
|
|
26
24
|
}: ThemePickerProps) {
|
|
27
25
|
const colors = useColors();
|
|
28
26
|
const [scheme, setScheme] = useState<ColorSchemePreference>(currentScheme);
|
|
29
|
-
const [focusPane, setFocusPane] = useState<
|
|
27
|
+
const [focusPane, setFocusPane] = useState<"scheme" | "list">("list");
|
|
30
28
|
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
31
29
|
|
|
32
30
|
// Filter and group themes based on selected scheme
|
|
33
31
|
const listItems = useMemo<ThemeItem[]>(() => {
|
|
34
|
-
const darkThemes = themes.filter(t => t.colorScheme ===
|
|
35
|
-
const lightThemes = themes.filter(t => t.colorScheme ===
|
|
32
|
+
const darkThemes = themes.filter((t) => t.colorScheme === "dark");
|
|
33
|
+
const lightThemes = themes.filter((t) => t.colorScheme === "light");
|
|
36
34
|
|
|
37
|
-
if (scheme ===
|
|
38
|
-
return darkThemes.map(t => ({ type:
|
|
35
|
+
if (scheme === "dark") {
|
|
36
|
+
return darkThemes.map((t) => ({ type: "theme", theme: t }));
|
|
39
37
|
}
|
|
40
|
-
if (scheme ===
|
|
41
|
-
return lightThemes.map(t => ({ type:
|
|
38
|
+
if (scheme === "light") {
|
|
39
|
+
return lightThemes.map((t) => ({ type: "theme", theme: t }));
|
|
42
40
|
}
|
|
43
41
|
// Auto: Show both with headers
|
|
44
42
|
return [
|
|
45
|
-
{ type:
|
|
46
|
-
...darkThemes.map(t => ({ type:
|
|
47
|
-
{ type:
|
|
48
|
-
...lightThemes.map(t => ({ type:
|
|
43
|
+
{ type: "header", label: "DARK" },
|
|
44
|
+
...darkThemes.map((t) => ({ type: "theme", theme: t }) as const),
|
|
45
|
+
{ type: "header", label: "LIGHT" },
|
|
46
|
+
...lightThemes.map((t) => ({ type: "theme", theme: t }) as const),
|
|
49
47
|
];
|
|
50
48
|
}, [themes, scheme]);
|
|
51
49
|
|
|
52
50
|
// Find initial index for current theme
|
|
53
51
|
useEffect(() => {
|
|
54
|
-
const idx = listItems.findIndex(
|
|
52
|
+
const idx = listItems.findIndex(
|
|
53
|
+
(item) => item.type === "theme" && item.theme.id === currentThemeId,
|
|
54
|
+
);
|
|
55
55
|
if (idx >= 0) {
|
|
56
56
|
setFocusedIndex(idx);
|
|
57
57
|
} else {
|
|
58
58
|
// If not found (e.g. switched scheme and current theme is hidden), select first theme
|
|
59
|
-
const firstThemeIdx = listItems.findIndex(item => item.type ===
|
|
59
|
+
const firstThemeIdx = listItems.findIndex((item) => item.type === "theme");
|
|
60
60
|
if (firstThemeIdx >= 0) setFocusedIndex(firstThemeIdx);
|
|
61
61
|
}
|
|
62
62
|
}, [currentThemeId, listItems]);
|
|
@@ -75,71 +75,71 @@ export function ThemePicker({
|
|
|
75
75
|
const hasMoreBelow = scrollOffset + visibleCount < listItems.length;
|
|
76
76
|
|
|
77
77
|
useKeyboard((key) => {
|
|
78
|
-
if (key.name ===
|
|
78
|
+
if (key.name === "escape") {
|
|
79
79
|
onCancel();
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
if (key.name ===
|
|
84
|
-
setFocusPane(p => p ===
|
|
83
|
+
if (key.name === "tab") {
|
|
84
|
+
setFocusPane((p) => (p === "scheme" ? "list" : "scheme"));
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
if (focusPane ===
|
|
89
|
-
if (key.name ===
|
|
90
|
-
const schemes: ColorSchemePreference[] = [
|
|
88
|
+
if (focusPane === "scheme") {
|
|
89
|
+
if (key.name === "left" || key.name === "h" || key.name === "right" || key.name === "l") {
|
|
90
|
+
const schemes: ColorSchemePreference[] = ["auto", "dark", "light"];
|
|
91
91
|
const currentIdx = schemes.indexOf(scheme);
|
|
92
|
-
const direction =
|
|
92
|
+
const direction = key.name === "left" || key.name === "h" ? -1 : 1;
|
|
93
93
|
const nextIdx = (currentIdx + direction + schemes.length) % schemes.length;
|
|
94
94
|
const newScheme = schemes[nextIdx]!;
|
|
95
95
|
setScheme(newScheme);
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
// Preview first theme of new scheme
|
|
98
|
-
const darkThemes = themes.filter(t => t.colorScheme ===
|
|
99
|
-
const lightThemes = themes.filter(t => t.colorScheme ===
|
|
98
|
+
const darkThemes = themes.filter((t) => t.colorScheme === "dark");
|
|
99
|
+
const lightThemes = themes.filter((t) => t.colorScheme === "light");
|
|
100
100
|
let nextTheme: ThemePlugin | undefined;
|
|
101
|
-
|
|
102
|
-
if (newScheme ===
|
|
103
|
-
else if (newScheme ===
|
|
101
|
+
|
|
102
|
+
if (newScheme === "dark") nextTheme = darkThemes[0];
|
|
103
|
+
else if (newScheme === "light") nextTheme = lightThemes[0];
|
|
104
104
|
else nextTheme = darkThemes[0]; // Auto defaults to dark usually
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
if (nextTheme) {
|
|
107
|
-
|
|
107
|
+
onPreview(nextTheme);
|
|
108
108
|
}
|
|
109
|
-
} else if (key.name ===
|
|
110
|
-
setFocusPane(
|
|
109
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
110
|
+
setFocusPane("list");
|
|
111
111
|
}
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
if (focusPane ===
|
|
116
|
-
if (key.name ===
|
|
115
|
+
if (focusPane === "list") {
|
|
116
|
+
if (key.name === "up" || key.name === "k") {
|
|
117
117
|
let nextIdx = focusedIndex - 1;
|
|
118
118
|
// Skip headers going up
|
|
119
|
-
while (nextIdx >= 0 && listItems[nextIdx]?.type ===
|
|
119
|
+
while (nextIdx >= 0 && listItems[nextIdx]?.type === "header") {
|
|
120
120
|
nextIdx--;
|
|
121
121
|
}
|
|
122
122
|
if (nextIdx >= 0) {
|
|
123
123
|
setFocusedIndex(nextIdx);
|
|
124
124
|
const item = listItems[nextIdx];
|
|
125
|
-
if (item?.type ===
|
|
125
|
+
if (item?.type === "theme") onPreview(item.theme);
|
|
126
126
|
} else {
|
|
127
|
-
setFocusPane(
|
|
127
|
+
setFocusPane("scheme");
|
|
128
128
|
}
|
|
129
|
-
} else if (key.name ===
|
|
129
|
+
} else if (key.name === "down" || key.name === "j") {
|
|
130
130
|
let nextIdx = focusedIndex + 1;
|
|
131
131
|
// Skip headers going down
|
|
132
|
-
while (nextIdx < listItems.length && listItems[nextIdx]?.type ===
|
|
132
|
+
while (nextIdx < listItems.length && listItems[nextIdx]?.type === "header") {
|
|
133
133
|
nextIdx++;
|
|
134
134
|
}
|
|
135
135
|
if (nextIdx < listItems.length) {
|
|
136
136
|
setFocusedIndex(nextIdx);
|
|
137
137
|
const item = listItems[nextIdx];
|
|
138
|
-
if (item?.type ===
|
|
138
|
+
if (item?.type === "theme") onPreview(item.theme);
|
|
139
139
|
}
|
|
140
|
-
} else if (key.name ===
|
|
140
|
+
} else if (key.name === "return") {
|
|
141
141
|
const item = listItems[focusedIndex];
|
|
142
|
-
if (item?.type ===
|
|
142
|
+
if (item?.type === "theme") {
|
|
143
143
|
onSelect(item.theme.id, scheme);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
@@ -150,18 +150,18 @@ export function ThemePicker({
|
|
|
150
150
|
<box flexDirection="column" flexGrow={1} padding={1}>
|
|
151
151
|
{/* Section 1: Scheme Selector */}
|
|
152
152
|
<box flexDirection="row" justifyContent="center" marginBottom={1} height={1}>
|
|
153
|
-
{([
|
|
153
|
+
{(["auto", "dark", "light"] as const).map((s) => {
|
|
154
154
|
const isActive = scheme === s;
|
|
155
|
-
const isFocused = focusPane ===
|
|
155
|
+
const isFocused = focusPane === "scheme";
|
|
156
156
|
return (
|
|
157
157
|
<box key={s} marginRight={1}>
|
|
158
158
|
<text
|
|
159
159
|
{...(isActive ? { bg: colors.primary } : {})}
|
|
160
160
|
fg={isActive ? colors.background : isFocused ? colors.text : colors.textMuted}
|
|
161
161
|
>
|
|
162
|
-
{isFocused && isActive ?
|
|
162
|
+
{isFocused && isActive ? "▸ " : " "}
|
|
163
163
|
{s.charAt(0).toUpperCase() + s.slice(1)}
|
|
164
|
-
{isFocused && isActive ?
|
|
164
|
+
{isFocused && isActive ? " ◂" : " "}
|
|
165
165
|
</text>
|
|
166
166
|
</box>
|
|
167
167
|
);
|
|
@@ -174,19 +174,22 @@ export function ThemePicker({
|
|
|
174
174
|
<text fg={colors.textMuted}>THEMES</text>
|
|
175
175
|
{(hasMoreAbove || hasMoreBelow) && (
|
|
176
176
|
<text fg={colors.textSubtle}>
|
|
177
|
-
{hasMoreAbove ?
|
|
177
|
+
{hasMoreAbove ? "▲" : " "}
|
|
178
|
+
{hasMoreBelow ? "▼" : " "}
|
|
178
179
|
</text>
|
|
179
180
|
)}
|
|
180
181
|
</box>
|
|
181
182
|
|
|
182
183
|
{visibleItems.map((item, idx) => {
|
|
183
184
|
const realIndex = scrollOffset + idx;
|
|
184
|
-
const isFocused = realIndex === focusedIndex && focusPane ===
|
|
185
|
-
|
|
186
|
-
if (item.type ===
|
|
185
|
+
const isFocused = realIndex === focusedIndex && focusPane === "list";
|
|
186
|
+
|
|
187
|
+
if (item.type === "header") {
|
|
187
188
|
return (
|
|
188
189
|
<box key={`header-${realIndex}`} height={1} marginTop={1} marginBottom={0}>
|
|
189
|
-
<text fg={colors.textSubtle}
|
|
190
|
+
<text fg={colors.textSubtle}>
|
|
191
|
+
<strong>{item.label}</strong>
|
|
192
|
+
</text>
|
|
190
193
|
</box>
|
|
191
194
|
);
|
|
192
195
|
}
|
|
@@ -203,21 +206,19 @@ export function ThemePicker({
|
|
|
203
206
|
{...(isFocused ? { backgroundColor: colors.primary } : {})}
|
|
204
207
|
>
|
|
205
208
|
<text fg={isFocused ? colors.background : colors.primary}>
|
|
206
|
-
{isFocused ?
|
|
209
|
+
{isFocused ? "▸ " : " "}
|
|
207
210
|
</text>
|
|
208
|
-
|
|
211
|
+
|
|
209
212
|
<box width={20}>
|
|
210
|
-
<text fg={isFocused ? colors.background : colors.text}>
|
|
211
|
-
{theme.name}
|
|
212
|
-
</text>
|
|
213
|
+
<text fg={isFocused ? colors.background : colors.text}>{theme.name}</text>
|
|
213
214
|
</box>
|
|
214
215
|
|
|
215
216
|
<box flexDirection="row" gap={1}>
|
|
216
|
-
<text bg={theme.colors.background}>
|
|
217
|
-
<text bg={theme.colors.primary}>
|
|
218
|
-
<text bg={theme.colors.secondary}>
|
|
219
|
-
<text bg={theme.colors.success}>
|
|
220
|
-
<text bg={theme.colors.error}>
|
|
217
|
+
<text bg={theme.colors.background}> </text>
|
|
218
|
+
<text bg={theme.colors.primary}> </text>
|
|
219
|
+
<text bg={theme.colors.secondary}> </text>
|
|
220
|
+
<text bg={theme.colors.success}> </text>
|
|
221
|
+
<text bg={theme.colors.error}> </text>
|
|
221
222
|
</box>
|
|
222
223
|
|
|
223
224
|
{isActive && (
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useColors } from
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useColors } from "../contexts/ThemeContext.tsx";
|
|
3
3
|
|
|
4
4
|
interface ToastProps {
|
|
5
5
|
message: string;
|
|
6
6
|
duration?: number;
|
|
7
7
|
onDismiss: () => void;
|
|
8
|
-
type?:
|
|
8
|
+
type?: "info" | "success" | "warning" | "error";
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function Toast({ message, duration = 2000, onDismiss, type =
|
|
11
|
+
export function Toast({ message, duration = 2000, onDismiss, type = "success" }: ToastProps) {
|
|
12
12
|
const colors = useColors();
|
|
13
13
|
|
|
14
14
|
useEffect(() => {
|
|
@@ -43,13 +43,16 @@ export function Toast({ message, duration = 2000, onDismiss, type = 'success' }:
|
|
|
43
43
|
|
|
44
44
|
interface ToastState {
|
|
45
45
|
message: string;
|
|
46
|
-
type:
|
|
46
|
+
type: "info" | "success" | "warning" | "error";
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function useToast() {
|
|
50
50
|
const [toast, setToast] = useState<ToastState | null>(null);
|
|
51
51
|
|
|
52
|
-
const showToast = (
|
|
52
|
+
const showToast = (
|
|
53
|
+
message: string,
|
|
54
|
+
type: "info" | "success" | "warning" | "error" = "success",
|
|
55
|
+
) => {
|
|
53
56
|
setToast({ message, type });
|
|
54
57
|
};
|
|
55
58
|
|