rax-flow 0.1.9 → 0.2.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/dist/bin.js +8 -6
- package/dist/bin.js.map +1 -1
- package/dist/hub/commands/index.d.ts.map +1 -1
- package/dist/hub/commands/index.js +7 -4
- package/dist/hub/commands/index.js.map +1 -1
- package/dist/hub/event-listener.d.ts +6 -0
- package/dist/hub/event-listener.d.ts.map +1 -1
- package/dist/hub/event-listener.js +10 -6
- package/dist/hub/event-listener.js.map +1 -1
- package/dist/hub/index.d.ts.map +1 -1
- package/dist/hub/index.js +12 -0
- package/dist/hub/index.js.map +1 -1
- package/dist/setup/components/ApiKeyInput.d.ts +25 -0
- package/dist/setup/components/ApiKeyInput.d.ts.map +1 -0
- package/dist/setup/components/ApiKeyInput.js +54 -0
- package/dist/setup/components/ApiKeyInput.js.map +1 -0
- package/dist/setup/components/AsciiBanner.d.ts +22 -0
- package/dist/setup/components/AsciiBanner.d.ts.map +1 -0
- package/dist/setup/components/AsciiBanner.js +55 -0
- package/dist/setup/components/AsciiBanner.js.map +1 -0
- package/dist/setup/components/CliDetector.d.ts +17 -0
- package/dist/setup/components/CliDetector.d.ts.map +1 -0
- package/dist/setup/components/CliDetector.js +79 -0
- package/dist/setup/components/CliDetector.js.map +1 -0
- package/dist/setup/components/ModeSelector.d.ts +8 -0
- package/dist/setup/components/ModeSelector.d.ts.map +1 -0
- package/dist/setup/components/ModeSelector.js +76 -0
- package/dist/setup/components/ModeSelector.js.map +1 -0
- package/dist/setup/components/ProviderSelector.d.ts +18 -0
- package/dist/setup/components/ProviderSelector.d.ts.map +1 -0
- package/dist/setup/components/ProviderSelector.js +97 -0
- package/dist/setup/components/ProviderSelector.js.map +1 -0
- package/dist/setup/components/SetupWizard.d.ts +2 -0
- package/dist/setup/components/SetupWizard.d.ts.map +1 -0
- package/dist/setup/components/SetupWizard.js +212 -0
- package/dist/setup/components/SetupWizard.js.map +1 -0
- package/dist/setup/components/StepIndicator.d.ts +13 -0
- package/dist/setup/components/StepIndicator.d.ts.map +1 -0
- package/dist/setup/components/StepIndicator.js +18 -0
- package/dist/setup/components/StepIndicator.js.map +1 -0
- package/dist/setup/components/SuccessScreen.d.ts +18 -0
- package/dist/setup/components/SuccessScreen.d.ts.map +1 -0
- package/dist/setup/components/SuccessScreen.js +38 -0
- package/dist/setup/components/SuccessScreen.js.map +1 -0
- package/dist/setup/index.d.ts +12 -0
- package/dist/setup/index.d.ts.map +1 -0
- package/dist/setup/index.js +29 -0
- package/dist/setup/index.js.map +1 -0
- package/dist/setup/utils/cli-detection.d.ts +12 -0
- package/dist/setup/utils/cli-detection.d.ts.map +1 -0
- package/dist/setup/utils/cli-detection.js +83 -0
- package/dist/setup/utils/cli-detection.js.map +1 -0
- package/dist/setup/utils/config-writer.d.ts +43 -0
- package/dist/setup/utils/config-writer.d.ts.map +1 -0
- package/dist/setup/utils/config-writer.js +180 -0
- package/dist/setup/utils/config-writer.js.map +1 -0
- package/dist/tui/App.d.ts +2 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +78 -18
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/AgentStateIcon.d.ts +18 -0
- package/dist/tui/components/AgentStateIcon.d.ts.map +1 -0
- package/dist/tui/components/AgentStateIcon.js +57 -0
- package/dist/tui/components/AgentStateIcon.js.map +1 -0
- package/dist/tui/components/AnimatedBranch.d.ts +39 -0
- package/dist/tui/components/AnimatedBranch.d.ts.map +1 -0
- package/dist/tui/components/AnimatedBranch.js +64 -0
- package/dist/tui/components/AnimatedBranch.js.map +1 -0
- package/dist/tui/components/ChatPanel.d.ts +2 -1
- package/dist/tui/components/ChatPanel.d.ts.map +1 -1
- package/dist/tui/components/ChatPanel.js +47 -28
- package/dist/tui/components/ChatPanel.js.map +1 -1
- package/dist/tui/components/DAGPanel.d.ts +14 -2
- package/dist/tui/components/DAGPanel.d.ts.map +1 -1
- package/dist/tui/components/DAGPanel.js +48 -27
- package/dist/tui/components/DAGPanel.js.map +1 -1
- package/dist/tui/components/ExecutionTimeline.d.ts +34 -0
- package/dist/tui/components/ExecutionTimeline.d.ts.map +1 -0
- package/dist/tui/components/ExecutionTimeline.js +67 -0
- package/dist/tui/components/ExecutionTimeline.js.map +1 -0
- package/dist/tui/components/Header.d.ts +2 -1
- package/dist/tui/components/Header.d.ts.map +1 -1
- package/dist/tui/components/Header.js +23 -20
- package/dist/tui/components/Header.js.map +1 -1
- package/dist/tui/components/HelpOverlay.d.ts +2 -1
- package/dist/tui/components/HelpOverlay.d.ts.map +1 -1
- package/dist/tui/components/HelpOverlay.js +59 -41
- package/dist/tui/components/HelpOverlay.js.map +1 -1
- package/dist/tui/components/InputBar.d.ts +3 -1
- package/dist/tui/components/InputBar.d.ts.map +1 -1
- package/dist/tui/components/InputBar.js +12 -7
- package/dist/tui/components/InputBar.js.map +1 -1
- package/dist/tui/components/LogsPanel.d.ts +3 -1
- package/dist/tui/components/LogsPanel.d.ts.map +1 -1
- package/dist/tui/components/LogsPanel.js +52 -3
- package/dist/tui/components/LogsPanel.js.map +1 -1
- package/dist/tui/components/MemoryPanel.d.ts +8 -6
- package/dist/tui/components/MemoryPanel.d.ts.map +1 -1
- package/dist/tui/components/MemoryPanel.js +37 -10
- package/dist/tui/components/MemoryPanel.js.map +1 -1
- package/dist/tui/components/MetricsPanel.d.ts +12 -7
- package/dist/tui/components/MetricsPanel.d.ts.map +1 -1
- package/dist/tui/components/MetricsPanel.js +24 -11
- package/dist/tui/components/MetricsPanel.js.map +1 -1
- package/dist/tui/components/StatusPanel.d.ts +3 -1
- package/dist/tui/components/StatusPanel.d.ts.map +1 -1
- package/dist/tui/components/StatusPanel.js +20 -18
- package/dist/tui/components/StatusPanel.js.map +1 -1
- package/dist/tui/components/TaskTree.d.ts +28 -0
- package/dist/tui/components/TaskTree.d.ts.map +1 -0
- package/dist/tui/components/TaskTree.js +29 -0
- package/dist/tui/components/TaskTree.js.map +1 -0
- package/dist/tui/components/animations/ProgressBar.d.ts +39 -0
- package/dist/tui/components/animations/ProgressBar.d.ts.map +1 -0
- package/dist/tui/components/animations/ProgressBar.js +39 -0
- package/dist/tui/components/animations/ProgressBar.js.map +1 -0
- package/dist/tui/components/animations/Pulse.d.ts +17 -0
- package/dist/tui/components/animations/Pulse.d.ts.map +1 -0
- package/dist/tui/components/animations/Pulse.js +47 -0
- package/dist/tui/components/animations/Pulse.js.map +1 -0
- package/dist/tui/components/animations/Spinner.d.ts +13 -0
- package/dist/tui/components/animations/Spinner.d.ts.map +1 -0
- package/dist/tui/components/animations/Spinner.js +36 -0
- package/dist/tui/components/animations/Spinner.js.map +1 -0
- package/dist/tui/components/animations/StatusAnimator.d.ts +27 -0
- package/dist/tui/components/animations/StatusAnimator.d.ts.map +1 -0
- package/dist/tui/components/animations/StatusAnimator.js +85 -0
- package/dist/tui/components/animations/StatusAnimator.js.map +1 -0
- package/dist/tui/components/animations/TypingEffect.d.ts +26 -0
- package/dist/tui/components/animations/TypingEffect.d.ts.map +1 -0
- package/dist/tui/components/animations/TypingEffect.js +59 -0
- package/dist/tui/components/animations/TypingEffect.js.map +1 -0
- package/dist/tui/components/animations/index.d.ts +8 -0
- package/dist/tui/components/animations/index.d.ts.map +1 -0
- package/dist/tui/components/animations/index.js +6 -0
- package/dist/tui/components/animations/index.js.map +1 -0
- package/dist/tui/hooks/useAnimation.d.ts +42 -0
- package/dist/tui/hooks/useAnimation.d.ts.map +1 -0
- package/dist/tui/hooks/useAnimation.js +212 -0
- package/dist/tui/hooks/useAnimation.js.map +1 -0
- package/dist/tui/hooks/useAppState.d.ts +9 -6
- package/dist/tui/hooks/useAppState.d.ts.map +1 -1
- package/dist/tui/hooks/useAppState.js +84 -69
- package/dist/tui/hooks/useAppState.js.map +1 -1
- package/dist/tui/services/orchestrator.d.ts.map +1 -1
- package/dist/tui/services/orchestrator.js +47 -3
- package/dist/tui/services/orchestrator.js.map +1 -1
- package/dist/tui/styles/borders.d.ts +31 -0
- package/dist/tui/styles/borders.d.ts.map +1 -0
- package/dist/tui/styles/borders.js +47 -0
- package/dist/tui/styles/borders.js.map +1 -0
- package/dist/tui/styles/colors.d.ts +18 -0
- package/dist/tui/styles/colors.d.ts.map +1 -0
- package/dist/tui/styles/colors.js +18 -0
- package/dist/tui/styles/colors.js.map +1 -0
- package/dist/tui/styles/index.d.ts +6 -0
- package/dist/tui/styles/index.d.ts.map +1 -0
- package/dist/tui/styles/index.js +6 -0
- package/dist/tui/styles/index.js.map +1 -0
- package/dist/tui/styles/indicators.d.ts +67 -0
- package/dist/tui/styles/indicators.d.ts.map +1 -0
- package/dist/tui/styles/indicators.js +42 -0
- package/dist/tui/styles/indicators.js.map +1 -0
- package/dist/tui/styles/layout.d.ts +21 -0
- package/dist/tui/styles/layout.d.ts.map +1 -0
- package/dist/tui/styles/layout.js +42 -0
- package/dist/tui/styles/layout.js.map +1 -0
- package/dist/tui/styles/providers.d.ts +77 -0
- package/dist/tui/styles/providers.d.ts.map +1 -0
- package/dist/tui/styles/providers.js +31 -0
- package/dist/tui/styles/providers.js.map +1 -0
- package/dist/tui/utils/animation.d.ts +44 -0
- package/dist/tui/utils/animation.d.ts.map +1 -0
- package/dist/tui/utils/animation.js +107 -0
- package/dist/tui/utils/animation.js.map +1 -0
- package/package.json +8 -8
- package/src/bin.ts +8 -5
- package/src/hub/commands/index.ts +7 -4
- package/src/hub/event-listener.ts +14 -10
- package/src/hub/index.ts +14 -2
- package/src/setup/components/ApiKeyInput.tsx +158 -0
- package/src/setup/components/AsciiBanner.tsx +125 -0
- package/src/setup/components/CliDetector.tsx +230 -0
- package/src/setup/components/ModeSelector.tsx +137 -0
- package/src/setup/components/ProviderSelector.tsx +174 -0
- package/src/setup/components/SetupWizard.tsx +368 -0
- package/src/setup/components/StepIndicator.tsx +74 -0
- package/src/setup/components/SuccessScreen.tsx +229 -0
- package/src/setup/index.ts +34 -0
- package/src/setup/utils/cli-detection.ts +99 -0
- package/src/setup/utils/config-writer.ts +249 -0
- package/src/tui/App.tsx +95 -64
- package/src/tui/components/AgentStateIcon.tsx +84 -0
- package/src/tui/components/AnimatedBranch.tsx +134 -0
- package/src/tui/components/ChatPanel.tsx +85 -43
- package/src/tui/components/DAGPanel.tsx +150 -58
- package/src/tui/components/ExecutionTimeline.tsx +225 -0
- package/src/tui/components/Header.tsx +67 -43
- package/src/tui/components/HelpOverlay.tsx +118 -61
- package/src/tui/components/InputBar.tsx +33 -19
- package/src/tui/components/LogsPanel.tsx +106 -14
- package/src/tui/components/MemoryPanel.tsx +106 -42
- package/src/tui/components/MetricsPanel.tsx +102 -61
- package/src/tui/components/StatusPanel.tsx +84 -37
- package/src/tui/components/TaskTree.tsx +159 -0
- package/src/tui/components/animations/ProgressBar.tsx +160 -0
- package/src/tui/components/animations/Pulse.tsx +73 -0
- package/src/tui/components/animations/Spinner.tsx +54 -0
- package/src/tui/components/animations/StatusAnimator.tsx +153 -0
- package/src/tui/components/animations/TypingEffect.tsx +119 -0
- package/src/tui/components/animations/index.ts +16 -0
- package/src/tui/hooks/useAnimation.ts +290 -0
- package/src/tui/hooks/useAppState.ts +52 -32
- package/src/tui/services/orchestrator.ts +57 -4
- package/src/tui/styles/borders.ts +51 -0
- package/src/tui/styles/colors.ts +19 -0
- package/src/tui/styles/index.ts +20 -0
- package/src/tui/styles/indicators.ts +54 -0
- package/src/tui/styles/layout.ts +44 -0
- package/src/tui/styles/providers.ts +32 -0
- package/src/tui/utils/animation.ts +124 -0
- package/LICENSE +0 -21
- package/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export interface AnimationOptions {
|
|
4
|
+
interval?: number;
|
|
5
|
+
loop?: boolean;
|
|
6
|
+
autoStart?: boolean;
|
|
7
|
+
totalFrames?: number;
|
|
8
|
+
maxFps?: number;
|
|
9
|
+
pauseOnHidden?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AnimationState {
|
|
13
|
+
frame: number;
|
|
14
|
+
tick: number;
|
|
15
|
+
isAnimating: boolean;
|
|
16
|
+
progress: number;
|
|
17
|
+
start: () => void;
|
|
18
|
+
stop: () => void;
|
|
19
|
+
reset: () => void;
|
|
20
|
+
toggle: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DEFAULT_FPS_LIMIT = 24;
|
|
24
|
+
const FRAME_SKIP_THRESHOLD = 2;
|
|
25
|
+
|
|
26
|
+
export function useAnimation(options: AnimationOptions = {}): AnimationState {
|
|
27
|
+
const {
|
|
28
|
+
interval = 100,
|
|
29
|
+
loop = true,
|
|
30
|
+
autoStart = true,
|
|
31
|
+
totalFrames = 100,
|
|
32
|
+
maxFps = DEFAULT_FPS_LIMIT,
|
|
33
|
+
pauseOnHidden = true,
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
const [frame, setFrame] = useState(0);
|
|
37
|
+
const [isAnimating, setIsAnimating] = useState(autoStart);
|
|
38
|
+
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
39
|
+
const lastFrameTimeRef = useRef<number>(0);
|
|
40
|
+
const frameCountRef = useRef<number>(0);
|
|
41
|
+
const isHiddenRef = useRef<boolean>(false);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (!pauseOnHidden || typeof document === "undefined") return;
|
|
45
|
+
|
|
46
|
+
const handleVisibility = () => {
|
|
47
|
+
isHiddenRef.current = document.hidden;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
document.addEventListener("visibilitychange", handleVisibility);
|
|
51
|
+
return () => document.removeEventListener("visibilitychange", handleVisibility);
|
|
52
|
+
}, [pauseOnHidden]);
|
|
53
|
+
|
|
54
|
+
const start = useCallback(() => {
|
|
55
|
+
setIsAnimating(true);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const stop = useCallback(() => {
|
|
59
|
+
setIsAnimating(false);
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const reset = useCallback(() => {
|
|
63
|
+
setFrame(0);
|
|
64
|
+
frameCountRef.current = 0;
|
|
65
|
+
lastFrameTimeRef.current = 0;
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const toggle = useCallback(() => {
|
|
69
|
+
setIsAnimating((prev) => !prev);
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!isAnimating) {
|
|
74
|
+
if (intervalRef.current) {
|
|
75
|
+
clearInterval(intervalRef.current);
|
|
76
|
+
intervalRef.current = null;
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const minFrameTime = 1000 / maxFps;
|
|
82
|
+
|
|
83
|
+
intervalRef.current = setInterval(() => {
|
|
84
|
+
if (pauseOnHidden && isHiddenRef.current) return;
|
|
85
|
+
|
|
86
|
+
const now = performance.now();
|
|
87
|
+
const elapsed = now - lastFrameTimeRef.current;
|
|
88
|
+
|
|
89
|
+
if (elapsed < minFrameTime) return;
|
|
90
|
+
|
|
91
|
+
const framesToSkip = Math.floor(elapsed / interval) - 1;
|
|
92
|
+
const shouldSkipFrame = framesToSkip > FRAME_SKIP_THRESHOLD;
|
|
93
|
+
|
|
94
|
+
if (shouldSkipFrame) {
|
|
95
|
+
frameCountRef.current += framesToSkip;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lastFrameTimeRef.current = now;
|
|
99
|
+
frameCountRef.current++;
|
|
100
|
+
|
|
101
|
+
setFrame((prev) => {
|
|
102
|
+
if (loop) {
|
|
103
|
+
return prev + 1;
|
|
104
|
+
}
|
|
105
|
+
return prev >= totalFrames - 1 ? prev : prev + 1;
|
|
106
|
+
});
|
|
107
|
+
}, interval);
|
|
108
|
+
|
|
109
|
+
return () => {
|
|
110
|
+
if (intervalRef.current) {
|
|
111
|
+
clearInterval(intervalRef.current);
|
|
112
|
+
intervalRef.current = null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}, [isAnimating, interval, loop, totalFrames, maxFps, pauseOnHidden]);
|
|
116
|
+
|
|
117
|
+
const progress = totalFrames > 0 ? (frame % totalFrames) / totalFrames : 0;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
frame,
|
|
121
|
+
tick: frame,
|
|
122
|
+
isAnimating,
|
|
123
|
+
progress,
|
|
124
|
+
start,
|
|
125
|
+
stop,
|
|
126
|
+
reset,
|
|
127
|
+
toggle,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function usePulse(interval: number = 2000): { intensity: number; frame: number } {
|
|
132
|
+
const { frame } = useAnimation({ interval: 100, loop: true, maxFps: 15 });
|
|
133
|
+
const intensity = 0.5 + Math.sin((frame * 100 / interval) * Math.PI * 2) * 0.5;
|
|
134
|
+
return { intensity, frame };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function useTypewriter(
|
|
138
|
+
text: string,
|
|
139
|
+
speed: number = 50,
|
|
140
|
+
startDelay: number = 0
|
|
141
|
+
): { displayedText: string; isComplete: boolean; restart: () => void } {
|
|
142
|
+
const [displayedChars, setDisplayedChars] = useState(0);
|
|
143
|
+
const [isStarted, setIsStarted] = useState(false);
|
|
144
|
+
const [isComplete, setIsComplete] = useState(false);
|
|
145
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
146
|
+
|
|
147
|
+
const restart = useCallback(() => {
|
|
148
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
149
|
+
setDisplayedChars(0);
|
|
150
|
+
setIsStarted(false);
|
|
151
|
+
setIsComplete(false);
|
|
152
|
+
}, []);
|
|
153
|
+
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
restart();
|
|
156
|
+
}, [text, restart]);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (startDelay > 0 && !isStarted) {
|
|
160
|
+
const timeout = setTimeout(() => setIsStarted(true), startDelay);
|
|
161
|
+
return () => clearTimeout(timeout);
|
|
162
|
+
} else if (startDelay === 0) {
|
|
163
|
+
setIsStarted(true);
|
|
164
|
+
}
|
|
165
|
+
}, [startDelay, isStarted]);
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!isStarted || isComplete) return;
|
|
169
|
+
|
|
170
|
+
if (displayedChars >= text.length) {
|
|
171
|
+
setIsComplete(true);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
timeoutRef.current = setTimeout(() => {
|
|
176
|
+
setDisplayedChars((prev) => prev + 1);
|
|
177
|
+
}, speed);
|
|
178
|
+
|
|
179
|
+
return () => {
|
|
180
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
181
|
+
};
|
|
182
|
+
}, [isStarted, displayedChars, text.length, speed, isComplete]);
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
displayedText: text.slice(0, displayedChars),
|
|
186
|
+
isComplete,
|
|
187
|
+
restart,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function useCountdown(
|
|
192
|
+
initialSeconds: number,
|
|
193
|
+
options: { autoStart?: boolean; onComplete?: () => void } = {}
|
|
194
|
+
): { seconds: number; isRunning: boolean; start: () => void; pause: () => void; reset: () => void } {
|
|
195
|
+
const { autoStart = false, onComplete } = options;
|
|
196
|
+
const [seconds, setSeconds] = useState(initialSeconds);
|
|
197
|
+
const [isRunning, setIsRunning] = useState(autoStart);
|
|
198
|
+
const onCompleteRef = useRef(onComplete);
|
|
199
|
+
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
200
|
+
onCompleteRef.current = onComplete;
|
|
201
|
+
|
|
202
|
+
const start = useCallback(() => setIsRunning(true), []);
|
|
203
|
+
const pause = useCallback(() => setIsRunning(false), []);
|
|
204
|
+
const reset = useCallback(() => {
|
|
205
|
+
setSeconds(initialSeconds);
|
|
206
|
+
setIsRunning(false);
|
|
207
|
+
}, [initialSeconds]);
|
|
208
|
+
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!isRunning || seconds <= 0) {
|
|
211
|
+
if (seconds <= 0 && isRunning) {
|
|
212
|
+
setIsRunning(false);
|
|
213
|
+
onCompleteRef.current?.();
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
intervalRef.current = setInterval(() => {
|
|
219
|
+
setSeconds((prev) => prev - 1);
|
|
220
|
+
}, 1000);
|
|
221
|
+
|
|
222
|
+
return () => {
|
|
223
|
+
if (intervalRef.current) clearInterval(intervalRef.current);
|
|
224
|
+
};
|
|
225
|
+
}, [isRunning, seconds]);
|
|
226
|
+
|
|
227
|
+
return { seconds, isRunning, start, pause, reset };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function useFps(targetFps: number = 24): number {
|
|
231
|
+
const [frame, setFrame] = useState(0);
|
|
232
|
+
const frameRef = useRef(0);
|
|
233
|
+
const lastTimeRef = useRef(performance.now());
|
|
234
|
+
const rafIdRef = useRef<number | null>(null);
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
const interval = 1000 / targetFps;
|
|
238
|
+
|
|
239
|
+
const tick = () => {
|
|
240
|
+
const now = performance.now();
|
|
241
|
+
const delta = now - lastTimeRef.current;
|
|
242
|
+
|
|
243
|
+
if (delta >= interval) {
|
|
244
|
+
lastTimeRef.current = now - (delta % interval);
|
|
245
|
+
frameRef.current++;
|
|
246
|
+
setFrame(frameRef.current);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
rafIdRef.current = requestAnimationFrame(tick);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
rafIdRef.current = requestAnimationFrame(tick);
|
|
253
|
+
return () => {
|
|
254
|
+
if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current);
|
|
255
|
+
};
|
|
256
|
+
}, [targetFps]);
|
|
257
|
+
|
|
258
|
+
return frame;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function useDebouncedCallback<T extends (...args: unknown[]) => void>(
|
|
262
|
+
callback: T,
|
|
263
|
+
delay: number
|
|
264
|
+
): T {
|
|
265
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
266
|
+
const callbackRef = useRef(callback);
|
|
267
|
+
callbackRef.current = callback;
|
|
268
|
+
|
|
269
|
+
return useCallback(((...args: Parameters<T>) => {
|
|
270
|
+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
271
|
+
timeoutRef.current = setTimeout(() => callbackRef.current(...args), delay);
|
|
272
|
+
}) as T, [delay]);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function useThrottledCallback<T extends (...args: unknown[]) => void>(
|
|
276
|
+
callback: T,
|
|
277
|
+
delay: number
|
|
278
|
+
): T {
|
|
279
|
+
const lastCallRef = useRef<number>(0);
|
|
280
|
+
const callbackRef = useRef(callback);
|
|
281
|
+
callbackRef.current = callback;
|
|
282
|
+
|
|
283
|
+
return useCallback(((...args: Parameters<T>) => {
|
|
284
|
+
const now = performance.now();
|
|
285
|
+
if (now - lastCallRef.current >= delay) {
|
|
286
|
+
lastCallRef.current = now;
|
|
287
|
+
callbackRef.current(...args);
|
|
288
|
+
}
|
|
289
|
+
}) as T, [delay]);
|
|
290
|
+
}
|
|
@@ -41,6 +41,15 @@ interface WorkflowState {
|
|
|
41
41
|
totalProgress: number;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
interface MetricsState {
|
|
45
|
+
sessions: number;
|
|
46
|
+
avgDuration: number;
|
|
47
|
+
successRate: number;
|
|
48
|
+
totalCost: number;
|
|
49
|
+
totalRetries: number;
|
|
50
|
+
totalEscalations: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
interface AppState {
|
|
45
54
|
projectName: string;
|
|
46
55
|
agentCount: number;
|
|
@@ -55,12 +64,7 @@ interface AppState {
|
|
|
55
64
|
isProcessing: boolean;
|
|
56
65
|
workflowState: WorkflowState;
|
|
57
66
|
logs: string[];
|
|
58
|
-
metrics:
|
|
59
|
-
sessions: number;
|
|
60
|
-
avgDuration: number;
|
|
61
|
-
successRate: number;
|
|
62
|
-
totalCost: number;
|
|
63
|
-
};
|
|
67
|
+
metrics: MetricsState;
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
const COMMAND_SUGGESTIONS = [
|
|
@@ -150,8 +154,10 @@ export function useAppState() {
|
|
|
150
154
|
metrics: {
|
|
151
155
|
sessions: 0,
|
|
152
156
|
avgDuration: 0,
|
|
153
|
-
successRate:
|
|
157
|
+
successRate: 100,
|
|
154
158
|
totalCost: 0,
|
|
159
|
+
totalRetries: 0,
|
|
160
|
+
totalEscalations: 0,
|
|
155
161
|
},
|
|
156
162
|
});
|
|
157
163
|
|
|
@@ -205,17 +211,30 @@ export function useAppState() {
|
|
|
205
211
|
|
|
206
212
|
case "run_end":
|
|
207
213
|
addLog(`[${timeStr}] [DONE] Task completed (confidence: ${event.metrics.confidence.toFixed(2)})`);
|
|
208
|
-
setState((prev) =>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
metrics
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
214
|
+
setState((prev) => {
|
|
215
|
+
const newSessions = prev.metrics.sessions + 1;
|
|
216
|
+
const newAvgDuration = prev.metrics.avgDuration === 0
|
|
217
|
+
? event.metrics.totalLatencyMs
|
|
218
|
+
: Math.round((prev.metrics.avgDuration * prev.metrics.sessions + event.metrics.totalLatencyMs) / newSessions);
|
|
219
|
+
const newSuccessRate = event.metrics.confidence >= 0.7
|
|
220
|
+
? Math.round((prev.metrics.successRate * prev.metrics.sessions + 100) / newSessions)
|
|
221
|
+
: Math.round((prev.metrics.successRate * prev.metrics.sessions) / newSessions);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
...prev,
|
|
225
|
+
status: "ready",
|
|
226
|
+
isProcessing: false,
|
|
227
|
+
fitness: Math.min(prev.fitness + 0.02, 0.99),
|
|
228
|
+
metrics: {
|
|
229
|
+
sessions: newSessions,
|
|
230
|
+
avgDuration: newAvgDuration,
|
|
231
|
+
successRate: newSuccessRate,
|
|
232
|
+
totalCost: prev.metrics.totalCost + (event.metrics.totalCostUsd || 0),
|
|
233
|
+
totalRetries: prev.metrics.totalRetries + (event.metrics.retries || 0),
|
|
234
|
+
totalEscalations: prev.metrics.totalEscalations + (event.metrics.escalations || 0),
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
});
|
|
219
238
|
break;
|
|
220
239
|
}
|
|
221
240
|
}, []);
|
|
@@ -288,16 +307,16 @@ export function useAppState() {
|
|
|
288
307
|
}, []);
|
|
289
308
|
|
|
290
309
|
const processCommand = useCallback((input: string) => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
310
|
+
const normalizedInput = input.startsWith("/") ? input : `/${input}`;
|
|
311
|
+
const cmd = normalizedInput.slice(1).toLowerCase();
|
|
312
|
+
const parts = cmd.split(" ");
|
|
313
|
+
const command = parts[0];
|
|
314
|
+
const args = parts.slice(1).join(" ");
|
|
315
|
+
|
|
316
|
+
switch (command) {
|
|
317
|
+
case "help":
|
|
318
|
+
addMessage("system", `Commandes: /run /status /agents /providers /workflows /logs /metrics /memory /doctor /bridge-test /evolve /config /exit\n(Note: commands work with or without '/' prefix)`);
|
|
319
|
+
break;
|
|
301
320
|
|
|
302
321
|
case "status":
|
|
303
322
|
const status = orchestratorRef.current.getStatus();
|
|
@@ -353,11 +372,12 @@ export function useAppState() {
|
|
|
353
372
|
break;
|
|
354
373
|
|
|
355
374
|
default:
|
|
356
|
-
|
|
375
|
+
if (args || command) {
|
|
376
|
+
addMessage("error", `Commande inconnue: /${command}. Tapez /help pour l'aide.`);
|
|
377
|
+
} else {
|
|
378
|
+
runWorkflow(input);
|
|
379
|
+
}
|
|
357
380
|
}
|
|
358
|
-
} else {
|
|
359
|
-
runWorkflow(input);
|
|
360
|
-
}
|
|
361
381
|
}, [state, addMessage]);
|
|
362
382
|
|
|
363
383
|
const runWorkflow = useCallback(async (prompt: string) => {
|
|
@@ -9,6 +9,13 @@ import {
|
|
|
9
9
|
ModelResponse,
|
|
10
10
|
ProviderCallOptions,
|
|
11
11
|
} from "rax-flow-core";
|
|
12
|
+
import {
|
|
13
|
+
OpenAIAdapter,
|
|
14
|
+
ClaudeAdapter,
|
|
15
|
+
GeminiAdapter,
|
|
16
|
+
GroqAdapter,
|
|
17
|
+
HostBridgeAdapter,
|
|
18
|
+
} from "rax-flow-providers";
|
|
12
19
|
|
|
13
20
|
export interface OrchestratorService {
|
|
14
21
|
run(prompt: string): Promise<void>;
|
|
@@ -24,6 +31,36 @@ export interface OrchestratorStatus {
|
|
|
24
31
|
fitness: number;
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
function createProviderFromEnv(name: string): IModelProvider | null {
|
|
35
|
+
switch (name) {
|
|
36
|
+
case "openai":
|
|
37
|
+
if (process.env.OPENAI_API_KEY) {
|
|
38
|
+
return new OpenAIAdapter({ apiKey: process.env.OPENAI_API_KEY });
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
case "claude":
|
|
42
|
+
case "anthropic":
|
|
43
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
44
|
+
return new ClaudeAdapter({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
case "gemini":
|
|
48
|
+
if (process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY) {
|
|
49
|
+
return new GeminiAdapter({ apiKey: process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY! });
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
case "groq":
|
|
53
|
+
if (process.env.GROQ_API_KEY) {
|
|
54
|
+
return new GroqAdapter({ apiKey: process.env.GROQ_API_KEY });
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
case "host":
|
|
58
|
+
return new HostBridgeAdapter({ mode: "auto" });
|
|
59
|
+
default:
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
27
64
|
function createMockProvider(): IModelProvider {
|
|
28
65
|
return {
|
|
29
66
|
async callModel(prompt: string, options?: ProviderCallOptions): Promise<ModelResponse<string>> {
|
|
@@ -49,6 +86,25 @@ function createMockProvider(): IModelProvider {
|
|
|
49
86
|
};
|
|
50
87
|
}
|
|
51
88
|
|
|
89
|
+
function buildProviders(): Record<string, IModelProvider> {
|
|
90
|
+
const providers: Record<string, IModelProvider> = {};
|
|
91
|
+
|
|
92
|
+
const hostProvider = createProviderFromEnv("host");
|
|
93
|
+
if (hostProvider) providers["host"] = hostProvider;
|
|
94
|
+
|
|
95
|
+
for (const name of ["openai", "claude", "gemini", "groq"]) {
|
|
96
|
+
const provider = createProviderFromEnv(name);
|
|
97
|
+
if (provider) providers[name] = provider;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (Object.keys(providers).length === 0) {
|
|
101
|
+
console.log("[Orchestrator] No API keys found, using mock provider");
|
|
102
|
+
providers["mock"] = createMockProvider();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return providers;
|
|
106
|
+
}
|
|
107
|
+
|
|
52
108
|
function createAgent(name: string, role: string): AgentDefinition {
|
|
53
109
|
return {
|
|
54
110
|
name,
|
|
@@ -83,10 +139,7 @@ const DEFAULT_AGENTS = [
|
|
|
83
139
|
];
|
|
84
140
|
|
|
85
141
|
export function createOrchestrator(): OrchestratorService {
|
|
86
|
-
const providers
|
|
87
|
-
host: createMockProvider(),
|
|
88
|
-
};
|
|
89
|
-
|
|
142
|
+
const providers = buildProviders();
|
|
90
143
|
const agents: Record<string, AgentDefinition> = {};
|
|
91
144
|
for (const agent of DEFAULT_AGENTS) {
|
|
92
145
|
agents[agent.name] = agent;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const borders = {
|
|
2
|
+
horizontal: "─",
|
|
3
|
+
vertical: "│",
|
|
4
|
+
topLeft: "┌",
|
|
5
|
+
topRight: "┐",
|
|
6
|
+
bottomLeft: "└",
|
|
7
|
+
bottomRight: "┘",
|
|
8
|
+
leftT: "├",
|
|
9
|
+
rightT: "┤",
|
|
10
|
+
topT: "┬",
|
|
11
|
+
bottomT: "┴",
|
|
12
|
+
cross: "┼",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const doubleBorders = {
|
|
16
|
+
horizontal: "═",
|
|
17
|
+
vertical: "║",
|
|
18
|
+
topLeft: "╔",
|
|
19
|
+
topRight: "╗",
|
|
20
|
+
bottomLeft: "╚",
|
|
21
|
+
bottomRight: "╝",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const treeChars = {
|
|
25
|
+
branch: "├─",
|
|
26
|
+
lastBranch: "└─",
|
|
27
|
+
vertical: "│",
|
|
28
|
+
indent: " ",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function drawTopBorder(width: number, title?: string): string {
|
|
32
|
+
const { topLeft, topRight, horizontal } = borders;
|
|
33
|
+
if (title) {
|
|
34
|
+
const titleStr = ` ${title} `;
|
|
35
|
+
const titleLen = titleStr.length;
|
|
36
|
+
const remaining = width - 2 - titleLen;
|
|
37
|
+
const rightDashes = Math.max(0, remaining);
|
|
38
|
+
return `${topLeft}${titleStr}${horizontal.repeat(rightDashes)}${topRight}`;
|
|
39
|
+
}
|
|
40
|
+
return `${topLeft}${horizontal.repeat(width - 2)}${topRight}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function drawBottomBorder(width: number): string {
|
|
44
|
+
const { bottomLeft, bottomRight, horizontal } = borders;
|
|
45
|
+
return `${bottomLeft}${horizontal.repeat(width - 2)}${bottomRight}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function drawSeparator(width: number): string {
|
|
49
|
+
const { leftT, rightT, horizontal } = borders;
|
|
50
|
+
return `${leftT}${horizontal.repeat(width - 2)}${rightT}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { colors } from "../../hub/styles/colors.js";
|
|
2
|
+
|
|
3
|
+
export { colors };
|
|
4
|
+
|
|
5
|
+
export const tuiColors = {
|
|
6
|
+
background: "#050505",
|
|
7
|
+
surface: "#0a0a0a",
|
|
8
|
+
primary: "#f97316",
|
|
9
|
+
textPrimary: "#ffffff",
|
|
10
|
+
textSecondary: "#a1a1aa",
|
|
11
|
+
textTertiary: "#71717a",
|
|
12
|
+
textQuaternary: "#3f3f46",
|
|
13
|
+
success: "#22c55e",
|
|
14
|
+
warning: "#f59e0b",
|
|
15
|
+
error: "#ef4444",
|
|
16
|
+
border: "#27272a",
|
|
17
|
+
mutation: "#f59e0b",
|
|
18
|
+
checkpoint: "#f97316",
|
|
19
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { colors, tuiColors } from "./colors.js";
|
|
2
|
+
export { borders, doubleBorders, treeChars, drawTopBorder, drawBottomBorder, drawSeparator } from "./borders.js";
|
|
3
|
+
export {
|
|
4
|
+
statusIndicators,
|
|
5
|
+
getStatusIndicator,
|
|
6
|
+
spinnerFrames,
|
|
7
|
+
getSpinnerFrame,
|
|
8
|
+
dotsSpinnerFrames,
|
|
9
|
+
circleSpinnerFrames,
|
|
10
|
+
pulseFrames,
|
|
11
|
+
breathFrames,
|
|
12
|
+
progressMarkerFrames,
|
|
13
|
+
progressGlowFrames,
|
|
14
|
+
getPulseFrame,
|
|
15
|
+
getBreathFrame,
|
|
16
|
+
getProgressMarker,
|
|
17
|
+
getProgressGlow
|
|
18
|
+
} from "./indicators.js";
|
|
19
|
+
export { providerTags, getProviderTag, logStatusTags } from "./providers.js";
|
|
20
|
+
export { layout, formatTimestamp, formatTimestampMs, padRight, truncate } from "./layout.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export const statusIndicators = {
|
|
2
|
+
online: { icon: "●", color: "#22c55e" },
|
|
3
|
+
offline: { icon: "○", color: "#71717a" },
|
|
4
|
+
pending: { icon: "○", color: "#71717a" },
|
|
5
|
+
running: { icon: "▶", color: "#f97316" },
|
|
6
|
+
loading: { icon: "◐", color: "#f59e0b" },
|
|
7
|
+
queued: { icon: "◐", color: "#f59e0b" },
|
|
8
|
+
done: { icon: "●", color: "#22c55e" },
|
|
9
|
+
success: { icon: "●", color: "#22c55e" },
|
|
10
|
+
error: { icon: "✗", color: "#ef4444" },
|
|
11
|
+
mutation: { icon: "◆", color: "#f59e0b" },
|
|
12
|
+
checkpoint: { icon: "■", color: "#f97316" },
|
|
13
|
+
idle: { icon: "○", color: "#71717a" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function getStatusIndicator(status: string): { icon: string; color: string } {
|
|
17
|
+
const normalized = status.toLowerCase();
|
|
18
|
+
return statusIndicators[normalized as keyof typeof statusIndicators] || statusIndicators.pending;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const spinnerFrames = ["◐", "◓", "◑", "◒"];
|
|
22
|
+
|
|
23
|
+
export const dotsSpinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⼸", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
24
|
+
|
|
25
|
+
export const circleSpinnerFrames = ["◐", "◓", "◑", "◒"];
|
|
26
|
+
|
|
27
|
+
export const pulseFrames = ["●", "◐", "○", "◐"];
|
|
28
|
+
|
|
29
|
+
export const breathFrames = ["●", "●", "◐", "◐", "○", "○", "◐", "◐"];
|
|
30
|
+
|
|
31
|
+
export const progressMarkerFrames = ["▶", "▸", "▶", "▸"];
|
|
32
|
+
|
|
33
|
+
export const progressGlowFrames = ["█", "▓", "█", "▓"];
|
|
34
|
+
|
|
35
|
+
export function getSpinnerFrame(tick: number, type: "circle" | "dots" | "pulse" = "circle"): string {
|
|
36
|
+
const frames = type === "dots" ? dotsSpinnerFrames : type === "pulse" ? pulseFrames : circleSpinnerFrames;
|
|
37
|
+
return frames[tick % frames.length];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getPulseFrame(tick: number): string {
|
|
41
|
+
return pulseFrames[tick % pulseFrames.length];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getBreathFrame(tick: number): string {
|
|
45
|
+
return breathFrames[tick % breathFrames.length];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getProgressMarker(tick: number): string {
|
|
49
|
+
return progressMarkerFrames[tick % progressMarkerFrames.length];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getProgressGlow(tick: number): string {
|
|
53
|
+
return progressGlowFrames[tick % progressGlowFrames.length];
|
|
54
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const layout = {
|
|
2
|
+
defaultWidth: 80,
|
|
3
|
+
panelWidth: {
|
|
4
|
+
chat: 40,
|
|
5
|
+
dag: 38,
|
|
6
|
+
status: 28,
|
|
7
|
+
logs: 32,
|
|
8
|
+
metrics: 28,
|
|
9
|
+
memory: 30,
|
|
10
|
+
},
|
|
11
|
+
padding: {
|
|
12
|
+
x: 1,
|
|
13
|
+
y: 0,
|
|
14
|
+
},
|
|
15
|
+
progressWidth: 16,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function formatTimestamp(date: Date): string {
|
|
19
|
+
return date.toLocaleTimeString("fr-FR", {
|
|
20
|
+
hour: "2-digit",
|
|
21
|
+
minute: "2-digit",
|
|
22
|
+
second: "2-digit",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatTimestampMs(date: Date): string {
|
|
27
|
+
const time = date.toLocaleTimeString("fr-FR", {
|
|
28
|
+
hour: "2-digit",
|
|
29
|
+
minute: "2-digit",
|
|
30
|
+
second: "2-digit",
|
|
31
|
+
});
|
|
32
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
33
|
+
return `${time}.${ms}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function padRight(text: string, width: number): string {
|
|
37
|
+
const padding = Math.max(0, width - text.length);
|
|
38
|
+
return text + " ".repeat(padding);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function truncate(text: string, maxLength: number): string {
|
|
42
|
+
if (text.length <= maxLength) return text;
|
|
43
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
44
|
+
}
|