@matterailab/orbcode 0.1.8 → 0.1.13
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 +96 -3
- package/bin/orbcode.js +3 -0
- package/dist/config/settings.js +101 -1
- package/dist/core/agent.js +240 -7
- package/dist/core/hooks.js +358 -0
- package/dist/core/sessions.js +5 -1
- package/dist/headless.js +14 -1
- package/dist/index.js +4 -0
- package/dist/prompts/system.js +4 -0
- package/dist/ui/App.js +91 -11
- package/dist/ui/components/HookTrustPrompt.js +21 -0
- package/dist/ui/components/StatusBar.js +10 -17
- package/package.json +1 -1
package/dist/ui/App.js
CHANGED
|
@@ -6,12 +6,13 @@ import { COLORS, VERSION } from "../branding.js";
|
|
|
6
6
|
import { AXON_MODELS, getModel, isValidAxonModel } from "../api/models.js";
|
|
7
7
|
import { LoginView } from "./LoginView.js";
|
|
8
8
|
import { APP_URL, fetchBalance, fetchProfile, fetchTaskTitle } from "../auth/auth.js";
|
|
9
|
-
import { getAuthToken, loadSettings, saveSettings } from "../config/settings.js";
|
|
9
|
+
import { getAuthToken, getPendingProjectHooks, loadSettings, saveSettings, trustProjectHooks, } from "../config/settings.js";
|
|
10
10
|
import { Agent } from "../core/agent.js";
|
|
11
11
|
import { Spinner } from "./components/Spinner.js";
|
|
12
12
|
import { InputBox } from "./components/InputBox.js";
|
|
13
13
|
import { ApprovalPrompt } from "./components/ApprovalPrompt.js";
|
|
14
14
|
import { FollowupPrompt } from "./components/FollowupPrompt.js";
|
|
15
|
+
import { HookTrustPrompt } from "./components/HookTrustPrompt.js";
|
|
15
16
|
import { StatusBar } from "./components/StatusBar.js";
|
|
16
17
|
import { ModelPicker } from "./components/ModelPicker.js";
|
|
17
18
|
import { SessionPicker } from "./components/SessionPicker.js";
|
|
@@ -157,6 +158,9 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
157
158
|
const [runningTool, setRunningTool] = useState(null);
|
|
158
159
|
const [pendingApproval, setPendingApproval] = useState(null);
|
|
159
160
|
const [pendingFollowup, setPendingFollowup] = useState(null);
|
|
161
|
+
// Set when the current project defines hooks that haven't been trusted yet;
|
|
162
|
+
// gates input until the user decides (project hooks run shell commands).
|
|
163
|
+
const [pendingHookTrust, setPendingHookTrust] = useState(null);
|
|
160
164
|
// FIFO queue of messages the user typed while the LLM was still streaming.
|
|
161
165
|
// Drained one-per-turn on each `turn-end` event so multi-step work can
|
|
162
166
|
// keep flowing without making the user wait for the previous response.
|
|
@@ -193,6 +197,10 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
193
197
|
// we can drain it inside `handleEvent` without re-creating that callback
|
|
194
198
|
// on every keystroke).
|
|
195
199
|
const queueRef = useRef([]);
|
|
200
|
+
// Holds the startup prompt while we wait for a project-hook trust decision.
|
|
201
|
+
const deferredPromptRef = useRef(null);
|
|
202
|
+
// Guards endAndExit against double-invocation (Ctrl+D spam).
|
|
203
|
+
const exitingRef = useRef(false);
|
|
196
204
|
const enqueueMessage = useCallback((text) => {
|
|
197
205
|
queueRef.current = [...queueRef.current, text];
|
|
198
206
|
setQueuedMessages(queueRef.current);
|
|
@@ -220,7 +228,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
220
228
|
void fetchTaskTitle(agent.taskId, token).then((title) => {
|
|
221
229
|
if (title && agentRef.current?.taskId === agent.taskId) {
|
|
222
230
|
setSessionTitle(title);
|
|
223
|
-
setTerminalTitle(
|
|
231
|
+
setTerminalTitle(title);
|
|
224
232
|
agent.setTitle(title);
|
|
225
233
|
}
|
|
226
234
|
});
|
|
@@ -296,6 +304,9 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
296
304
|
case "completion":
|
|
297
305
|
pushRow({ kind: "completion", text: event.result });
|
|
298
306
|
break;
|
|
307
|
+
case "system":
|
|
308
|
+
pushRow({ kind: event.isError ? "error" : "info", text: event.message });
|
|
309
|
+
break;
|
|
299
310
|
case "error":
|
|
300
311
|
pushRow({ kind: "error", text: event.message });
|
|
301
312
|
break;
|
|
@@ -343,6 +354,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
343
354
|
baseUrl: current.baseUrl,
|
|
344
355
|
autoApproveEdits: current.autoApproveEdits,
|
|
345
356
|
autoApproveSafeCommands: current.autoApproveSafeCommands,
|
|
357
|
+
hooks: current.hooks,
|
|
346
358
|
resume,
|
|
347
359
|
callbacks: {
|
|
348
360
|
onEvent: handleEvent,
|
|
@@ -363,12 +375,16 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
363
375
|
resetTranscript();
|
|
364
376
|
setTasks(session.todos ?? "");
|
|
365
377
|
setTotalCost(session.totalCost ?? 0);
|
|
378
|
+
// Repopulate the status bar immediately. Without this the context
|
|
379
|
+
// number only appears once the next streaming `usage` chunk arrives,
|
|
380
|
+
// which can be after several seconds of "ctx 0".
|
|
381
|
+
setContextTokens(session.contextTokens ?? 0);
|
|
366
382
|
if (session.title) {
|
|
367
383
|
// The stored title is already the backend one (or the prompt
|
|
368
384
|
// fallback); don't re-fetch for this task.
|
|
369
385
|
titleTaskRef.current = session.id;
|
|
370
386
|
setSessionTitle(session.title);
|
|
371
|
-
setTerminalTitle(
|
|
387
|
+
setTerminalTitle(session.title);
|
|
372
388
|
}
|
|
373
389
|
// Replay the conversation into the transcript.
|
|
374
390
|
for (const message of session.messages) {
|
|
@@ -390,6 +406,33 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
390
406
|
agentRef.current?.setModel(modelId);
|
|
391
407
|
pushRow({ kind: "info", text: `Model switched to ${getModel(modelId).name}` });
|
|
392
408
|
}, [pushRow]);
|
|
409
|
+
// Fire SessionEnd hooks (best-effort, capped at 3s) before quitting.
|
|
410
|
+
const endAndExit = useCallback((reason) => {
|
|
411
|
+
const agent = agentRef.current;
|
|
412
|
+
if (!agent) {
|
|
413
|
+
exit();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
// Guard against double-invocation (e.g. the user spamming Ctrl+D):
|
|
417
|
+
// the first call schedules the shutdown; subsequent calls are no-ops.
|
|
418
|
+
if (exitingRef.current)
|
|
419
|
+
return;
|
|
420
|
+
exitingRef.current = true;
|
|
421
|
+
// Clear the cap timer when SessionEnd finishes first — Promise.race
|
|
422
|
+
// leaves the loser pending, and a ref'd setTimeout would otherwise
|
|
423
|
+
// keep the event loop alive (delaying the actual exit by up to 3s).
|
|
424
|
+
let capTimer;
|
|
425
|
+
const cap = new Promise((resolve) => {
|
|
426
|
+
capTimer = setTimeout(resolve, 3000);
|
|
427
|
+
capTimer.unref?.();
|
|
428
|
+
});
|
|
429
|
+
void Promise.race([agent.endSession(reason), cap]).finally(() => {
|
|
430
|
+
if (capTimer)
|
|
431
|
+
clearTimeout(capTimer);
|
|
432
|
+
agent.abort();
|
|
433
|
+
exit();
|
|
434
|
+
});
|
|
435
|
+
}, [exit]);
|
|
393
436
|
const handleCommand = useCallback((command) => {
|
|
394
437
|
const [name, ...rest] = command.split(/\s+/);
|
|
395
438
|
const arg = rest.join(" ");
|
|
@@ -557,6 +600,9 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
557
600
|
break;
|
|
558
601
|
case "/logout": {
|
|
559
602
|
clearQueue();
|
|
603
|
+
void agentRef.current?.endSession("logout");
|
|
604
|
+
setPendingHookTrust(null);
|
|
605
|
+
deferredPromptRef.current = null;
|
|
560
606
|
const updated = { ...settings, token: undefined };
|
|
561
607
|
setSettings(updated);
|
|
562
608
|
saveSettings(updated);
|
|
@@ -565,12 +611,12 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
565
611
|
break;
|
|
566
612
|
}
|
|
567
613
|
case "/exit":
|
|
568
|
-
|
|
614
|
+
endAndExit("prompt_input_exit");
|
|
569
615
|
break;
|
|
570
616
|
default:
|
|
571
617
|
pushRow({ kind: "error", text: `Unknown command: ${name}. Try /help.` });
|
|
572
618
|
}
|
|
573
|
-
}, [settings, tasks, contextTokens, totalCost, sessionTitle,
|
|
619
|
+
}, [settings, tasks, contextTokens, totalCost, sessionTitle, endAndExit, pushRow, getAgent, switchModel, resetTranscript, clearQueue]);
|
|
574
620
|
const handleSubmit = useCallback((value) => {
|
|
575
621
|
if (value.startsWith("/")) {
|
|
576
622
|
handleCommand(value);
|
|
@@ -592,6 +638,28 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
592
638
|
setBusyLabel("Thinking");
|
|
593
639
|
void getAgent().runTurn(value);
|
|
594
640
|
}, [settings, busy, handleCommand, enqueueMessage, getAgent, pushRow]);
|
|
641
|
+
// Resolve the project-hook trust prompt: enable hooks for this workspace (and
|
|
642
|
+
// the live agent) on approval, then run any prompt we deferred while asking.
|
|
643
|
+
const resolveHookTrust = useCallback((trust) => {
|
|
644
|
+
setPendingHookTrust(null);
|
|
645
|
+
if (trust) {
|
|
646
|
+
trustProjectHooks(process.cwd());
|
|
647
|
+
const updated = loadSettings();
|
|
648
|
+
setSettings(updated);
|
|
649
|
+
agentRef.current?.setHooks(updated.hooks);
|
|
650
|
+
pushRow({ kind: "info", text: "Project hooks trusted — enabled for this workspace." });
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
pushRow({
|
|
654
|
+
kind: "info",
|
|
655
|
+
text: "Project hooks left disabled. Review .orbcode/settings.json and restart to re-decide.",
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
const deferred = deferredPromptRef.current;
|
|
659
|
+
deferredPromptRef.current = null;
|
|
660
|
+
if (deferred)
|
|
661
|
+
handleSubmit(deferred);
|
|
662
|
+
}, [handleSubmit, pushRow]);
|
|
595
663
|
// Apply --resume and an initial prompt (`orbcode "do something"`) on startup.
|
|
596
664
|
const bootedRef = useRef(false);
|
|
597
665
|
useEffect(() => {
|
|
@@ -600,8 +668,16 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
600
668
|
bootedRef.current = true;
|
|
601
669
|
if (initialSession)
|
|
602
670
|
handleResume(initialSession);
|
|
603
|
-
|
|
671
|
+
// If this project ships untrusted hooks, ask before running anything; the
|
|
672
|
+
// startup prompt waits until the user decides.
|
|
673
|
+
const pending = getPendingProjectHooks(process.cwd());
|
|
674
|
+
if (pending) {
|
|
675
|
+
setPendingHookTrust(pending);
|
|
676
|
+
deferredPromptRef.current = initialPrompt ?? null;
|
|
677
|
+
}
|
|
678
|
+
else if (initialPrompt) {
|
|
604
679
|
handleSubmit(initialPrompt);
|
|
680
|
+
}
|
|
605
681
|
refreshUsage();
|
|
606
682
|
}, [initialSession, initialPrompt, handleResume, handleSubmit, refreshUsage]);
|
|
607
683
|
// Resolve the npm version check after first paint so the TUI shows up
|
|
@@ -623,8 +699,7 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
623
699
|
// followup). InputBox swallows other Ctrl-combos, but this hook is a
|
|
624
700
|
// sibling of InputBox's hook, so it still sees the key.
|
|
625
701
|
if (key.ctrl && input === "d") {
|
|
626
|
-
|
|
627
|
-
exit();
|
|
702
|
+
endAndExit("prompt_input_exit");
|
|
628
703
|
return;
|
|
629
704
|
}
|
|
630
705
|
if (key.escape && busy && !pendingApproval && !pendingFollowup) {
|
|
@@ -665,21 +740,26 @@ export function App({ initialView, initialPrompt, initialSession, updateCheck, }
|
|
|
665
740
|
pushRow({ kind: "info", text: `Signed in${who ? ` as ${who}` : ""}. Ready when you are.` });
|
|
666
741
|
}, [pushRow]);
|
|
667
742
|
const taskLines = useMemo(() => tasks.split("\n").map((l) => l.trim()).filter(Boolean), [tasks]);
|
|
668
|
-
const inputActive = view === "chat" &&
|
|
743
|
+
const inputActive = view === "chat" &&
|
|
744
|
+
!pendingApproval &&
|
|
745
|
+
!pendingFollowup &&
|
|
746
|
+
!pendingHookTrust &&
|
|
747
|
+
!modelPickerOpen &&
|
|
748
|
+
!resumableSessions;
|
|
669
749
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: rows, children: (row) => _jsx(RowView, { row: row }, row.id) }, staticKey), view === "login" ? (_jsx(LoginSection, { onLogin: handleLogin })) : (_jsxs(Box, { flexDirection: "column", children: [updateInfo?.updateAvailable && updateInfo.latest && (_jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "round", borderColor: COLORS.warning, paddingX: 2, alignSelf: "flex-start", children: [_jsxs(Text, { color: COLORS.warning, bold: true, children: ["\u2191 Update available: v", updateInfo.current, " \u2192 v", updateInfo.latest] }), _jsxs(Text, { children: ["Run ", _jsx(Text, { color: COLORS.accent, children: "orbcode update" }), " to install the latest version, then relaunch."] })] })), streamingReasoning && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: COLORS.thinking, italic: true, children: "\u2726 Thinking\u2026" }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, italic: true, children: tail(streamingReasoning, expandReasoningRef.current ? 30 : 3) }) })] })), streamingText && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: COLORS.primary, children: "\u25CF " }), streamingText] }) })), runningTool && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: COLORS.warning, children: [formatToolName(runningTool.name), " ", _jsx(Text, { dimColor: true, children: runningTool.summary })] }) })), taskLines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: "Tasks" }), taskLines.slice(0, 10).map((line, i) => (_jsx(Text, { dimColor: /^[-*]\s*\[x\]/i.test(line), children: line
|
|
670
750
|
.replace(/^[-*]\s*\[x\]/i, " ■")
|
|
671
751
|
.replace(/^[-*]\s*\[-\]/, " ◧")
|
|
672
752
|
.replace(/^[-*]\s*\[ \]/, " □") }, i))), taskLines.length > 10 && _jsxs(Text, { dimColor: true, children: [" \u2026 ", taskLines.length - 10, " more"] })] })), modelPickerOpen && (_jsx(Box, { marginTop: 1, children: _jsx(ModelPicker, { currentId: settings.model, onSelect: (modelId) => {
|
|
673
753
|
setModelPickerOpen(false);
|
|
674
754
|
switchModel(modelId);
|
|
675
|
-
}, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingApproval && (_jsx(Box, { marginTop: 1, children: _jsx(ApprovalPrompt, { request: pendingApproval.request, onDecision: (decision) => {
|
|
755
|
+
}, onCancel: () => setModelPickerOpen(false) }) })), resumableSessions && (_jsx(Box, { marginTop: 1, children: _jsx(SessionPicker, { sessions: resumableSessions, onSelect: handleResume, onCancel: () => setResumableSessions(null) }) })), pendingHookTrust && (_jsx(Box, { marginTop: 1, children: _jsx(HookTrustPrompt, { cwd: process.cwd(), commands: pendingHookTrust.commands, onDecision: resolveHookTrust }) })), pendingApproval && (_jsx(Box, { marginTop: 1, children: _jsx(ApprovalPrompt, { request: pendingApproval.request, onDecision: (decision) => {
|
|
676
756
|
pendingApproval.resolve(decision);
|
|
677
757
|
setPendingApproval(null);
|
|
678
758
|
} }) })), pendingFollowup && (_jsx(Box, { marginTop: 1, children: _jsx(FollowupPrompt, { question: pendingFollowup.question, suggestions: pendingFollowup.suggestions, onAnswer: (answer) => {
|
|
679
759
|
pushRow({ kind: "user", text: answer });
|
|
680
760
|
pendingFollowup.resolve(answer);
|
|
681
761
|
setPendingFollowup(null);
|
|
682
|
-
} }) })), busy && !pendingApproval && !pendingFollowup && !streamingText && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: busyLabel }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [queuedMessages.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginBottom: 1, children: [_jsxs(Text, { dimColor: true, bold: true, children: ["Queue (", queuedMessages.length, ")"] }), queuedMessages.slice(0, 5).map((msg, i) => (_jsxs(Text, { dimColor: true, children: [i + 1, ". ", truncateForQueue(msg).replace(/\n/g, "↵")] }, i))), queuedMessages.length > 5 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", queuedMessages.length - 5, " more"] }))] })), _jsx(InputBox, { active: inputActive, slashCommands: SLASH_COMMANDS, onSubmit: handleSubmit }), _jsx(StatusBar, { modelId: settings.model, contextTokens: contextTokens, totalCost: totalCost, state: busy ? busyLabel : "", approvalMode: approvalMode, busy: busy, title: sessionTitle, plan: usage?.plan, usagePercentage: usage?.usagePercentage, tieredUsage: usage?.tieredUsage })] })] }))] }));
|
|
762
|
+
} }) })), busy && !pendingApproval && !pendingFollowup && !pendingHookTrust && !streamingText && (_jsx(Box, { marginTop: 1, children: _jsx(Spinner, { label: busyLabel }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [queuedMessages.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 1, marginBottom: 1, children: [_jsxs(Text, { dimColor: true, bold: true, children: ["Queue (", queuedMessages.length, ")"] }), queuedMessages.slice(0, 5).map((msg, i) => (_jsxs(Text, { dimColor: true, children: [i + 1, ". ", truncateForQueue(msg).replace(/\n/g, "↵")] }, i))), queuedMessages.length > 5 && (_jsxs(Text, { dimColor: true, children: [" \u2026 ", queuedMessages.length - 5, " more"] }))] })), _jsx(InputBox, { active: inputActive, slashCommands: SLASH_COMMANDS, onSubmit: handleSubmit }), _jsx(StatusBar, { modelId: settings.model, contextTokens: contextTokens, totalCost: totalCost, state: busy ? busyLabel : "", approvalMode: approvalMode, busy: busy, title: sessionTitle, plan: usage?.plan, usagePercentage: usage?.usagePercentage, tieredUsage: usage?.tieredUsage })] })] }))] }));
|
|
683
763
|
}
|
|
684
764
|
function tail(text, lines) {
|
|
685
765
|
const all = text.split("\n").filter((l) => l.trim());
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import { COLORS } from "../../branding.js";
|
|
4
|
+
const MAX_SHOWN = 8;
|
|
5
|
+
/**
|
|
6
|
+
* Shown at startup when the current project defines hooks that haven't been
|
|
7
|
+
* trusted yet. Project hooks run arbitrary shell commands, so they stay
|
|
8
|
+
* disabled until the user explicitly approves them here.
|
|
9
|
+
*/
|
|
10
|
+
export function HookTrustPrompt({ cwd, commands, onDecision }) {
|
|
11
|
+
useInput((input, key) => {
|
|
12
|
+
const lower = input.toLowerCase();
|
|
13
|
+
if (lower === "y")
|
|
14
|
+
onDecision(true);
|
|
15
|
+
else if (lower === "n" || key.escape || key.return)
|
|
16
|
+
onDecision(false);
|
|
17
|
+
});
|
|
18
|
+
const shown = commands.slice(0, MAX_SHOWN);
|
|
19
|
+
const extra = commands.length - shown.length;
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: COLORS.error, paddingX: 1, children: [_jsxs(Text, { bold: true, color: COLORS.error, children: ["\u26A0 This project defines ", commands.length, " hook command", commands.length === 1 ? "" : "s"] }), _jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: [cwd, "/.orbcode/settings.json"] }), _jsx(Text, { children: "Project hooks run these shell commands automatically during the session:" }), shown.map((command, i) => (_jsxs(Text, { color: COLORS.warning, children: [" • ", command.length > 100 ? command.slice(0, 99) + "…" : command] }, i))), extra > 0 && _jsxs(Text, { dimColor: true, children: [" \u2026 ", extra, " more"] })] }), _jsx(Text, { dimColor: true, children: "Only trust hooks from a repository you trust. (y) trust & enable \u00B7 (n or Enter) keep disabled" })] }));
|
|
21
|
+
}
|
|
@@ -33,25 +33,18 @@ function truncate(text, max) {
|
|
|
33
33
|
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* Formats the 5hr window as "5hr limit XX% · resets <relative>".
|
|
37
|
+
* Returns null when tiered usage is unavailable so the line can be hidden.
|
|
38
38
|
*/
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
];
|
|
45
|
-
const [label, pct, resetsAt] = windows.reduce((best, cur) => cur[1] >= best[1] ? cur : best);
|
|
46
|
-
return `${label} ${Math.round(pct)}% · resets ${formatRelativeTime(resetsAt)}`;
|
|
39
|
+
function fiveHourSummary(tu) {
|
|
40
|
+
if (!tu?.fiveHour)
|
|
41
|
+
return null;
|
|
42
|
+
const { percentage, resetsAt } = tu.fiveHour;
|
|
43
|
+
return `5hr limit ${Math.round(percentage)}% · resets ${formatRelativeTime(resetsAt)}`;
|
|
47
44
|
}
|
|
48
|
-
export function StatusBar({ modelId, contextTokens, totalCost, state, approvalMode, busy, title, plan, usagePercentage, tieredUsage, }) {
|
|
45
|
+
export function StatusBar({ modelId, contextTokens, totalCost: _totalCost, state, approvalMode, busy, title, plan: _plan, usagePercentage: _usagePercentage, tieredUsage, }) {
|
|
49
46
|
const model = getModel(modelId);
|
|
50
47
|
const contextPct = Math.min(100, Math.round((contextTokens / model.contextWindow) * 100));
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
: typeof usagePercentage === "number"
|
|
54
|
-
? `${Math.round(usagePercentage)}% used`
|
|
55
|
-
: null;
|
|
56
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: approvalMode === "ask" ? undefined : COLORS.warning, children: MODE_LABELS[approvalMode] }), " (shift+tab to cycle)", busy && " · esc to interrupt", state ? _jsxs(Text, { color: COLORS.thinking, children: [" \u00B7 ", state] }) : null] }), _jsxs(Text, { dimColor: true, children: [title ? `${truncate(title, 32)} · ` : "", model.name, " \u00B7 ctx ", contextTokens.toLocaleString(), " (", contextPct, "%)", model.free ? " · free" : ` · $${totalCost.toFixed(4)}`] })] }), (plan || constrainedLine) && (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { dimColor: true, children: [plan && constrainedLine ? " · " : "", constrainedLine] }) }))] }));
|
|
48
|
+
const fiveHourLine = fiveHourSummary(tieredUsage);
|
|
49
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: approvalMode === "ask" ? undefined : COLORS.warning, children: MODE_LABELS[approvalMode] }), " (shift+tab to cycle)", busy && " · esc to interrupt", state ? _jsxs(Text, { color: COLORS.thinking, children: [" \u00B7 ", state] }) : null] }), _jsxs(Text, { dimColor: true, children: [title ? `${truncate(title, 32)} · ` : "", model.name, " \u00B7 ctx ", contextTokens.toLocaleString(), " (", contextPct, "%)"] })] }), fiveHourLine && (_jsx(Box, { justifyContent: "flex-end", children: _jsx(Text, { dimColor: true, children: fiveHourLine }) }))] }));
|
|
57
50
|
}
|