@ruimte/agents-react 0.0.0 → 0.7.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/LICENSE +110 -0
- package/README.md +142 -1
- package/dist/agents/AccountDot.d.ts +4 -0
- package/dist/agents/AccountDot.js +8 -0
- package/dist/agents/AgentIcon.d.ts +6 -0
- package/dist/agents/AgentIcon.js +20 -0
- package/dist/agents/ProviderLogo.d.ts +8 -0
- package/dist/agents/ProviderLogo.js +8 -0
- package/dist/agents/account-limits.d.ts +7 -0
- package/dist/agents/account-limits.js +26 -0
- package/dist/agents/accounts.d.ts +22 -0
- package/dist/agents/accounts.js +103 -0
- package/dist/agents/model-name.d.ts +6 -0
- package/dist/agents/model-name.js +44 -0
- package/dist/agents/provider-paths.d.ts +2 -0
- package/dist/agents/provider-paths.js +8 -0
- package/dist/agents/status-look.d.ts +13 -0
- package/dist/agents/status-look.js +22 -0
- package/dist/chat/account-choice.d.ts +11 -0
- package/dist/chat/account-choice.js +30 -0
- package/dist/chat/actions.d.ts +6 -0
- package/dist/chat/actions.js +64 -0
- package/dist/chat/activity.d.ts +5 -0
- package/dist/chat/activity.js +4 -0
- package/dist/chat/attachments.d.ts +23 -0
- package/dist/chat/attachments.js +70 -0
- package/dist/chat/bookmarks.d.ts +14 -0
- package/dist/chat/bookmarks.js +73 -0
- package/dist/chat/chat-client.d.ts +53 -0
- package/dist/chat/chat-client.js +232 -0
- package/dist/chat/chat-references.d.ts +5 -0
- package/dist/chat/chat-references.js +5 -0
- package/dist/chat/drafts.d.ts +29 -0
- package/dist/chat/drafts.js +104 -0
- package/dist/chat/forks.d.ts +2 -0
- package/dist/chat/forks.js +26 -0
- package/dist/chat/guards.d.ts +12 -0
- package/dist/chat/guards.js +63 -0
- package/dist/chat/logic/answers.d.ts +3 -0
- package/dist/chat/logic/answers.js +14 -0
- package/dist/chat/logic/bookmarks.d.ts +5 -0
- package/dist/chat/logic/bookmarks.js +31 -0
- package/dist/chat/logic/context-usage.d.ts +12 -0
- package/dist/chat/logic/context-usage.js +20 -0
- package/dist/chat/logic/fork.d.ts +44 -0
- package/dist/chat/logic/fork.js +138 -0
- package/dist/chat/logic/handback.d.ts +2 -0
- package/dist/chat/logic/handback.js +8 -0
- package/dist/chat/logic/json.d.ts +1 -0
- package/dist/chat/logic/json.js +2 -0
- package/dist/chat/logic/limit.d.ts +7 -0
- package/dist/chat/logic/limit.js +34 -0
- package/dist/chat/logic/resume-compaction.d.ts +14 -0
- package/dist/chat/logic/resume-compaction.js +47 -0
- package/dist/chat/logic/scrubber.d.ts +50 -0
- package/dist/chat/logic/scrubber.js +216 -0
- package/dist/chat/logic/timeline-copy.d.ts +4 -0
- package/dist/chat/logic/timeline-copy.js +51 -0
- package/dist/chat/logic/timeline-target.d.ts +12 -0
- package/dist/chat/logic/timeline-target.js +34 -0
- package/dist/chat/logic/timeline.d.ts +110 -0
- package/dist/chat/logic/timeline.js +339 -0
- package/dist/chat/logic/tool-catalog.d.ts +10 -0
- package/dist/chat/logic/tool-catalog.js +40 -0
- package/dist/chat/logic/tools.d.ts +15 -0
- package/dist/chat/logic/tools.js +79 -0
- package/dist/chat/logic/workflow.d.ts +14 -0
- package/dist/chat/logic/workflow.js +47 -0
- package/dist/chat/mentions.d.ts +46 -0
- package/dist/chat/mentions.js +128 -0
- package/dist/chat/persisted-json.d.ts +5 -0
- package/dist/chat/persisted-json.js +24 -0
- package/dist/chat/preferences.d.ts +23 -0
- package/dist/chat/preferences.js +110 -0
- package/dist/chat/quote.d.ts +2 -0
- package/dist/chat/quote.js +25 -0
- package/dist/chat/recent-messages.d.ts +17 -0
- package/dist/chat/recent-messages.js +28 -0
- package/dist/chat/resume-compaction-dismissals.d.ts +3 -0
- package/dist/chat/resume-compaction-dismissals.js +14 -0
- package/dist/chat/runtime-modes.d.ts +4 -0
- package/dist/chat/runtime-modes.js +11 -0
- package/dist/chat/stash.d.ts +24 -0
- package/dist/chat/stash.js +88 -0
- package/dist/chat/subagent-conversation.d.ts +38 -0
- package/dist/chat/subagent-conversation.js +150 -0
- package/dist/chat/subagent-list.d.ts +16 -0
- package/dist/chat/subagent-list.js +76 -0
- package/dist/chat/subagent-support.d.ts +6 -0
- package/dist/chat/subagent-support.js +14 -0
- package/dist/chat/subagent-view.d.ts +33 -0
- package/dist/chat/subagent-view.js +65 -0
- package/dist/chat/thumbnails.d.ts +10 -0
- package/dist/chat/thumbnails.js +87 -0
- package/dist/chat/timeline-scroll.d.ts +16 -0
- package/dist/chat/timeline-scroll.js +100 -0
- package/dist/chat/ui/AccountPill.d.ts +3 -0
- package/dist/chat/ui/AccountPill.js +24 -0
- package/dist/chat/ui/BookmarkMarker.d.ts +8 -0
- package/dist/chat/ui/BookmarkMarker.js +59 -0
- package/dist/chat/ui/BookmarkSubmenu.d.ts +3 -0
- package/dist/chat/ui/BookmarkSubmenu.js +44 -0
- package/dist/chat/ui/ChatActivity.d.ts +3 -0
- package/dist/chat/ui/ChatActivity.js +87 -0
- package/dist/chat/ui/ChatReferenceChip.d.ts +4 -0
- package/dist/chat/ui/ChatReferenceChip.js +17 -0
- package/dist/chat/ui/CodeBlock.d.ts +4 -0
- package/dist/chat/ui/CodeBlock.js +130 -0
- package/dist/chat/ui/Composer.d.ts +14 -0
- package/dist/chat/ui/Composer.js +821 -0
- package/dist/chat/ui/ComposerInput.d.ts +28 -0
- package/dist/chat/ui/ComposerInput.js +118 -0
- package/dist/chat/ui/DiffPool.d.ts +5 -0
- package/dist/chat/ui/DiffPool.js +26 -0
- package/dist/chat/ui/EditDiff.d.ts +4 -0
- package/dist/chat/ui/EditDiff.js +21 -0
- package/dist/chat/ui/FadingWords.d.ts +3 -0
- package/dist/chat/ui/FadingWords.js +5 -0
- package/dist/chat/ui/ImageView.d.ts +6 -0
- package/dist/chat/ui/ImageView.js +85 -0
- package/dist/chat/ui/LimitState.d.ts +8 -0
- package/dist/chat/ui/LimitState.js +117 -0
- package/dist/chat/ui/Markdown.d.ts +18 -0
- package/dist/chat/ui/Markdown.js +158 -0
- package/dist/chat/ui/MessageActions.d.ts +9 -0
- package/dist/chat/ui/MessageActions.js +56 -0
- package/dist/chat/ui/Pickers.d.ts +15 -0
- package/dist/chat/ui/Pickers.js +110 -0
- package/dist/chat/ui/PromptComposer.d.ts +13 -0
- package/dist/chat/ui/PromptComposer.js +48 -0
- package/dist/chat/ui/QuoteButton.d.ts +7 -0
- package/dist/chat/ui/QuoteButton.js +76 -0
- package/dist/chat/ui/ResumeCompactionDock.d.ts +5 -0
- package/dist/chat/ui/ResumeCompactionDock.js +15 -0
- package/dist/chat/ui/RunSettings.d.ts +25 -0
- package/dist/chat/ui/RunSettings.js +148 -0
- package/dist/chat/ui/Scrubber.d.ts +17 -0
- package/dist/chat/ui/Scrubber.js +121 -0
- package/dist/chat/ui/SubagentControls.d.ts +11 -0
- package/dist/chat/ui/SubagentControls.js +37 -0
- package/dist/chat/ui/SubagentStopButton.d.ts +7 -0
- package/dist/chat/ui/SubagentStopButton.js +36 -0
- package/dist/chat/ui/SubagentTimeline.d.ts +4 -0
- package/dist/chat/ui/SubagentTimeline.js +92 -0
- package/dist/chat/ui/Timeline.d.ts +5 -0
- package/dist/chat/ui/Timeline.js +318 -0
- package/dist/chat/ui/TimelineMenu.d.ts +12 -0
- package/dist/chat/ui/TimelineMenu.js +61 -0
- package/dist/chat/ui/UnifiedDiff.d.ts +8 -0
- package/dist/chat/ui/UnifiedDiff.js +51 -0
- package/dist/chat/ui/UploadThumb.d.ts +4 -0
- package/dist/chat/ui/UploadThumb.js +11 -0
- package/dist/chat/ui/chips.d.ts +4 -0
- package/dist/chat/ui/chips.js +11 -0
- package/dist/chat/ui/code-lines.d.ts +18 -0
- package/dist/chat/ui/code-lines.js +46 -0
- package/dist/chat/ui/code-streaming.d.ts +1 -0
- package/dist/chat/ui/code-streaming.js +3 -0
- package/dist/chat/ui/code-theme.d.ts +3 -0
- package/dist/chat/ui/code-theme.js +10 -0
- package/dist/chat/ui/composer/chips.d.ts +6 -0
- package/dist/chat/ui/composer/chips.js +29 -0
- package/dist/chat/ui/composer/editor.d.ts +11 -0
- package/dist/chat/ui/composer/editor.js +94 -0
- package/dist/chat/ui/composer/keys.d.ts +30 -0
- package/dist/chat/ui/composer/keys.js +98 -0
- package/dist/chat/ui/diff-theme.d.ts +2 -0
- package/dist/chat/ui/diff-theme.js +20 -0
- package/dist/chat/ui/file-links.d.ts +5 -0
- package/dist/chat/ui/file-links.js +18 -0
- package/dist/chat/ui/find-reveal.d.ts +7 -0
- package/dist/chat/ui/find-reveal.js +13 -0
- package/dist/chat/ui/icons.d.ts +4 -0
- package/dist/chat/ui/icons.js +20 -0
- package/dist/chat/ui/markdown-blocks.d.ts +6 -0
- package/dist/chat/ui/markdown-blocks.js +101 -0
- package/dist/chat/ui/quote-selection.d.ts +3 -0
- package/dist/chat/ui/quote-selection.js +44 -0
- package/dist/chat/ui/rehype-chips.d.ts +23 -0
- package/dist/chat/ui/rehype-chips.js +28 -0
- package/dist/chat/ui/rehype-fade.d.ts +23 -0
- package/dist/chat/ui/rehype-fade.js +30 -0
- package/dist/chat/ui/remark-html-as-text.d.ts +8 -0
- package/dist/chat/ui/remark-html-as-text.js +24 -0
- package/dist/chat/ui/reveal.d.ts +13 -0
- package/dist/chat/ui/reveal.js +112 -0
- package/dist/chat/ui/rows/ForksRow.d.ts +4 -0
- package/dist/chat/ui/rows/ForksRow.js +40 -0
- package/dist/chat/ui/rows/MessageRows.d.ts +36 -0
- package/dist/chat/ui/rows/MessageRows.js +190 -0
- package/dist/chat/ui/rows/Rows.d.ts +12 -0
- package/dist/chat/ui/rows/Rows.js +49 -0
- package/dist/chat/ui/rows/SubagentRow.d.ts +15 -0
- package/dist/chat/ui/rows/SubagentRow.js +98 -0
- package/dist/chat/ui/rows/WorkRows.d.ts +46 -0
- package/dist/chat/ui/rows/WorkRows.js +143 -0
- package/dist/chat/ui/rows/WorkflowRow.d.ts +7 -0
- package/dist/chat/ui/rows/WorkflowRow.js +37 -0
- package/dist/chat/ui/rows/row-rhythm.d.ts +3 -0
- package/dist/chat/ui/rows/row-rhythm.js +14 -0
- package/dist/chat/ui/useToggleSet.d.ts +6 -0
- package/dist/chat/ui/useToggleSet.js +25 -0
- package/dist/host.d.ts +167 -0
- package/dist/host.js +41 -0
- package/dist/lazy.d.ts +5 -0
- package/dist/lazy.js +35 -0
- package/dist/locales/en/agent-chat.json +417 -0
- package/dist/locales/en/agent-prompts.json +42 -0
- package/dist/locales/en/agent-providers.json +131 -0
- package/dist/locales/en/agent-usage.json +131 -0
- package/dist/locales/nl/agent-chat.json +417 -0
- package/dist/locales/nl/agent-prompts.json +42 -0
- package/dist/locales/nl/agent-providers.json +131 -0
- package/dist/locales/nl/agent-usage.json +131 -0
- package/dist/locales.d.ts +5 -0
- package/dist/locales.js +18 -0
- package/dist/mounted-registry.d.ts +7 -0
- package/dist/mounted-registry.js +39 -0
- package/dist/port-transport.d.ts +6 -0
- package/dist/port-transport.js +120 -0
- package/dist/prompts/logic/focus.d.ts +1 -0
- package/dist/prompts/logic/focus.js +17 -0
- package/dist/prompts/logic/keys.d.ts +42 -0
- package/dist/prompts/logic/keys.js +84 -0
- package/dist/prompts/logic/prompts.d.ts +34 -0
- package/dist/prompts/logic/prompts.fixtures.d.ts +5 -0
- package/dist/prompts/logic/prompts.fixtures.js +73 -0
- package/dist/prompts/logic/prompts.js +22 -0
- package/dist/prompts/logic/subjects.d.ts +49 -0
- package/dist/prompts/logic/subjects.js +56 -0
- package/dist/prompts/logic/useFocusAfterAnswer.d.ts +6 -0
- package/dist/prompts/logic/useFocusAfterAnswer.js +28 -0
- package/dist/prompts/logic/usePromptSession.d.ts +24 -0
- package/dist/prompts/logic/usePromptSession.js +87 -0
- package/dist/prompts/ui/ApprovalBody.d.ts +13 -0
- package/dist/prompts/ui/ApprovalBody.js +35 -0
- package/dist/prompts/ui/PromptActions.d.ts +26 -0
- package/dist/prompts/ui/PromptActions.js +19 -0
- package/dist/prompts/ui/PromptCard.d.ts +16 -0
- package/dist/prompts/ui/PromptCard.js +50 -0
- package/dist/prompts/ui/PromptView.d.ts +2 -0
- package/dist/prompts/ui/PromptView.js +47 -0
- package/dist/prompts/ui/QuestionBody.d.ts +9 -0
- package/dist/prompts/ui/QuestionBody.js +87 -0
- package/dist/providers/AccountColors.d.ts +5 -0
- package/dist/providers/AccountColors.js +8 -0
- package/dist/providers/AccountDetail.d.ts +12 -0
- package/dist/providers/AccountDetail.js +76 -0
- package/dist/providers/AccountVariables.d.ts +8 -0
- package/dist/providers/AccountVariables.js +48 -0
- package/dist/providers/AddAccountForm.d.ts +10 -0
- package/dist/providers/AddAccountForm.js +58 -0
- package/dist/providers/CliDetail.d.ts +11 -0
- package/dist/providers/CliDetail.js +62 -0
- package/dist/providers/ProvidersPane.d.ts +8 -0
- package/dist/providers/ProvidersPane.js +90 -0
- package/dist/providers/account-actions.d.ts +7 -0
- package/dist/providers/account-actions.js +53 -0
- package/dist/providers/parts.d.ts +9 -0
- package/dist/providers/parts.js +18 -0
- package/dist/providers/provider-abilities.d.ts +2 -0
- package/dist/providers/provider-abilities.js +7 -0
- package/dist/providers/variables.d.ts +13 -0
- package/dist/providers/variables.js +53 -0
- package/dist/scope.d.ts +11 -0
- package/dist/scope.js +10 -0
- package/dist/settings/sections.d.ts +13 -0
- package/dist/settings/sections.js +30 -0
- package/dist/state/chats.d.ts +32 -0
- package/dist/state/chats.js +124 -0
- package/dist/state/provider-accounts.d.ts +17 -0
- package/dist/state/provider-accounts.js +63 -0
- package/dist/state/providers.d.ts +17 -0
- package/dist/state/providers.js +29 -0
- package/dist/state/usage.d.ts +41 -0
- package/dist/state/usage.js +100 -0
- package/dist/theme.css +186 -0
- package/dist/transport.d.ts +24 -0
- package/dist/transport.js +16 -0
- package/dist/ui/PlainTextarea.d.ts +2 -0
- package/dist/ui/PlainTextarea.js +5 -0
- package/dist/usage/LimitsList.d.ts +18 -0
- package/dist/usage/LimitsList.js +69 -0
- package/dist/usage/UsageBreakdown.d.ts +9 -0
- package/dist/usage/UsageBreakdown.js +69 -0
- package/dist/usage/UsageChart.d.ts +10 -0
- package/dist/usage/UsageChart.js +49 -0
- package/dist/usage/UsageDialog.d.ts +6 -0
- package/dist/usage/UsageDialog.js +15 -0
- package/dist/usage/UsageLimits.d.ts +1 -0
- package/dist/usage/UsageLimits.js +62 -0
- package/dist/usage/UsageLimitsCard.d.ts +4 -0
- package/dist/usage/UsageLimitsCard.js +27 -0
- package/dist/usage/UsagePage.d.ts +7 -0
- package/dist/usage/UsagePage.js +161 -0
- package/dist/usage/UsagePane.d.ts +3 -0
- package/dist/usage/UsagePane.js +24 -0
- package/dist/usage/UsageSummary.d.ts +12 -0
- package/dist/usage/UsageSummary.js +15 -0
- package/dist/usage/UsageTiles.d.ts +7 -0
- package/dist/usage/UsageTiles.js +19 -0
- package/dist/usage/format.d.ts +14 -0
- package/dist/usage/format.js +50 -0
- package/dist/usage/limit-groups.d.ts +22 -0
- package/dist/usage/limit-groups.js +78 -0
- package/dist/usage/limits.d.ts +4 -0
- package/dist/usage/limits.js +58 -0
- package/dist/usage/money.d.ts +1 -0
- package/dist/usage/money.js +13 -0
- package/dist/usage/summary.d.ts +34 -0
- package/dist/usage/summary.js +90 -0
- package/package.json +639 -2
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { Dialog } from '@base-ui-components/react/dialog';
|
|
6
|
+
import { ChartNoAxesColumn, RefreshCw } from 'lucide-react';
|
|
7
|
+
import { USAGE_PROVIDERS } from '@ruimte/agent-contracts';
|
|
8
|
+
import { Button } from '@ruimte/ui/Button';
|
|
9
|
+
import { FORM_ERROR } from '@ruimte/ui/classes';
|
|
10
|
+
import { CloseButton } from '@ruimte/ui/CloseButton';
|
|
11
|
+
import { Segmented, Skeleton } from '@ruimte/ui/controls';
|
|
12
|
+
import { EmptyState } from '@ruimte/ui/EmptyState';
|
|
13
|
+
import { Icon } from '@ruimte/ui/Icon';
|
|
14
|
+
import { Select } from '@ruimte/ui/Select';
|
|
15
|
+
import { Tooltip } from '@ruimte/ui/Tooltip';
|
|
16
|
+
import { AccountDot } from '../agents/AccountDot';
|
|
17
|
+
import { chatHost } from '../host';
|
|
18
|
+
import { useChatScope } from '../scope';
|
|
19
|
+
import { useProvidersStore } from '../state/providers';
|
|
20
|
+
import { askedKey, summaryPayload, USAGE_PERIODS, useUsage, useUsageStore } from '../state/usage';
|
|
21
|
+
import { formatClock, formatCount, formatDate, formatTokens, PROVIDER_LABELS } from './format';
|
|
22
|
+
import { useMoney } from './money';
|
|
23
|
+
import { deriveUsage, labelEveryFor } from './summary';
|
|
24
|
+
import { UsageBreakdown } from './UsageBreakdown';
|
|
25
|
+
import { UsageChart } from './UsageChart';
|
|
26
|
+
import { UsageLimits } from './UsageLimits';
|
|
27
|
+
import { UsageSummary } from './UsageSummary';
|
|
28
|
+
import { UsageTiles } from './UsageTiles';
|
|
29
|
+
const METRICS = ['cost', 'tokens'];
|
|
30
|
+
/* The picker's value for every account, which no account id can be: an id starts with a letter. */
|
|
31
|
+
const ALL_ACCOUNTS = '*';
|
|
32
|
+
/* Over the whole popup rather than the body under the header, so an empty state stands in the middle of the dialog. */
|
|
33
|
+
const DIALOG_CENTER = 'pointer-events-none absolute inset-0';
|
|
34
|
+
/* Whether the scope's host answers right now, redrawn as its link comes and goes. */
|
|
35
|
+
const useAnswering = (transport) => useSyncExternalStore(useCallback((onChange) => transport.subscribeStatus(onChange), [transport]), () => transport.status === 'open');
|
|
36
|
+
/*
|
|
37
|
+
* Which agent CLIs the host has, under an empty usage page: no usage is what a host without any
|
|
38
|
+
* agent looks like too, and the way to fix that is the agent settings.
|
|
39
|
+
*/
|
|
40
|
+
function HostAgents() {
|
|
41
|
+
const { t } = useTranslation('agent-usage');
|
|
42
|
+
const { id } = useChatScope();
|
|
43
|
+
const row = useProvidersStore((s) => s.byScope[id]);
|
|
44
|
+
const installed = useMemo(() => (row?.providers ?? []).filter((provider) => provider.installed), [row]);
|
|
45
|
+
if (!row?.loaded) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return (_jsxs("div", { className: "flex flex-col items-center gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: installed.length === 0 ? t('agents.none') : t('agents.installed', { list: installed.map((provider) => provider.name).join(', ') }) }), _jsx(Button, { size: "sm", variant: "secondary", onClick: () => chatHost().openSettings('agents'), children: t('agents.settings') })] }));
|
|
49
|
+
}
|
|
50
|
+
/*
|
|
51
|
+
* Asks the host on screen for the period that is up, keeps the answer under the key of the question
|
|
52
|
+
* it answers, and tells the host to keep scanning while the page is here. A late answer to the period
|
|
53
|
+
* before this one never lands, because the key it carries is no longer the one being shown.
|
|
54
|
+
*/
|
|
55
|
+
const useSummary = (scopeId, transport, period, account) => {
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const subscribe = () => void transport.request('usage.subscribe', {}).catch(() => undefined);
|
|
58
|
+
if (transport.status === 'open') {
|
|
59
|
+
subscribe();
|
|
60
|
+
}
|
|
61
|
+
// A host knows its followers per link, so every connection has to be told again, on a reconnect and on a move to another host.
|
|
62
|
+
const off = transport.subscribeStatus((status) => {
|
|
63
|
+
if (status === 'open') {
|
|
64
|
+
subscribe();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return () => {
|
|
68
|
+
off();
|
|
69
|
+
void transport.request('usage.unsubscribe', {}).catch(() => undefined);
|
|
70
|
+
};
|
|
71
|
+
}, [transport]);
|
|
72
|
+
const load = useCallback(() => {
|
|
73
|
+
const payload = summaryPayload(period, account);
|
|
74
|
+
const asked = askedKey(payload);
|
|
75
|
+
useUsageStore.getState().setLoading(scopeId, true);
|
|
76
|
+
transport
|
|
77
|
+
.request('usage.summary', payload)
|
|
78
|
+
.then((summary) => useUsageStore.getState().receive(scopeId, asked, summary))
|
|
79
|
+
.catch(() => useUsageStore.getState().fail(scopeId));
|
|
80
|
+
}, [transport, scopeId, period, account]);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (transport.status === 'open') {
|
|
83
|
+
load();
|
|
84
|
+
}
|
|
85
|
+
// A scan that found something is the sign to ask again; nothing else changes the numbers.
|
|
86
|
+
const offChanged = transport.on('usage.changed', load);
|
|
87
|
+
// Until the other host answers there is nothing to ask, and what is on screen is the host that left.
|
|
88
|
+
const offStatus = transport.subscribeStatus((status) => {
|
|
89
|
+
if (status === 'open') {
|
|
90
|
+
load();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return () => {
|
|
94
|
+
offChanged();
|
|
95
|
+
offStatus();
|
|
96
|
+
};
|
|
97
|
+
}, [transport, load]);
|
|
98
|
+
return load;
|
|
99
|
+
};
|
|
100
|
+
function Provenance() {
|
|
101
|
+
const { t } = useTranslation('agent-usage');
|
|
102
|
+
const summary = useUsage((s) => s.summary);
|
|
103
|
+
const currency = useUsage((s) => s.currency);
|
|
104
|
+
const failed = useUsage((s) => s.failed);
|
|
105
|
+
if (failed) {
|
|
106
|
+
return (_jsx("p", { className: `${FORM_ERROR} mt-auto text-center`, role: "alert", children: t('provenance.failed') }));
|
|
107
|
+
}
|
|
108
|
+
if (summary === null) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const prices = summary.pricing.fetchedAt === null ? t('provenance.bundledPrices') : t('provenance.fetchedPrices', { date: formatDate(summary.pricing.fetchedAt) });
|
|
112
|
+
// The rate is named only when it is being used, so a page in dollars says nothing about euros.
|
|
113
|
+
const rate = currency === 'USD' || summary.rate === null ? null : t('provenance.rate', { currency: summary.rate.currency, date: summary.rate.date });
|
|
114
|
+
return (_jsxs("p", { className: "mt-auto text-center text-xs text-text-muted", children: [t('provenance.scanned', { count: summary.scan.files, time: formatClock(summary.scan.at), files: formatCount(summary.scan.files) }), " \u00B7 ", prices, rate !== null && ` · ${rate}`] }));
|
|
115
|
+
}
|
|
116
|
+
function LoadingBody() {
|
|
117
|
+
return (_jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("div", { className: "grid gap-6 lg:grid-cols-[18rem_1fr]", children: [_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx(Skeleton, { className: "h-10 w-40" }), _jsx(Skeleton, { className: "w-56" }), _jsx(Skeleton, { className: "w-48" })] }), _jsx(Skeleton, { className: "h-56 w-full" })] }), _jsx("div", { className: "grid grid-cols-2 gap-2 md:grid-cols-6", children: [0, 1, 2, 3, 4, 5].map((tile) => (_jsx(Skeleton, { className: "h-16 w-full rounded-xl" }, tile))) })] }));
|
|
118
|
+
}
|
|
119
|
+
/*
|
|
120
|
+
* Every account of the host, once a CLI has more than one: picking one narrows the page to what that
|
|
121
|
+
* account spent. The chart keeps its split per CLI, since an account's color is a dot and not a series.
|
|
122
|
+
*/
|
|
123
|
+
function AccountPicker({ accounts, value, onChange }) {
|
|
124
|
+
const { t } = useTranslation('agent-usage');
|
|
125
|
+
if (!USAGE_PROVIDERS.some((kind) => accounts.filter((account) => account.kind === kind).length > 1)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return (_jsx(Select, { value: value ?? ALL_ACCOUNTS, items: [
|
|
129
|
+
{ value: ALL_ACCOUNTS, label: t('dialog.accounts.all') },
|
|
130
|
+
...accounts.map((account) => ({
|
|
131
|
+
value: account.id,
|
|
132
|
+
label: account.label,
|
|
133
|
+
icon: _jsx(AccountDot, { color: account.color }),
|
|
134
|
+
description: account.label === PROVIDER_LABELS[account.kind] ? undefined : PROVIDER_LABELS[account.kind]
|
|
135
|
+
}))
|
|
136
|
+
], onValueChange: (id) => onChange(id === ALL_ACCOUNTS ? null : id), label: t('dialog.accounts.label'), align: "end" }));
|
|
137
|
+
}
|
|
138
|
+
/*
|
|
139
|
+
* What the CLIs of one host cost and how much of their plans is left: the host of the scope this is
|
|
140
|
+
* rendered in. It is the body of a dialog (`UsageDialog`); the page remounts per host, so nothing of
|
|
141
|
+
* one outlives a switch to another.
|
|
142
|
+
*/
|
|
143
|
+
export function UsagePage({ pickers, notice }) {
|
|
144
|
+
const { t } = useTranslation('agent-usage');
|
|
145
|
+
const scope = useChatScope();
|
|
146
|
+
const period = useUsage((s) => s.period);
|
|
147
|
+
const metric = useUsage((s) => s.metric);
|
|
148
|
+
const summary = useUsage((s) => s.summary);
|
|
149
|
+
const asked = useUsage((s) => s.asked);
|
|
150
|
+
const loading = useUsage((s) => s.loading);
|
|
151
|
+
// Per host, since account ids are a host's own.
|
|
152
|
+
const [account, setAccount] = useState(null);
|
|
153
|
+
const reload = useSummary(scope.id, scope.transport, period, account);
|
|
154
|
+
const money = useMoney();
|
|
155
|
+
const answering = useAnswering(scope.transport);
|
|
156
|
+
const shown = summary !== null && asked === askedKey(summaryPayload(period, account)) ? summary : null;
|
|
157
|
+
const derived = shown === null ? null : deriveUsage(shown, metric);
|
|
158
|
+
const value = metric === 'cost' ? money : formatTokens;
|
|
159
|
+
const noRoots = shown !== null && shown.roots.every((root) => root.status === 'missing');
|
|
160
|
+
return (_jsxs("div", { className: "flex min-h-0 grow flex-col", children: [_jsxs("header", { className: "flex shrink-0 flex-wrap items-center gap-3 border-b border-border py-3 pr-3 pl-6 max-[960px]:pl-4", children: [_jsx(Dialog.Title, { className: "text-base font-semibold text-text", children: t('dialog.title') }), _jsxs("div", { className: "ml-auto flex flex-wrap items-center gap-2", children: [pickers, _jsx(AccountPicker, { accounts: summary?.accounts ?? [], value: account, onChange: setAccount }), _jsx(Segmented, { value: period, options: USAGE_PERIODS.map((entry) => ({ id: entry.id, label: t(`dialog.periods.${entry.id}`) })), onChange: (id) => useUsageStore.getState().setPeriod(id), label: t('dialog.period') }), _jsx(Segmented, { value: metric, options: METRICS.map((id) => ({ id, label: t(`dialog.metrics.${id}`) })), onChange: (id) => useUsageStore.getState().setMetric(id), label: t('dialog.metric') }), _jsx(Tooltip, { label: t('dialog.rescan'), name: true, children: _jsx("button", { className: "icon-btn", onClick: reload, disabled: loading || !answering, children: _jsx(Icon, { icon: RefreshCw, size: 16, className: clsx(loading && 'animate-spin') }) }) }), _jsx(CloseButton, { label: t('dialog.close'), dialog: true })] })] }), _jsxs("div", { className: "flex min-h-0 grow flex-col gap-6 overflow-y-auto px-6 pt-4 pb-6 max-[960px]:px-4", children: [notice?.(shown !== null), noRoots && (_jsx(EmptyState, { className: DIALOG_CENTER, icon: ChartNoAxesColumn, action: _jsx(HostAgents, {}), children: t('empty.noTranscripts') })), shown === null && answering && _jsx(LoadingBody, {}), shown === null && !answering && (_jsx(EmptyState, { className: DIALOG_CENTER, icon: ChartNoAxesColumn, action: _jsx(HostAgents, {}), children: t('empty.noUsage') })), shown !== null && derived !== null && !noRoots && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid gap-6 lg:grid-cols-[18rem_1fr]", children: [_jsx(UsageSummary, { metric: metric, costUsd: derived.costUsd, totals: derived.totals, sessions: shown.sessions, providers: derived.providers }), _jsx(UsageChart, { slots: derived.slots, providers: derived.active, format: value, labelEvery: labelEveryFor(derived.slots.length) })] }), _jsx(UsageTiles, { totals: derived.totals, cacheSavingsUsd: derived.cacheSavingsUsd }), _jsx(UsageBreakdown, { summary: shown, metric: metric, providers: derived.active }), _jsx(UsageLimits, {})] })), _jsx(Provenance, {})] })] }));
|
|
161
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChartNoAxesColumn } from 'lucide-react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { SettingsRow } from '@ruimte/ui/settings/SettingsRow';
|
|
5
|
+
import { SettingsSection } from '@ruimte/ui/settings/SettingsSection';
|
|
6
|
+
import { Segmented } from '@ruimte/ui/controls';
|
|
7
|
+
import { useUsage, useUsageStore } from '../state/usage';
|
|
8
|
+
import { Button } from '@ruimte/ui/Button';
|
|
9
|
+
import { Icon } from '@ruimte/ui/Icon';
|
|
10
|
+
const CURRENCIES = ['USD', 'EUR'];
|
|
11
|
+
/*
|
|
12
|
+
* Every price a model has is in dollars, so the only choice here is what to read them in. The usage
|
|
13
|
+
* page is a dialog of its own, so `onOpenPage` is where the app lets settings step aside for it.
|
|
14
|
+
*/
|
|
15
|
+
export function UsagePane({ onOpenPage }) {
|
|
16
|
+
const { t } = useTranslation('agent-usage');
|
|
17
|
+
const currency = useUsage((s) => s.currency);
|
|
18
|
+
const rate = useUsage((s) => s.summary?.rate ?? null);
|
|
19
|
+
return (_jsxs(SettingsSection, { children: [_jsx(SettingsRow, { searchId: "usage.currency", label: t('settings.currency.label'), description: currency === 'USD'
|
|
20
|
+
? t('settings.currency.dollars')
|
|
21
|
+
: rate === null
|
|
22
|
+
? t('settings.currency.noRate')
|
|
23
|
+
: t('settings.currency.converted', { date: rate.date }), control: _jsx(Segmented, { value: currency, options: CURRENCIES.map((id) => ({ id, label: t(`settings.currency.options.${id}`) })), onChange: (id) => useUsageStore.getState().setCurrency(id), label: t('settings.currency.label') }) }), _jsx(SettingsRow, { searchId: "usage.page", label: t('settings.page.label'), description: t('settings.page.description'), control: _jsxs(Button, { variant: "secondary", onClick: onOpenPage, children: [_jsx(Icon, { icon: ChartNoAxesColumn, size: 14 }), " ", t('settings.page.open')] }) })] }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UsageTotals } from '@ruimte/agent-contracts';
|
|
2
|
+
import type { UsageMetric } from '../state/usage';
|
|
3
|
+
import type { ProviderTotal } from './summary';
|
|
4
|
+
interface UsageSummaryProps {
|
|
5
|
+
metric: UsageMetric;
|
|
6
|
+
costUsd: number;
|
|
7
|
+
totals: UsageTotals;
|
|
8
|
+
sessions: number;
|
|
9
|
+
providers: readonly ProviderTotal[];
|
|
10
|
+
}
|
|
11
|
+
export declare function UsageSummary({ metric, costUsd, totals, sessions, providers }: UsageSummaryProps): import("react").JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ProviderLogo } from '../agents/ProviderLogo';
|
|
4
|
+
import { totalTokensOf } from '@ruimte/agent-contracts';
|
|
5
|
+
import { formatCount, formatTokens, PROVIDER_COLORS, PROVIDER_LABELS } from './format';
|
|
6
|
+
import { useMoney } from './money';
|
|
7
|
+
/*
|
|
8
|
+
* The number the page is about, with a row per provider under it. Those rows double as the chart's
|
|
9
|
+
* legend, which is why the mark is the same color the bar segment is drawn in.
|
|
10
|
+
*/
|
|
11
|
+
export function UsageSummary({ metric, costUsd, totals, sessions, providers }) {
|
|
12
|
+
const { t } = useTranslation('agent-usage');
|
|
13
|
+
const money = useMoney();
|
|
14
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { children: [_jsx("p", { className: "text-4xl font-semibold tabular-nums", children: metric === 'cost' ? money(costUsd) : formatTokens(totalTokensOf(totals)) }), _jsxs("p", { className: "mt-1 text-xs text-text-muted", children: [t('summary.sessions', { count: sessions, sessions: formatCount(sessions) }), " \u00B7", ' ', t('summary.calls', { count: totals.calls, calls: formatCount(totals.calls) }), " \u00B7 ", t('summary.apiRates')] })] }), _jsx("div", { className: "flex flex-col gap-2", children: providers.map((provider) => (_jsxs("div", { className: "flex items-center gap-2 text-xs", children: [_jsx("span", { style: { color: PROVIDER_COLORS[provider.provider] }, children: _jsx(ProviderLogo, { provider: provider.provider }) }), _jsx("span", { className: "text-text", children: PROVIDER_LABELS[provider.provider] }), _jsxs("span", { className: "ml-auto tabular-nums text-text-muted", children: [money(provider.costUsd), " \u00B7 ", formatTokens(totalTokensOf(provider.totals))] })] }, provider.provider))) })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UsageTotals } from '@ruimte/agent-contracts';
|
|
2
|
+
interface UsageTilesProps {
|
|
3
|
+
totals: UsageTotals;
|
|
4
|
+
cacheSavingsUsd: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function UsageTiles({ totals, cacheSavingsUsd }: UsageTilesProps): import("react").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { totalTokensOf } from '@ruimte/agent-contracts';
|
|
4
|
+
import { formatTokens } from './format';
|
|
5
|
+
import { useMoney } from './money';
|
|
6
|
+
/* What the period moved, split by the kinds of token that are priced differently. */
|
|
7
|
+
export function UsageTiles({ totals, cacheSavingsUsd }) {
|
|
8
|
+
const { t } = useTranslation('agent-usage');
|
|
9
|
+
const money = useMoney();
|
|
10
|
+
const tiles = [
|
|
11
|
+
{ id: 'processed', value: formatTokens(totalTokensOf(totals)) },
|
|
12
|
+
{ id: 'uncachedInput', value: formatTokens(totals.input) },
|
|
13
|
+
{ id: 'cachedInput', value: formatTokens(totals.cacheRead) },
|
|
14
|
+
{ id: 'cacheWrites', value: formatTokens(totals.cacheWrite) },
|
|
15
|
+
{ id: 'output', value: formatTokens(totals.output) },
|
|
16
|
+
{ id: 'cacheSavings', value: money(cacheSavingsUsd) }
|
|
17
|
+
];
|
|
18
|
+
return (_jsx("div", { className: "grid grid-cols-2 gap-2 md:grid-cols-6", children: tiles.map((tile) => (_jsxs("div", { className: "rounded-xl border border-border bg-surface px-4 py-3", children: [_jsx("p", { className: "text-xs text-text-muted", children: t(`tiles.${tile.id}`) }), _jsx("p", { className: "mt-0.5 text-base font-medium tabular-nums", children: tile.value })] }, tile.id))) }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { UsageProvider, UsageRate } from '@ruimte/agent-contracts';
|
|
2
|
+
import { formatTokens } from '@ruimte/ui/format/number';
|
|
3
|
+
export type UsageCurrency = 'USD' | 'EUR';
|
|
4
|
+
export declare const formatCount: (value: number) => string;
|
|
5
|
+
export { formatTokens };
|
|
6
|
+
export declare const moneyFormat: (currency: UsageCurrency, rate: UsageRate | null) => ((usd: number) => string);
|
|
7
|
+
export declare const formatUsd: (usd: number) => string;
|
|
8
|
+
export declare const PROVIDER_LABELS: Record<UsageProvider, string>;
|
|
9
|
+
export declare const PROVIDER_COLORS: Record<UsageProvider, string>;
|
|
10
|
+
export declare const slotLabel: (slot: string) => string;
|
|
11
|
+
export declare const slotAxisLabel: (slot: string) => string;
|
|
12
|
+
export { formatClock } from '@ruimte/ui/format/datetime';
|
|
13
|
+
export declare const formatDate: (at: number) => string;
|
|
14
|
+
export declare const shortPath: (path: string) => string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { formatDay, formatHour, formatWeekdayDay } from '@ruimte/ui/format/datetime';
|
|
2
|
+
import { formatMoney, formatNumber, formatTokens } from '@ruimte/ui/format/number';
|
|
3
|
+
/* Every number on this page is read in the region of the machine looking at it, as a bank statement
|
|
4
|
+
would be: `$5,480.96` in the US, `$ 5.480,96` in the Netherlands. The labels stay English. */
|
|
5
|
+
export const formatCount = (value) => formatNumber(value);
|
|
6
|
+
export { formatTokens };
|
|
7
|
+
/*
|
|
8
|
+
* What a call cost, in the currency the page is set to. Prices are in dollars, so anything else is
|
|
9
|
+
* that amount at the day's reference rate; without a rate the page stays in dollars rather than
|
|
10
|
+
* showing a euro sign in front of a dollar amount.
|
|
11
|
+
*/
|
|
12
|
+
export const moneyFormat = (currency, rate) => {
|
|
13
|
+
const converts = currency !== 'USD' && rate !== null && rate.currency === currency;
|
|
14
|
+
const factor = converts ? rate.rate : 1;
|
|
15
|
+
const code = converts ? currency : 'USD';
|
|
16
|
+
return (usd) => {
|
|
17
|
+
const value = usd * factor;
|
|
18
|
+
// Under a cent the two decimals of a currency read as zero, which is the one thing it is not.
|
|
19
|
+
return formatMoney(value, code, value !== 0 && Math.abs(value) < 0.01 ? 4 : undefined);
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/* The dollar amounts of a page that has no summary yet, and of a tooltip that prices nothing. */
|
|
23
|
+
export const formatUsd = moneyFormat('USD', null);
|
|
24
|
+
export const PROVIDER_LABELS = { claude: 'Claude Code', codex: 'Codex' };
|
|
25
|
+
export const PROVIDER_COLORS = { claude: 'var(--chart-claude)', codex: 'var(--chart-codex)' };
|
|
26
|
+
/* A slot is `2026-09-10` or `2026-09-10T14`, already in the viewer's own zone, so it is read as
|
|
27
|
+
plain wall clock and never handed to a parser that would shift it back. */
|
|
28
|
+
const dateOfSlot = (slot) => new Date(Number(slot.slice(0, 4)), Number(slot.slice(5, 7)) - 1, Number(slot.slice(8, 10)));
|
|
29
|
+
const hourOfSlot = (slot) => (slot.length > 10 ? Number(slot.slice(11, 13)) : null);
|
|
30
|
+
const atHour = (slot, hour) => {
|
|
31
|
+
const day = dateOfSlot(slot);
|
|
32
|
+
day.setHours(hour);
|
|
33
|
+
return day;
|
|
34
|
+
};
|
|
35
|
+
export const slotLabel = (slot) => {
|
|
36
|
+
const hour = hourOfSlot(slot);
|
|
37
|
+
return hour === null ? formatWeekdayDay(dateOfSlot(slot)) : formatHour(atHour(slot, hour));
|
|
38
|
+
};
|
|
39
|
+
export const slotAxisLabel = (slot) => {
|
|
40
|
+
const hour = hourOfSlot(slot);
|
|
41
|
+
return hour === null ? formatDay(dateOfSlot(slot)) : formatHour(atHour(slot, hour));
|
|
42
|
+
};
|
|
43
|
+
export { formatClock } from '@ruimte/ui/format/datetime';
|
|
44
|
+
export const formatDate = (at) => formatDay(at);
|
|
45
|
+
/* A path under a row that already carries the name: enough of the tail to tell two checkouts of the
|
|
46
|
+
same repository apart, without the home directory nobody needs to read again. */
|
|
47
|
+
export const shortPath = (path) => {
|
|
48
|
+
const parts = path.split('/').filter(Boolean);
|
|
49
|
+
return parts.length <= 3 ? path : `…/${parts.slice(-3).join('/')}`;
|
|
50
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ProviderAccounts, type ProviderAccountStatus, type UsageLimitsProvider, type UsageLimitsSnapshot, type UsageProvider, type UsageWindow } from '@ruimte/agent-contracts';
|
|
2
|
+
export interface LimitAccount {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: UsageProvider;
|
|
5
|
+
name: string;
|
|
6
|
+
color: string | undefined;
|
|
7
|
+
entry: UsageLimitsProvider | null;
|
|
8
|
+
status: ProviderAccountStatus | null;
|
|
9
|
+
}
|
|
10
|
+
export interface LimitGroup {
|
|
11
|
+
kind: UsageProvider;
|
|
12
|
+
accounts: LimitAccount[];
|
|
13
|
+
}
|
|
14
|
+
export declare const limitGroups: (snapshot: UsageLimitsSnapshot, accounts: ProviderAccounts | null) => LimitGroup[];
|
|
15
|
+
export declare const hasSeveralAccounts: (groups: readonly LimitGroup[]) => boolean;
|
|
16
|
+
export declare const isSignedOut: (account: LimitAccount) => boolean;
|
|
17
|
+
export declare const nextReset: (windows: readonly UsageWindow[], now: number) => (UsageWindow & {
|
|
18
|
+
resetsAt: number;
|
|
19
|
+
}) | null;
|
|
20
|
+
export declare const checkedLabel: (entry: UsageLimitsProvider, now: number) => string | null;
|
|
21
|
+
export declare const explain: (provider: UsageLimitsProvider) => string | null;
|
|
22
|
+
export declare const accountNote: (account: LimitAccount) => string | null;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
import { USAGE_PROVIDERS } from '@ruimte/agent-contracts';
|
|
3
|
+
import { limitsAccountId } from '../agents/account-limits';
|
|
4
|
+
import { accountName, accountsOfKind, accountStatusLine } from '../agents/accounts';
|
|
5
|
+
import { formatDuration } from '@ruimte/ui/format/duration';
|
|
6
|
+
import { PROVIDER_LABELS } from './format';
|
|
7
|
+
const MINUTE = 60_000;
|
|
8
|
+
/*
|
|
9
|
+
* The plan numbers per CLI, one row per account. The machine only reads an account that is signed in,
|
|
10
|
+
* so an account that is on but signed out comes from the account list, where it can say why it has
|
|
11
|
+
* no numbers. An account that is off is left out, unless the machine still reads it (a CLI's default).
|
|
12
|
+
*/
|
|
13
|
+
export const limitGroups = (snapshot, accounts) => USAGE_PROVIDERS.flatMap((kind) => {
|
|
14
|
+
const known = accountsOfKind(accounts, kind);
|
|
15
|
+
const read = snapshot.providers
|
|
16
|
+
.filter((entry) => entry.kind === kind)
|
|
17
|
+
.map((entry) => {
|
|
18
|
+
const id = limitsAccountId(entry);
|
|
19
|
+
const account = known.find((each) => each.id === id);
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
kind,
|
|
23
|
+
name: account === undefined ? (entry.account?.label ?? PROVIDER_LABELS[kind]) : accountName(account, PROVIDER_LABELS[kind]),
|
|
24
|
+
color: account === undefined ? entry.account?.color : account.account.color,
|
|
25
|
+
entry,
|
|
26
|
+
status: account?.status ?? null
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const unread = known
|
|
30
|
+
.filter((account) => account.account.enabled !== false && !read.some((row) => row.id === account.id))
|
|
31
|
+
.map((account) => ({
|
|
32
|
+
id: account.id,
|
|
33
|
+
kind,
|
|
34
|
+
name: accountName(account, PROVIDER_LABELS[kind]),
|
|
35
|
+
color: account.account.color,
|
|
36
|
+
entry: null,
|
|
37
|
+
status: account.status
|
|
38
|
+
}));
|
|
39
|
+
const rows = [...read, ...unread];
|
|
40
|
+
return rows.length === 0 ? [] : [{ kind, accounts: rows }];
|
|
41
|
+
});
|
|
42
|
+
/* Whether any CLI has more than one account, which is when the limits name the account of every row. */
|
|
43
|
+
export const hasSeveralAccounts = (groups) => groups.some((group) => group.accounts.length > 1);
|
|
44
|
+
export const isSignedOut = (account) => account.status?.state === 'signed-out';
|
|
45
|
+
/* The window that resets first from now, the one reset a line per window has no room to repeat. */
|
|
46
|
+
export const nextReset = (windows, now) => {
|
|
47
|
+
let next = null;
|
|
48
|
+
for (const window of windows) {
|
|
49
|
+
if (window.resetsAt !== null && window.resetsAt > now && (next === null || window.resetsAt < next.resetsAt)) {
|
|
50
|
+
next = { ...window, resetsAt: window.resetsAt };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return next;
|
|
54
|
+
};
|
|
55
|
+
/* When the numbers came in and from where: a read of the machine's own, or a turn that reported them. Null before the first read. */
|
|
56
|
+
export const checkedLabel = (entry, now) => {
|
|
57
|
+
if (entry.checkedAt <= 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const past = now - entry.checkedAt;
|
|
61
|
+
const when = past < MINUTE ? i18next.t('agent-usage:limits.checked.now') : i18next.t('agent-usage:limits.checked.ago', { duration: formatDuration(past) });
|
|
62
|
+
return entry.source === 'event' ? i18next.t('agent-usage:limits.checked.fromTurn', { when }) : when;
|
|
63
|
+
};
|
|
64
|
+
/* Why a read account has no bars, or null when it has them. */
|
|
65
|
+
export const explain = (provider) => {
|
|
66
|
+
if (provider.unavailable === null) {
|
|
67
|
+
return provider.windows.length === 0 ? i18next.t('agent-usage:limits.none') : null;
|
|
68
|
+
}
|
|
69
|
+
if (provider.unavailable.reason === 'not-installed') {
|
|
70
|
+
return i18next.t('agent-usage:limits.notInstalled');
|
|
71
|
+
}
|
|
72
|
+
if (provider.unavailable.reason === 'no-subscription') {
|
|
73
|
+
return i18next.t('agent-usage:limits.noSubscription');
|
|
74
|
+
}
|
|
75
|
+
return provider.unavailable.message ?? i18next.t('agent-usage:limits.unreachable');
|
|
76
|
+
};
|
|
77
|
+
/* Why an account has no bars, or null when it has them. An account the machine does not read says what its login is doing. */
|
|
78
|
+
export const accountNote = (account) => account.entry === null ? accountStatusLine(account.status, true).text : explain(account.entry);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { UsageLimitsSnapshot } from '@ruimte/agent-contracts';
|
|
2
|
+
import { type LimitGroup } from './limit-groups';
|
|
3
|
+
export declare const useUsageLimits: () => UsageLimitsSnapshot | null;
|
|
4
|
+
export declare const useLimitGroups: (limits: UsageLimitsSnapshot | null) => LimitGroup[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useEffect, useMemo } from 'react';
|
|
2
|
+
import { useChatScope } from '../scope';
|
|
3
|
+
import { useProviderAccountsStore } from '../state/provider-accounts';
|
|
4
|
+
import { useUsage, useUsageStore } from '../state/usage';
|
|
5
|
+
import { limitGroups } from './limit-groups';
|
|
6
|
+
/* Asking the CLIs is expensive, so everything that shows the windows shares one read per host. */
|
|
7
|
+
const holds = new Map();
|
|
8
|
+
/* On the scope's transport as it stands: reading the windows never makes the host connect, so they fill once it is connected for another reason. */
|
|
9
|
+
const hold = (scope) => {
|
|
10
|
+
const link = scope.transport;
|
|
11
|
+
const scopeId = scope.id;
|
|
12
|
+
const ask = () => {
|
|
13
|
+
link.request('usage.limits', {})
|
|
14
|
+
.then((snapshot) => useUsageStore.getState().setLimits(scopeId, snapshot))
|
|
15
|
+
.catch(() => undefined);
|
|
16
|
+
};
|
|
17
|
+
if (link.status === 'open') {
|
|
18
|
+
ask();
|
|
19
|
+
}
|
|
20
|
+
// A running turn reports its own numbers, which is what keeps a bar moving between reads.
|
|
21
|
+
const offChanged = link.on('usage.limitsChanged', (snapshot) => useUsageStore.getState().setLimits(scopeId, snapshot));
|
|
22
|
+
const offStatus = link.subscribeStatus((status) => {
|
|
23
|
+
if (status === 'open') {
|
|
24
|
+
ask();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
readers: 0,
|
|
29
|
+
release: () => {
|
|
30
|
+
offChanged();
|
|
31
|
+
offStatus();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/* The snapshot of the host in scope, kept fresh for as long as anything shows it. */
|
|
36
|
+
export const useUsageLimits = () => {
|
|
37
|
+
const limits = useUsage((s) => s.limits);
|
|
38
|
+
const scope = useChatScope();
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const held = holds.get(scope.id) ?? hold(scope);
|
|
41
|
+
held.readers += 1;
|
|
42
|
+
holds.set(scope.id, held);
|
|
43
|
+
return () => {
|
|
44
|
+
held.readers -= 1;
|
|
45
|
+
if (held.readers === 0) {
|
|
46
|
+
held.release();
|
|
47
|
+
holds.delete(scope.id);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}, [scope]);
|
|
51
|
+
return limits;
|
|
52
|
+
};
|
|
53
|
+
/* The snapshot per CLI and account, with the accounts of the same host it is read from. */
|
|
54
|
+
export const useLimitGroups = (limits) => {
|
|
55
|
+
const { id } = useChatScope();
|
|
56
|
+
const accounts = useProviderAccountsStore((s) => s.byScope[id]?.accounts ?? null);
|
|
57
|
+
return useMemo(() => (limits === null ? [] : limitGroups(limits, accounts)), [limits, accounts]);
|
|
58
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const useMoney: () => ((usd: number) => string);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { useUsage } from '../state/usage';
|
|
3
|
+
import { moneyFormat } from './format';
|
|
4
|
+
/*
|
|
5
|
+
* The formatter every amount on the page goes through: the currency that was chosen, at the rate
|
|
6
|
+
* the summary on screen carries. It is a hook rather than a prop because every row wants it and
|
|
7
|
+
* none of them wants to know where the rate came from.
|
|
8
|
+
*/
|
|
9
|
+
export const useMoney = () => {
|
|
10
|
+
const currency = useUsage((s) => s.currency);
|
|
11
|
+
const rate = useUsage((s) => s.summary?.rate ?? null);
|
|
12
|
+
return useMemo(() => moneyFormat(currency, rate), [currency, rate]);
|
|
13
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { UsageProvider, UsageSummaryResult, UsageTotals } from '@ruimte/agent-contracts';
|
|
2
|
+
import type { UsageMetric } from '../state/usage';
|
|
3
|
+
export interface ChartSlot {
|
|
4
|
+
slot: string;
|
|
5
|
+
byProvider: Partial<Record<UsageProvider, number>>;
|
|
6
|
+
total: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ProviderTotal {
|
|
9
|
+
provider: UsageProvider;
|
|
10
|
+
costUsd: number;
|
|
11
|
+
totals: UsageTotals;
|
|
12
|
+
}
|
|
13
|
+
export declare const niceScale: (max: number, ticks?: number) => {
|
|
14
|
+
max: number;
|
|
15
|
+
step: number;
|
|
16
|
+
};
|
|
17
|
+
export interface DerivedUsage {
|
|
18
|
+
slots: ChartSlot[];
|
|
19
|
+
providers: ProviderTotal[];
|
|
20
|
+
active: UsageProvider[];
|
|
21
|
+
totals: UsageTotals;
|
|
22
|
+
costUsd: number;
|
|
23
|
+
cacheSavingsUsd: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const enumerateSlots: (summary: Pick<UsageSummaryResult, 'from' | 'to' | 'resolution'>) => string[];
|
|
26
|
+
export declare const labelEveryFor: (slots: number) => number;
|
|
27
|
+
export declare const deriveUsage: (summary: UsageSummaryResult, metric: UsageMetric) => DerivedUsage;
|
|
28
|
+
export interface UsageDay {
|
|
29
|
+
slot: string;
|
|
30
|
+
costByProvider: Partial<Record<UsageProvider, number>>;
|
|
31
|
+
costUsd: number;
|
|
32
|
+
tokens: number;
|
|
33
|
+
}
|
|
34
|
+
export declare const deriveDays: (summary: UsageSummaryResult) => UsageDay[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { EMPTY_TOTALS, USAGE_PROVIDERS, addTotals, totalTokensOf } from '@ruimte/agent-contracts';
|
|
2
|
+
/* Three or four round numbers up the side. The one piece of arithmetic a chart library would bring. */
|
|
3
|
+
export const niceScale = (max, ticks = 4) => {
|
|
4
|
+
if (!Number.isFinite(max) || max <= 0) {
|
|
5
|
+
return { max: 1, step: 1 };
|
|
6
|
+
}
|
|
7
|
+
const rough = max / ticks;
|
|
8
|
+
const magnitude = 10 ** Math.floor(Math.log10(rough));
|
|
9
|
+
const normalized = rough / magnitude;
|
|
10
|
+
const step = (normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10) * magnitude;
|
|
11
|
+
return { max: Math.ceil(max / step) * step, step };
|
|
12
|
+
};
|
|
13
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
14
|
+
/*
|
|
15
|
+
* Every slot the period has, whether anything happened in it or not: an empty day keeps its place on
|
|
16
|
+
* the axis, so a gap in the work reads as a gap and not as a shorter week.
|
|
17
|
+
*/
|
|
18
|
+
export const enumerateSlots = (summary) => {
|
|
19
|
+
if (summary.resolution === 'hour') {
|
|
20
|
+
return Array.from({ length: 24 }, (_, hour) => `${summary.to}T${pad(hour)}`);
|
|
21
|
+
}
|
|
22
|
+
const slots = [];
|
|
23
|
+
const at = new Date(`${summary.from}T00:00:00`);
|
|
24
|
+
const end = new Date(`${summary.to}T00:00:00`);
|
|
25
|
+
// A calendar loop rather than adding milliseconds, since a day is 23 or 25 hours long twice a year.
|
|
26
|
+
while (at <= end && slots.length < 400) {
|
|
27
|
+
slots.push(`${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())}`);
|
|
28
|
+
at.setDate(at.getDate() + 1);
|
|
29
|
+
}
|
|
30
|
+
return slots;
|
|
31
|
+
};
|
|
32
|
+
/* How often an x label fits: every third hour, every day, every week, every fortnight. */
|
|
33
|
+
export const labelEveryFor = (slots) => (slots <= 10 ? 1 : slots <= 24 ? 3 : slots <= 31 ? 7 : 15);
|
|
34
|
+
/*
|
|
35
|
+
* The buckets folded into what the page draws. The daemon sends one bucket per slot, provider and
|
|
36
|
+
* model, which is the smallest thing every part of the page can be summed out of.
|
|
37
|
+
*/
|
|
38
|
+
export const deriveUsage = (summary, metric) => {
|
|
39
|
+
const bySlot = new Map();
|
|
40
|
+
for (const slot of enumerateSlots(summary)) {
|
|
41
|
+
bySlot.set(slot, { slot, byProvider: {}, total: 0 });
|
|
42
|
+
}
|
|
43
|
+
const byProvider = new Map();
|
|
44
|
+
let totals = EMPTY_TOTALS;
|
|
45
|
+
let costUsd = 0;
|
|
46
|
+
let cacheSavingsUsd = 0;
|
|
47
|
+
for (const bucket of summary.buckets) {
|
|
48
|
+
const value = metric === 'cost' ? (bucket.costUsd ?? 0) : totalTokensOf(bucket.totals);
|
|
49
|
+
const slot = bySlot.get(bucket.slot);
|
|
50
|
+
if (slot !== undefined) {
|
|
51
|
+
slot.byProvider[bucket.provider] = (slot.byProvider[bucket.provider] ?? 0) + value;
|
|
52
|
+
slot.total += value;
|
|
53
|
+
}
|
|
54
|
+
const provider = byProvider.get(bucket.provider) ?? { provider: bucket.provider, costUsd: 0, totals: EMPTY_TOTALS };
|
|
55
|
+
provider.costUsd += bucket.costUsd ?? 0;
|
|
56
|
+
provider.totals = addTotals(provider.totals, bucket.totals);
|
|
57
|
+
byProvider.set(bucket.provider, provider);
|
|
58
|
+
totals = addTotals(totals, bucket.totals);
|
|
59
|
+
costUsd += bucket.costUsd ?? 0;
|
|
60
|
+
cacheSavingsUsd += bucket.cacheSavingsUsd;
|
|
61
|
+
}
|
|
62
|
+
const providers = [...byProvider.values()].sort((a, b) => b.costUsd - a.costUsd);
|
|
63
|
+
return {
|
|
64
|
+
slots: [...bySlot.values()],
|
|
65
|
+
providers,
|
|
66
|
+
active: USAGE_PROVIDERS.filter((provider) => byProvider.has(provider)),
|
|
67
|
+
totals,
|
|
68
|
+
costUsd,
|
|
69
|
+
cacheSavingsUsd
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/*
|
|
73
|
+
* The same buckets the chart draws, written out as a table of calendar days, newest first. Today is
|
|
74
|
+
* drawn per hour but read per day, so an hour slot is folded onto the date it names. A day nothing
|
|
75
|
+
* happened in is left out, since the chart already shows the gap, and a quiet fortnight would otherwise
|
|
76
|
+
* be fourteen rows of zeroes between the days worth reading.
|
|
77
|
+
*/
|
|
78
|
+
export const deriveDays = (summary) => {
|
|
79
|
+
const rows = new Map();
|
|
80
|
+
for (const bucket of summary.buckets) {
|
|
81
|
+
const day = bucket.slot.slice(0, 10);
|
|
82
|
+
const row = rows.get(day) ?? { slot: day, costByProvider: {}, costUsd: 0, tokens: 0 };
|
|
83
|
+
const cost = bucket.costUsd ?? 0;
|
|
84
|
+
row.costByProvider[bucket.provider] = (row.costByProvider[bucket.provider] ?? 0) + cost;
|
|
85
|
+
row.costUsd += cost;
|
|
86
|
+
row.tokens += totalTokensOf(bucket.totals);
|
|
87
|
+
rows.set(day, row);
|
|
88
|
+
}
|
|
89
|
+
return [...rows.values()].sort((a, b) => b.slot.localeCompare(a.slot));
|
|
90
|
+
};
|