@tokentop/ttop 0.2.2 → 0.4.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 +42 -38
- 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 +36 -23
- package/src/plugins/notifications/visual-flash.ts +31 -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 +68 -57
- package/src/plugins/registry.ts +80 -71
- 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/persistence-service.ts +206 -0
- 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/retention.ts +146 -0
- 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 +314 -183
- 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 +320 -169
- package/src/tui/components/SettingsModal.tsx +593 -269
- 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 +164 -151
- 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 +238 -183
- package/src/tui/contexts/RealTimeActivityContext.tsx +70 -36
- package/src/tui/contexts/StorageContext.tsx +95 -149
- 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 +7 -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 +164 -102
- package/src/tui/views/HistoricalTrendsView.tsx +579 -250
- package/src/tui/views/ProjectsView.tsx +495 -243
- package/src/tui/views/RealTimeDashboard.tsx +176 -116
- package/src/tui/views/SettingsView.tsx +225 -189
- 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,58 +1,58 @@
|
|
|
1
|
-
import { createContext,
|
|
1
|
+
import { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from "react";
|
|
2
2
|
|
|
3
|
-
export type TimeWindow =
|
|
3
|
+
export type TimeWindow = "5m" | "15m" | "1h" | "24h" | "7d" | "30d" | "all";
|
|
4
4
|
|
|
5
|
-
export const TIME_WINDOW_OPTIONS: TimeWindow[] = [
|
|
5
|
+
export const TIME_WINDOW_OPTIONS: TimeWindow[] = ["5m", "15m", "1h", "24h", "7d", "30d", "all"];
|
|
6
6
|
|
|
7
7
|
export const TIME_WINDOW_LABELS: Record<TimeWindow, string> = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
"5m": "5 min",
|
|
9
|
+
"15m": "15 min",
|
|
10
|
+
"1h": "1 hour",
|
|
11
|
+
"24h": "24 hours",
|
|
12
|
+
"7d": "7 days",
|
|
13
|
+
"30d": "30 days",
|
|
14
|
+
all: "All time",
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export const TIME_WINDOW_MS: Record<TimeWindow, number | null> = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
"5m": 5 * 60 * 1000,
|
|
19
|
+
"15m": 15 * 60 * 1000,
|
|
20
|
+
"1h": 60 * 60 * 1000,
|
|
21
|
+
"24h": 24 * 60 * 60 * 1000,
|
|
22
|
+
"7d": 7 * 24 * 60 * 60 * 1000,
|
|
23
|
+
"30d": 30 * 24 * 60 * 60 * 1000,
|
|
24
|
+
all: null,
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
export type BudgetType =
|
|
27
|
+
export type BudgetType = "daily" | "weekly" | "monthly" | "none";
|
|
28
28
|
|
|
29
29
|
export const TIME_WINDOW_BUDGET_TYPE: Record<TimeWindow, BudgetType> = {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
"5m": "daily",
|
|
31
|
+
"15m": "daily",
|
|
32
|
+
"1h": "daily",
|
|
33
|
+
"24h": "daily",
|
|
34
|
+
"7d": "weekly",
|
|
35
|
+
"30d": "monthly",
|
|
36
|
+
all: "none",
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
export const BUDGET_TYPE_LABELS: Record<BudgetType, string> = {
|
|
40
|
-
daily:
|
|
41
|
-
weekly:
|
|
42
|
-
monthly:
|
|
43
|
-
none:
|
|
40
|
+
daily: "Daily",
|
|
41
|
+
weekly: "Weekly",
|
|
42
|
+
monthly: "Monthly",
|
|
43
|
+
none: "Total",
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
-
export type BudgetLock = BudgetType |
|
|
46
|
+
export type BudgetLock = BudgetType | "sync";
|
|
47
47
|
|
|
48
|
-
const BUDGET_LOCK_CYCLE: BudgetLock[] = [
|
|
48
|
+
const BUDGET_LOCK_CYCLE: BudgetLock[] = ["sync", "daily", "weekly", "monthly"];
|
|
49
49
|
|
|
50
50
|
export const BUDGET_LOCK_LABELS: Record<BudgetLock, string> = {
|
|
51
|
-
sync:
|
|
52
|
-
daily:
|
|
53
|
-
weekly:
|
|
54
|
-
monthly:
|
|
55
|
-
none:
|
|
51
|
+
sync: "Sync",
|
|
52
|
+
daily: "Daily",
|
|
53
|
+
weekly: "Weekly",
|
|
54
|
+
monthly: "Monthly",
|
|
55
|
+
none: "None",
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
interface TimeWindowContextValue {
|
|
@@ -90,9 +90,9 @@ function getPrevWindow(current: TimeWindow): TimeWindow {
|
|
|
90
90
|
return TIME_WINDOW_OPTIONS[prevIdx] as TimeWindow;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function TimeWindowProvider({ children, defaultWindow =
|
|
93
|
+
export function TimeWindowProvider({ children, defaultWindow = "24h" }: TimeWindowProviderProps) {
|
|
94
94
|
const [window, setWindowState] = useState<TimeWindow>(defaultWindow);
|
|
95
|
-
const [budgetLock, setBudgetLock] = useState<BudgetLock>(
|
|
95
|
+
const [budgetLock, setBudgetLock] = useState<BudgetLock>("sync");
|
|
96
96
|
|
|
97
97
|
const setWindow = useCallback((w: TimeWindow) => {
|
|
98
98
|
setWindowState(w);
|
|
@@ -107,7 +107,7 @@ export function TimeWindowProvider({ children, defaultWindow = '24h' }: TimeWind
|
|
|
107
107
|
}, []);
|
|
108
108
|
|
|
109
109
|
const cycleBudgetLock = useCallback(() => {
|
|
110
|
-
setBudgetLock(current => {
|
|
110
|
+
setBudgetLock((current) => {
|
|
111
111
|
const idx = BUDGET_LOCK_CYCLE.indexOf(current);
|
|
112
112
|
const nextIdx = (idx + 1) % BUDGET_LOCK_CYCLE.length;
|
|
113
113
|
return BUDGET_LOCK_CYCLE[nextIdx] as BudgetLock;
|
|
@@ -116,7 +116,7 @@ export function TimeWindowProvider({ children, defaultWindow = '24h' }: TimeWind
|
|
|
116
116
|
|
|
117
117
|
const windowMs = TIME_WINDOW_MS[window];
|
|
118
118
|
const windowLabel = TIME_WINDOW_LABELS[window];
|
|
119
|
-
const budgetType = budgetLock ===
|
|
119
|
+
const budgetType = budgetLock === "sync" ? TIME_WINDOW_BUDGET_TYPE[window] : budgetLock;
|
|
120
120
|
const budgetTypeLabel = BUDGET_TYPE_LABELS[budgetType];
|
|
121
121
|
const budgetLockLabel = BUDGET_LOCK_LABELS[budgetLock];
|
|
122
122
|
|
|
@@ -126,33 +126,44 @@ export function TimeWindowProvider({ children, defaultWindow = '24h' }: TimeWind
|
|
|
126
126
|
return Date.now() - ms;
|
|
127
127
|
}, [window]);
|
|
128
128
|
|
|
129
|
-
const value: TimeWindowContextValue = useMemo(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
129
|
+
const value: TimeWindowContextValue = useMemo(
|
|
130
|
+
() => ({
|
|
131
|
+
window,
|
|
132
|
+
setWindow,
|
|
133
|
+
cycleWindow,
|
|
134
|
+
cycleWindowBack,
|
|
135
|
+
windowMs,
|
|
136
|
+
windowLabel,
|
|
137
|
+
getWindowStart,
|
|
138
|
+
budgetType,
|
|
139
|
+
budgetTypeLabel,
|
|
140
|
+
budgetLock,
|
|
141
|
+
setBudgetLock,
|
|
142
|
+
cycleBudgetLock,
|
|
143
|
+
budgetLockLabel,
|
|
144
|
+
}),
|
|
145
|
+
[
|
|
146
|
+
window,
|
|
147
|
+
setWindow,
|
|
148
|
+
cycleWindow,
|
|
149
|
+
windowMs,
|
|
150
|
+
windowLabel,
|
|
151
|
+
getWindowStart,
|
|
152
|
+
budgetType,
|
|
153
|
+
budgetTypeLabel,
|
|
154
|
+
budgetLock,
|
|
155
|
+
cycleBudgetLock,
|
|
156
|
+
budgetLockLabel,
|
|
157
|
+
],
|
|
149
158
|
);
|
|
159
|
+
|
|
160
|
+
return <TimeWindowContext.Provider value={value}>{children}</TimeWindowContext.Provider>;
|
|
150
161
|
}
|
|
151
162
|
|
|
152
163
|
export function useTimeWindow(): TimeWindowContextValue {
|
|
153
164
|
const context = useContext(TimeWindowContext);
|
|
154
165
|
if (!context) {
|
|
155
|
-
throw new Error(
|
|
166
|
+
throw new Error("useTimeWindow must be used within TimeWindowProvider");
|
|
156
167
|
}
|
|
157
168
|
return context;
|
|
158
169
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { createContext,
|
|
1
|
+
import { createContext, type ReactNode, useCallback, useContext, useState } from "react";
|
|
2
2
|
|
|
3
3
|
interface ToastState {
|
|
4
4
|
message: string;
|
|
5
|
-
type:
|
|
5
|
+
type: "info" | "success" | "warning" | "error";
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
interface ToastContextValue {
|
|
9
9
|
toast: ToastState | null;
|
|
10
|
-
showToast: (message: string, type?:
|
|
10
|
+
showToast: (message: string, type?: "info" | "success" | "warning" | "error") => void;
|
|
11
11
|
dismissToast: () => void;
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -16,9 +16,12 @@ const ToastContext = createContext<ToastContextValue | null>(null);
|
|
|
16
16
|
export function ToastProvider({ children }: { children: ReactNode }) {
|
|
17
17
|
const [toast, setToast] = useState<ToastState | null>(null);
|
|
18
18
|
|
|
19
|
-
const showToast = useCallback(
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const showToast = useCallback(
|
|
20
|
+
(message: string, type: "info" | "success" | "warning" | "error" = "success") => {
|
|
21
|
+
setToast({ message, type });
|
|
22
|
+
},
|
|
23
|
+
[],
|
|
24
|
+
);
|
|
22
25
|
|
|
23
26
|
const dismissToast = useCallback(() => {
|
|
24
27
|
setToast(null);
|
|
@@ -34,7 +37,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
|
|
34
37
|
export function useToastContext() {
|
|
35
38
|
const context = useContext(ToastContext);
|
|
36
39
|
if (!context) {
|
|
37
|
-
throw new Error(
|
|
40
|
+
throw new Error("useToastContext must be used within a ToastProvider");
|
|
38
41
|
}
|
|
39
42
|
return context;
|
|
40
43
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export { InputProvider, useInputFocus } from "./InputContext.tsx";
|
|
2
|
+
export { type LogEntry, type LogLevel, LogProvider, useLogger, useLogs } from "./LogContext.tsx";
|
|
3
|
+
export {
|
|
4
|
+
PluginProvider,
|
|
5
|
+
useConfiguredProviders,
|
|
6
|
+
usePlugins,
|
|
7
|
+
useProvider,
|
|
8
|
+
} from "./PluginContext.tsx";
|
|
9
|
+
export { ThemeProvider, useColors, useTheme } from "./ThemeContext.tsx";
|
package/src/tui/createApp.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ReactNode } from
|
|
2
|
-
import type { DemoPreset } from
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { DemoPreset } from "@/demo/simulator.ts";
|
|
3
|
+
import { App } from "./App.tsx";
|
|
4
|
+
import { TestModeContext } from "./hooks/useSafeRenderer.ts";
|
|
5
5
|
|
|
6
6
|
export { App };
|
|
7
7
|
|
|
@@ -24,7 +24,7 @@ export const DEFAULT_APP_OPTIONS = {
|
|
|
24
24
|
|
|
25
25
|
export function createAppElement(options: CreateAppOptions = {}): ReactNode {
|
|
26
26
|
const isTestMode = options.testMode ?? DEFAULT_APP_OPTIONS.testMode;
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
const appElement = (
|
|
29
29
|
<App
|
|
30
30
|
debug={options.debug ?? DEFAULT_APP_OPTIONS.debug}
|
|
@@ -34,14 +34,10 @@ export function createAppElement(options: CreateAppOptions = {}): ReactNode {
|
|
|
34
34
|
{...(options.cliPlugins ? { cliPlugins: options.cliPlugins } : {})}
|
|
35
35
|
/>
|
|
36
36
|
);
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
if (isTestMode) {
|
|
39
|
-
return
|
|
40
|
-
<TestModeContext.Provider value={true}>
|
|
41
|
-
{appElement}
|
|
42
|
-
</TestModeContext.Provider>
|
|
43
|
-
);
|
|
39
|
+
return <TestModeContext.Provider value={true}>{appElement}</TestModeContext.Provider>;
|
|
44
40
|
}
|
|
45
|
-
|
|
41
|
+
|
|
46
42
|
return appElement;
|
|
47
43
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Frame Capture Utilities for TUI Debugging
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Provides functions to capture the current terminal render buffer as text,
|
|
5
5
|
* enabling AI-assisted debugging of visual issues.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @see docs/debugging.md for comprehensive usage guide
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
import * as
|
|
12
|
-
import
|
|
13
|
-
import { PATHS } from
|
|
10
|
+
import type { CliRenderer } from "@opentui/core";
|
|
11
|
+
import * as fs from "fs/promises";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
import { PATHS } from "@/storage/paths.ts";
|
|
14
14
|
|
|
15
15
|
/** Metadata stored alongside each frame capture */
|
|
16
16
|
export interface FrameMetadata {
|
|
@@ -41,13 +41,13 @@ export interface BurstConfig {
|
|
|
41
41
|
* Get the frames output directory
|
|
42
42
|
*/
|
|
43
43
|
export function getFramesDir(): string {
|
|
44
|
-
return path.join(PATHS.data.logs,
|
|
44
|
+
return path.join(PATHS.data.logs, "frames");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Capture the current render buffer as a text string.
|
|
49
49
|
* This captures exactly what is displayed in the terminal.
|
|
50
|
-
*
|
|
50
|
+
*
|
|
51
51
|
* @param renderer - The CliRenderer instance
|
|
52
52
|
* @returns The current frame as a text string with line breaks
|
|
53
53
|
*/
|
|
@@ -60,30 +60,30 @@ export function captureFrameText(renderer: CliRenderer): string {
|
|
|
60
60
|
* Generate a timestamp-based filename prefix
|
|
61
61
|
*/
|
|
62
62
|
function generateTimestamp(): string {
|
|
63
|
-
return new Date().toISOString().replace(/[:.]/g,
|
|
63
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Capture the current frame and write to files.
|
|
68
|
-
*
|
|
68
|
+
*
|
|
69
69
|
* Creates two files:
|
|
70
70
|
* - frame-{timestamp}-{label}.txt - The raw frame text
|
|
71
71
|
* - frame-{timestamp}-{label}.json - Metadata (dimensions, timestamp, etc.)
|
|
72
|
-
*
|
|
72
|
+
*
|
|
73
73
|
* @param renderer - The CliRenderer instance
|
|
74
74
|
* @param label - Optional label for the capture (default: "manual")
|
|
75
75
|
* @returns Information about the captured files
|
|
76
76
|
*/
|
|
77
77
|
export async function captureFrameToFile(
|
|
78
78
|
renderer: CliRenderer,
|
|
79
|
-
label =
|
|
79
|
+
label = "manual",
|
|
80
80
|
): Promise<CaptureResult> {
|
|
81
81
|
const dir = getFramesDir();
|
|
82
82
|
await fs.mkdir(dir, { recursive: true });
|
|
83
83
|
|
|
84
84
|
const ts = generateTimestamp();
|
|
85
85
|
const baseName = `frame-${ts}-${label}`;
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
const frame = captureFrameText(renderer);
|
|
88
88
|
const metadata: FrameMetadata = {
|
|
89
89
|
timestamp: new Date().toISOString(),
|
|
@@ -96,8 +96,8 @@ export async function captureFrameToFile(
|
|
|
96
96
|
const metadataPath = path.join(dir, `${baseName}.json`);
|
|
97
97
|
|
|
98
98
|
await Promise.all([
|
|
99
|
-
fs.writeFile(framePath, frame,
|
|
100
|
-
fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2),
|
|
99
|
+
fs.writeFile(framePath, frame, "utf-8"),
|
|
100
|
+
fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8"),
|
|
101
101
|
]);
|
|
102
102
|
|
|
103
103
|
return { framePath, metadataPath, metadata };
|
|
@@ -133,7 +133,7 @@ export class BurstRecorder {
|
|
|
133
133
|
*/
|
|
134
134
|
async start(): Promise<CaptureResult[]> {
|
|
135
135
|
if (this.isRecording) {
|
|
136
|
-
throw new Error(
|
|
136
|
+
throw new Error("Burst recording already in progress");
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
this.isRecording = true;
|
|
@@ -150,7 +150,7 @@ export class BurstRecorder {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
this.lastCaptureTime = now;
|
|
153
|
-
|
|
153
|
+
|
|
154
154
|
try {
|
|
155
155
|
const result = await this.captureFrame();
|
|
156
156
|
this.capturedFrames.push(result);
|
|
@@ -184,7 +184,7 @@ export class BurstRecorder {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
this.isRecording = false;
|
|
187
|
-
|
|
187
|
+
|
|
188
188
|
if (this.cleanupFn) {
|
|
189
189
|
this.cleanupFn();
|
|
190
190
|
this.cleanupFn = null;
|
|
@@ -219,13 +219,13 @@ export class BurstRecorder {
|
|
|
219
219
|
const dir = path.join(getFramesDir(), `burst-${this.burstId}`);
|
|
220
220
|
await fs.mkdir(dir, { recursive: true });
|
|
221
221
|
|
|
222
|
-
const frameNum = String(this.framesCaptured + 1).padStart(4,
|
|
222
|
+
const frameNum = String(this.framesCaptured + 1).padStart(4, "0");
|
|
223
223
|
const baseName = `frame-${frameNum}`;
|
|
224
|
-
|
|
224
|
+
|
|
225
225
|
const frame = captureFrameText(this.renderer);
|
|
226
226
|
const metadata: FrameMetadata = {
|
|
227
227
|
timestamp: new Date().toISOString(),
|
|
228
|
-
label:
|
|
228
|
+
label: "burst",
|
|
229
229
|
width: this.renderer.width,
|
|
230
230
|
height: this.renderer.height,
|
|
231
231
|
frameIndex: this.framesCaptured + 1,
|
|
@@ -236,8 +236,8 @@ export class BurstRecorder {
|
|
|
236
236
|
const metadataPath = path.join(dir, `${baseName}.json`);
|
|
237
237
|
|
|
238
238
|
await Promise.all([
|
|
239
|
-
fs.writeFile(framePath, frame,
|
|
240
|
-
fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2),
|
|
239
|
+
fs.writeFile(framePath, frame, "utf-8"),
|
|
240
|
+
fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8"),
|
|
241
241
|
]);
|
|
242
242
|
|
|
243
243
|
return { framePath, metadataPath, metadata };
|
|
@@ -246,7 +246,7 @@ export class BurstRecorder {
|
|
|
246
246
|
|
|
247
247
|
/**
|
|
248
248
|
* Create a burst recorder for capturing multiple frames.
|
|
249
|
-
*
|
|
249
|
+
*
|
|
250
250
|
* @example
|
|
251
251
|
* ```ts
|
|
252
252
|
* const recorder = createBurstRecorder(renderer, { frameCount: 5 });
|
|
@@ -254,9 +254,6 @@ export class BurstRecorder {
|
|
|
254
254
|
* console.log(`Captured ${frames.length} frames`);
|
|
255
255
|
* ```
|
|
256
256
|
*/
|
|
257
|
-
export function createBurstRecorder(
|
|
258
|
-
renderer: CliRenderer,
|
|
259
|
-
config: BurstConfig
|
|
260
|
-
): BurstRecorder {
|
|
257
|
+
export function createBurstRecorder(renderer: CliRenderer, config: BurstConfig): BurstRecorder {
|
|
261
258
|
return new BurstRecorder(renderer, config);
|
|
262
259
|
}
|