@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,7 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
/* What a provider offers, in one sentence, where it can be opened and whether its hooks report status. */
|
|
3
|
+
export const providerAbilities = (provider) => {
|
|
4
|
+
const where = provider.capabilities.chat ? i18next.t('agent-providers:abilities.chatAndTerminal') : i18next.t('agent-providers:abilities.terminalOnly');
|
|
5
|
+
const status = provider.capabilities.hooks ? i18next.t('agent-providers:abilities.reportsStatus') : i18next.t('agent-providers:abilities.noStatus');
|
|
6
|
+
return `${where} ${status}`;
|
|
7
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProviderAccountVariable } from '@ruimte/agent-contracts';
|
|
2
|
+
export interface VariableDraft {
|
|
3
|
+
key: string;
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
sensitive: boolean;
|
|
7
|
+
kept: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare const draftsOf: (env: readonly ProviderAccountVariable[] | undefined) => VariableDraft[];
|
|
10
|
+
export declare const emptyDraft: (sensitive: boolean) => VariableDraft;
|
|
11
|
+
export declare const variablesProblem: (drafts: readonly VariableDraft[]) => string | null;
|
|
12
|
+
export declare const variablesOf: (drafts: readonly VariableDraft[]) => ProviderAccountVariable[];
|
|
13
|
+
export declare const variablesChanged: (drafts: readonly VariableDraft[], env: readonly ProviderAccountVariable[] | undefined) => boolean;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
3
|
+
let drafted = 0;
|
|
4
|
+
const nextKey = () => {
|
|
5
|
+
drafted += 1;
|
|
6
|
+
return `variable-${drafted}`;
|
|
7
|
+
};
|
|
8
|
+
export const draftsOf = (env) => (env ?? []).map((variable) => ({
|
|
9
|
+
key: nextKey(),
|
|
10
|
+
name: variable.name,
|
|
11
|
+
value: variable.value,
|
|
12
|
+
sensitive: variable.sensitive,
|
|
13
|
+
kept: variable.sensitive && variable.valueRedacted === true ? variable.name : null
|
|
14
|
+
}));
|
|
15
|
+
export const emptyDraft = (sensitive) => ({ key: nextKey(), name: '', value: '', sensitive, kept: null });
|
|
16
|
+
/*
|
|
17
|
+
* A value the keychain holds only counts under the name it was kept under, and only while it stays
|
|
18
|
+
* sensitive: the machine finds it by that name, and never hands it out to be stored in plain text.
|
|
19
|
+
*/
|
|
20
|
+
const keepsValue = (draft) => draft.value === '' && draft.sensitive && draft.kept === draft.name;
|
|
21
|
+
/* The first thing that keeps these from being saved, in a sentence; null when they can be. */
|
|
22
|
+
export const variablesProblem = (drafts) => {
|
|
23
|
+
const words = (key, name) => i18next.t(`agent-providers:account.variables.problem.${key}`, { name });
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const draft of drafts) {
|
|
26
|
+
const name = draft.name.trim();
|
|
27
|
+
if (name === '') {
|
|
28
|
+
return words('noName', '');
|
|
29
|
+
}
|
|
30
|
+
if (!NAME.test(name)) {
|
|
31
|
+
return words('invalidName', name);
|
|
32
|
+
}
|
|
33
|
+
if (seen.has(name)) {
|
|
34
|
+
return words('duplicate', name);
|
|
35
|
+
}
|
|
36
|
+
seen.add(name);
|
|
37
|
+
if (draft.value === '' && !keepsValue({ ...draft, name })) {
|
|
38
|
+
return words('noValue', name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
/* The variables as `accounts.save` takes them; a kept value goes back empty and redacted, so the machine keeps what it has. */
|
|
44
|
+
export const variablesOf = (drafts) => drafts.map((draft) => {
|
|
45
|
+
const name = draft.name.trim();
|
|
46
|
+
if (keepsValue({ ...draft, name })) {
|
|
47
|
+
return { name, value: '', sensitive: true, valueRedacted: true };
|
|
48
|
+
}
|
|
49
|
+
return { name, value: draft.value, sensitive: draft.sensitive };
|
|
50
|
+
});
|
|
51
|
+
/* Whether the drafts say something other than what the machine holds. */
|
|
52
|
+
export const variablesChanged = (drafts, env) => JSON.stringify(variablesOf(drafts)) !==
|
|
53
|
+
JSON.stringify((env ?? []).map(({ name, value, sensitive, valueRedacted }) => ({ name, value, sensitive, ...(valueRedacted ? { valueRedacted } : {}) })));
|
package/dist/scope.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ChatClient } from './chat/chat-client';
|
|
2
|
+
import type { ChatTransport } from './transport';
|
|
3
|
+
export interface ChatScope {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
keyOf(chatId: string): string;
|
|
6
|
+
owns(key: string): boolean;
|
|
7
|
+
readonly transport: ChatTransport;
|
|
8
|
+
readonly chats: ChatClient;
|
|
9
|
+
}
|
|
10
|
+
export declare const ChatScopeContext: import("react").Context<ChatScope | null>;
|
|
11
|
+
export declare const useChatScope: () => ChatScope;
|
package/dist/scope.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const ChatScopeContext = createContext(null);
|
|
3
|
+
/* Throws outside a scope on purpose: a component that reads a chat has to know which host it runs on. */
|
|
4
|
+
export const useChatScope = () => {
|
|
5
|
+
const scope = useContext(ChatScopeContext);
|
|
6
|
+
if (scope === null) {
|
|
7
|
+
throw new Error('This component reads a chat, so it has to be rendered inside a ChatScopeContext');
|
|
8
|
+
}
|
|
9
|
+
return scope;
|
|
10
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import { type LucideIcon } from 'lucide-react';
|
|
3
|
+
import type { SettingsSectionEntry } from '@ruimte/ui/settings/SettingsDialog';
|
|
4
|
+
export interface AgentsSettingsSection {
|
|
5
|
+
id: string;
|
|
6
|
+
icon: LucideIcon;
|
|
7
|
+
split?: boolean;
|
|
8
|
+
label(): string;
|
|
9
|
+
description(): string;
|
|
10
|
+
}
|
|
11
|
+
export declare const PROVIDERS_SECTION: AgentsSettingsSection;
|
|
12
|
+
export declare const USAGE_SECTION: AgentsSettingsSection;
|
|
13
|
+
export declare const settingsSection: (section: AgentsSettingsSection, pane: ComponentType) => SettingsSectionEntry;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
import { Brain, ChartNoAxesColumn } from 'lucide-react';
|
|
3
|
+
/* The agent CLIs of the host and the accounts they sign in with: `ProvidersPane`. */
|
|
4
|
+
export const PROVIDERS_SECTION = {
|
|
5
|
+
id: 'providers',
|
|
6
|
+
icon: Brain,
|
|
7
|
+
split: true,
|
|
8
|
+
label: () => i18next.t('agent-providers:section.label'),
|
|
9
|
+
description: () => i18next.t('agent-providers:section.description')
|
|
10
|
+
};
|
|
11
|
+
/* The currency of the usage page and the way to it: `UsagePane`. */
|
|
12
|
+
export const USAGE_SECTION = {
|
|
13
|
+
id: 'usage',
|
|
14
|
+
icon: ChartNoAxesColumn,
|
|
15
|
+
label: () => i18next.t('agent-usage:section.label'),
|
|
16
|
+
description: () => i18next.t('agent-usage:section.description')
|
|
17
|
+
};
|
|
18
|
+
/*
|
|
19
|
+
* The entry the dialog takes for a section, with the pane the app hands it: the pane of this package
|
|
20
|
+
* itself, a lazy one, or a wrapper that gives the pane what only the app knows. Built when the dialog
|
|
21
|
+
* draws, since the words follow the language.
|
|
22
|
+
*/
|
|
23
|
+
export const settingsSection = (section, pane) => ({
|
|
24
|
+
id: section.id,
|
|
25
|
+
icon: section.icon,
|
|
26
|
+
label: section.label(),
|
|
27
|
+
description: section.description(),
|
|
28
|
+
pane,
|
|
29
|
+
split: section.split
|
|
30
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ChatBookmark, ChatEvent, ChatInfo, ChatItem } from '@ruimte/agent-contracts';
|
|
2
|
+
export interface ChatState {
|
|
3
|
+
info: ChatInfo;
|
|
4
|
+
items: Record<string, ChatItem>;
|
|
5
|
+
structure: Record<string, ChatItem>;
|
|
6
|
+
order: string[];
|
|
7
|
+
bookmarks?: ChatBookmark[];
|
|
8
|
+
}
|
|
9
|
+
export type ChatsById = Record<string, ChatState>;
|
|
10
|
+
export type ChatStatuses = Record<string, Pick<ChatState, 'info'>>;
|
|
11
|
+
export interface ChatSink {
|
|
12
|
+
reset(chatId: string, info: ChatInfo, items: ChatItem[]): void;
|
|
13
|
+
apply(chatId: string, event: ChatEvent): void;
|
|
14
|
+
status(chatId: string, info: ChatInfo): void;
|
|
15
|
+
bookmarks(chatId: string, bookmarks: ChatBookmark[]): void;
|
|
16
|
+
forget(chatId: string): void;
|
|
17
|
+
}
|
|
18
|
+
interface ChatsStore {
|
|
19
|
+
byKey: ChatsById;
|
|
20
|
+
statusByKey: ChatStatuses;
|
|
21
|
+
reset(key: string, info: ChatInfo, items: ChatItem[]): void;
|
|
22
|
+
apply(key: string, event: ChatEvent): void;
|
|
23
|
+
status(key: string, info: ChatInfo): void;
|
|
24
|
+
bookmarks(key: string, bookmarks: ChatBookmark[]): void;
|
|
25
|
+
forget(key: string): void;
|
|
26
|
+
forgetWhere(matches: (key: string) => boolean): void;
|
|
27
|
+
}
|
|
28
|
+
export declare const applyEvent: (state: ChatState, event: ChatEvent) => ChatState;
|
|
29
|
+
export declare const useChats: import("zustand").UseBoundStore<import("zustand").StoreApi<ChatsStore>>;
|
|
30
|
+
export declare const chatSink: (keyOf: (chatId: string) => string) => ChatSink;
|
|
31
|
+
export declare const useChatRow: <T>(chatId: string, select: (row: ChatState | undefined) => T) => T;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { useChatScope } from '../scope';
|
|
3
|
+
const stateOf = (info, items) => {
|
|
4
|
+
const byId = Object.fromEntries(items.map((item) => [item.id, item]));
|
|
5
|
+
return { info, items: byId, structure: byId, order: items.map((item) => item.id) };
|
|
6
|
+
};
|
|
7
|
+
/* The state with one item replaced, in the thread and in the structure the rows come from. */
|
|
8
|
+
const withItem = (state, item) => {
|
|
9
|
+
const items = { ...state.items, [item.id]: item };
|
|
10
|
+
return { ...state, items, structure: items };
|
|
11
|
+
};
|
|
12
|
+
/* Cheap enough, since a status only arrives when the host saw one change, never on a streamed word. */
|
|
13
|
+
const sameInfo = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
14
|
+
/* The statuses with this chat's info in them, the same object when that info is already there. */
|
|
15
|
+
const withStatus = (statuses, key, info) => statuses[key]?.info === info ? statuses : { ...statuses, [key]: { info } };
|
|
16
|
+
const without = (rows, matches) => Object.fromEntries(Object.entries(rows).filter(([key]) => !matches(key)));
|
|
17
|
+
export const applyEvent = (state, event) => {
|
|
18
|
+
switch (event.type) {
|
|
19
|
+
case 'item': {
|
|
20
|
+
const known = event.item.id in state.items;
|
|
21
|
+
return { ...withItem(state, event.item), order: known ? state.order : [...state.order, event.item.id] };
|
|
22
|
+
}
|
|
23
|
+
case 'delta': {
|
|
24
|
+
const item = state.items[event.itemId];
|
|
25
|
+
if (item?.kind === 'assistant' || item?.kind === 'thinking') {
|
|
26
|
+
const grown = { ...item, text: item.text + event.text };
|
|
27
|
+
// The first text is structure after all, since an empty reply that is not streaming has no row.
|
|
28
|
+
// A sub-agent's text is drawn from inside its parent's row, which only the rows carry.
|
|
29
|
+
if (item.text === '' || (item.kind === 'assistant' && item.parentToolUseId)) {
|
|
30
|
+
return withItem(state, grown);
|
|
31
|
+
}
|
|
32
|
+
return { ...state, items: { ...state.items, [event.itemId]: grown } };
|
|
33
|
+
}
|
|
34
|
+
if (item?.kind === 'tool' && item.state === 'running') {
|
|
35
|
+
const progress = item.progress ?? { startedAt: null, description: null, output: null };
|
|
36
|
+
return withItem(state, { ...item, progress: { ...progress, output: (progress.output ?? '') + event.text } });
|
|
37
|
+
}
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
case 'info':
|
|
41
|
+
return { ...state, info: event.info };
|
|
42
|
+
case 'reset':
|
|
43
|
+
return stateOf(event.info, event.items);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export const useChats = create((set) => ({
|
|
47
|
+
byKey: {},
|
|
48
|
+
statusByKey: {},
|
|
49
|
+
reset(key, info, items) {
|
|
50
|
+
set((s) => {
|
|
51
|
+
// The bookmarks are not part of the thread; they come in on their own and outlive a reset of it.
|
|
52
|
+
const bookmarks = s.byKey[key]?.bookmarks;
|
|
53
|
+
return {
|
|
54
|
+
byKey: {
|
|
55
|
+
...s.byKey,
|
|
56
|
+
[key]: bookmarks === undefined ? stateOf(info, items) : { ...stateOf(info, items), bookmarks }
|
|
57
|
+
},
|
|
58
|
+
statusByKey: withStatus(s.statusByKey, key, info)
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
apply(key, event) {
|
|
63
|
+
set((s) => {
|
|
64
|
+
const current = s.byKey[key];
|
|
65
|
+
if (!current) {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
const next = applyEvent(current, event);
|
|
69
|
+
return { byKey: { ...s.byKey, [key]: next }, statusByKey: withStatus(s.statusByKey, key, next.info) };
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
/*
|
|
73
|
+
* The status of a chat this window is not reading, which the host sends for every chat it has
|
|
74
|
+
* loaded. The thread stays empty until somebody attaches, since this is what a header, a list and
|
|
75
|
+
* the counters need, and they ask the info and never the items.
|
|
76
|
+
*/
|
|
77
|
+
status(key, info) {
|
|
78
|
+
set((s) => {
|
|
79
|
+
const current = s.byKey[key];
|
|
80
|
+
if (!current) {
|
|
81
|
+
return { byKey: { ...s.byKey, [key]: stateOf(info, []) }, statusByKey: withStatus(s.statusByKey, key, info) };
|
|
82
|
+
}
|
|
83
|
+
// An attached client already had this on `chat.event`; writing it again would redraw the thread.
|
|
84
|
+
if (sameInfo(current.info, info)) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
return { byKey: { ...s.byKey, [key]: { ...current, info } }, statusByKey: withStatus(s.statusByKey, key, info) };
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
bookmarks(key, bookmarks) {
|
|
91
|
+
set((s) => {
|
|
92
|
+
const current = s.byKey[key];
|
|
93
|
+
if (!current) {
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
return { byKey: { ...s.byKey, [key]: { ...current, bookmarks } } };
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
forget(key) {
|
|
100
|
+
set((s) => {
|
|
101
|
+
const next = { ...s.byKey };
|
|
102
|
+
delete next[key];
|
|
103
|
+
const statuses = { ...s.statusByKey };
|
|
104
|
+
delete statuses[key];
|
|
105
|
+
return { byKey: next, statusByKey: statuses };
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
forgetWhere(matches) {
|
|
109
|
+
set((s) => ({ byKey: without(s.byKey, matches), statusByKey: without(s.statusByKey, matches) }));
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
/* The sink of one host's chat client: it hands over chat ids, this puts them under the keys of its scope. */
|
|
113
|
+
export const chatSink = (keyOf) => ({
|
|
114
|
+
reset: (chatId, info, items) => useChats.getState().reset(keyOf(chatId), info, items),
|
|
115
|
+
apply: (chatId, event) => useChats.getState().apply(keyOf(chatId), event),
|
|
116
|
+
status: (chatId, info) => useChats.getState().status(keyOf(chatId), info),
|
|
117
|
+
bookmarks: (chatId, bookmarks) => useChats.getState().bookmarks(keyOf(chatId), bookmarks),
|
|
118
|
+
forget: (chatId) => useChats.getState().forget(keyOf(chatId))
|
|
119
|
+
});
|
|
120
|
+
/* One chat's thread in the scope this is rendered in. The selector keeps a render tied to the field it reads. */
|
|
121
|
+
export const useChatRow = (chatId, select) => {
|
|
122
|
+
const { keyOf } = useChatScope();
|
|
123
|
+
return useChats((s) => select(s.byKey[keyOf(chatId)]));
|
|
124
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProviderAccounts } from '@ruimte/agent-contracts';
|
|
2
|
+
import type { ChatTransport } from '../transport';
|
|
3
|
+
export interface ProviderAccountsRow {
|
|
4
|
+
accounts: ProviderAccounts | null;
|
|
5
|
+
loaded: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface ProviderAccountsStore {
|
|
8
|
+
byScope: Record<string, ProviderAccountsRow>;
|
|
9
|
+
set(scopeId: string, accounts: ProviderAccounts | null): void;
|
|
10
|
+
forget(scopeId: string): void;
|
|
11
|
+
}
|
|
12
|
+
export declare const useProviderAccountsStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ProviderAccountsStore>>;
|
|
13
|
+
export declare const useProviderAccounts: <T>(select: (row: ProviderAccountsRow) => T) => T;
|
|
14
|
+
export declare const providerAccountsOf: (scopeId: string) => ProviderAccountsRow;
|
|
15
|
+
export declare const knownAccounts: (row: ProviderAccountsRow) => ProviderAccounts | null | undefined;
|
|
16
|
+
export declare const watchProviderAccounts: (scopeId: string, transport: ChatTransport) => (() => void);
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { useChatScope } from '../scope';
|
|
3
|
+
/* One object for a host that has not answered yet, so a selector gets a stable snapshot. */
|
|
4
|
+
const NONE = { accounts: null, loaded: false };
|
|
5
|
+
/* The accounts of every agent CLI on each host, and what each CLI last said about who is signed in there. */
|
|
6
|
+
export const useProviderAccountsStore = create((set, get) => ({
|
|
7
|
+
byScope: {},
|
|
8
|
+
set(scopeId, accounts) {
|
|
9
|
+
set({ byScope: { ...get().byScope, [scopeId]: { accounts, loaded: true } } });
|
|
10
|
+
},
|
|
11
|
+
forget(scopeId) {
|
|
12
|
+
const { [scopeId]: _gone, ...rest } = get().byScope;
|
|
13
|
+
set({ byScope: rest });
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
/* The accounts of the host in scope, like `useProviders`. */
|
|
17
|
+
export const useProviderAccounts = (select) => {
|
|
18
|
+
const { id } = useChatScope();
|
|
19
|
+
return useProviderAccountsStore((s) => select(s.byScope[id] ?? NONE));
|
|
20
|
+
};
|
|
21
|
+
export const providerAccountsOf = (scopeId) => useProviderAccountsStore.getState().byScope[scopeId] ?? NONE;
|
|
22
|
+
/* The accounts as `accountFor` takes them: undefined until the host answered, null for one that keeps none. */
|
|
23
|
+
export const knownAccounts = (row) => (row.loaded ? row.accounts : undefined);
|
|
24
|
+
/*
|
|
25
|
+
* Keeps one host's accounts in the store: the whole list on every fresh link, then every change the
|
|
26
|
+
* host announces. Answers the function that stops it.
|
|
27
|
+
*/
|
|
28
|
+
export const watchProviderAccounts = (scopeId, transport) => {
|
|
29
|
+
const store = useProviderAccountsStore.getState();
|
|
30
|
+
// A list asked for before a change arrived is older than that change, so it is dropped.
|
|
31
|
+
let changes = 0;
|
|
32
|
+
const load = () => {
|
|
33
|
+
const asked = changes;
|
|
34
|
+
transport
|
|
35
|
+
.request('accounts.list', {})
|
|
36
|
+
.then((accounts) => {
|
|
37
|
+
if (changes === asked) {
|
|
38
|
+
store.set(scopeId, accounts);
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
.catch(() => {
|
|
42
|
+
// A host from before accounts never answers; it has only each CLI's own login.
|
|
43
|
+
if (changes === asked && !providerAccountsOf(scopeId).loaded) {
|
|
44
|
+
store.set(scopeId, null);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const off = [
|
|
49
|
+
transport.on('accounts.changed', (accounts) => {
|
|
50
|
+
changes += 1;
|
|
51
|
+
store.set(scopeId, accounts);
|
|
52
|
+
}),
|
|
53
|
+
transport.subscribeStatus((status) => {
|
|
54
|
+
if (status === 'open') {
|
|
55
|
+
load();
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
];
|
|
59
|
+
if (transport.status === 'open') {
|
|
60
|
+
load();
|
|
61
|
+
}
|
|
62
|
+
return () => off.forEach((stop) => stop());
|
|
63
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProviderInfo } from '@ruimte/agent-contracts';
|
|
2
|
+
export interface ProvidersRow {
|
|
3
|
+
providers: ProviderInfo[];
|
|
4
|
+
loaded: boolean;
|
|
5
|
+
}
|
|
6
|
+
interface ProvidersStore {
|
|
7
|
+
byScope: Record<string, ProvidersRow>;
|
|
8
|
+
setProviders(scopeId: string, providers: ProviderInfo[]): void;
|
|
9
|
+
forget(scopeId: string): void;
|
|
10
|
+
}
|
|
11
|
+
export declare const useProvidersStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ProvidersStore>>;
|
|
12
|
+
export declare const useProviders: <T>(select: (row: ProvidersRow) => T) => T;
|
|
13
|
+
export declare const providersOf: (scopeId: string) => ProvidersRow;
|
|
14
|
+
export declare const providerSinkFor: (scopeId: string) => {
|
|
15
|
+
setProviders(providers: ProviderInfo[]): void;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { useChatScope } from '../scope';
|
|
3
|
+
/* One object for a host that has not answered yet, so a selector gets a stable snapshot. */
|
|
4
|
+
const NONE = { providers: [], loaded: false };
|
|
5
|
+
/* Which agent CLIs each host reports, and their models; filled once per connection. */
|
|
6
|
+
export const useProvidersStore = create((set, get) => ({
|
|
7
|
+
byScope: {},
|
|
8
|
+
setProviders(scopeId, providers) {
|
|
9
|
+
set({ byScope: { ...get().byScope, [scopeId]: { providers, loaded: true } } });
|
|
10
|
+
},
|
|
11
|
+
forget(scopeId) {
|
|
12
|
+
const { [scopeId]: _gone, ...rest } = get().byScope;
|
|
13
|
+
set({ byScope: rest });
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
/*
|
|
17
|
+
* The CLIs of the host in scope. An agent menu has to offer what the host the chat will run on has
|
|
18
|
+
* installed, never what another host answered.
|
|
19
|
+
*/
|
|
20
|
+
export const useProviders = (select) => {
|
|
21
|
+
const { id } = useChatScope();
|
|
22
|
+
return useProvidersStore((s) => select(s.byScope[id] ?? NONE));
|
|
23
|
+
};
|
|
24
|
+
/* The same answer outside a render. */
|
|
25
|
+
export const providersOf = (scopeId) => useProvidersStore.getState().byScope[scopeId] ?? NONE;
|
|
26
|
+
/* What one host's chat client fills in; it knows its own host and nothing of the others. */
|
|
27
|
+
export const providerSinkFor = (scopeId) => ({
|
|
28
|
+
setProviders: (providers) => useProvidersStore.getState().setProviders(scopeId, providers)
|
|
29
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UsageLimitsSnapshot, UsageSummaryPayload, UsageSummaryResult } from '@ruimte/agent-contracts';
|
|
2
|
+
import type { UsageCurrency } from '../usage/format';
|
|
3
|
+
export type UsagePeriod = 'today' | '7d' | '30d' | '90d';
|
|
4
|
+
export type UsageMetric = 'cost' | 'tokens';
|
|
5
|
+
export declare const USAGE_PERIODS: readonly {
|
|
6
|
+
id: UsagePeriod;
|
|
7
|
+
days: number;
|
|
8
|
+
}[];
|
|
9
|
+
interface Preferences {
|
|
10
|
+
period: UsagePeriod;
|
|
11
|
+
metric: UsageMetric;
|
|
12
|
+
currency: UsageCurrency;
|
|
13
|
+
}
|
|
14
|
+
export declare const dayOf: (date: Date) => string;
|
|
15
|
+
export declare const windowFor: (period: UsagePeriod, now?: Date) => UsageSummaryPayload;
|
|
16
|
+
export declare const summaryPayload: (period: UsagePeriod, account: string | null, now?: Date) => UsageSummaryPayload;
|
|
17
|
+
export declare const askedKey: (payload: UsageSummaryPayload) => string;
|
|
18
|
+
interface UsageRow {
|
|
19
|
+
asked: string | null;
|
|
20
|
+
summary: UsageSummaryResult | null;
|
|
21
|
+
loading: boolean;
|
|
22
|
+
failed: boolean;
|
|
23
|
+
limits: UsageLimitsSnapshot | null;
|
|
24
|
+
}
|
|
25
|
+
interface UsageStore extends Preferences {
|
|
26
|
+
byScope: Record<string, UsageRow>;
|
|
27
|
+
chosen: string | null;
|
|
28
|
+
choose(scopeId: string | null): void;
|
|
29
|
+
setPeriod(period: UsagePeriod): void;
|
|
30
|
+
setMetric(metric: UsageMetric): void;
|
|
31
|
+
setCurrency(currency: UsageCurrency): void;
|
|
32
|
+
setLoading(scopeId: string, loading: boolean): void;
|
|
33
|
+
receive(scopeId: string, asked: string, summary: UsageSummaryResult): void;
|
|
34
|
+
fail(scopeId: string): void;
|
|
35
|
+
setLimits(scopeId: string, limits: UsageLimitsSnapshot): void;
|
|
36
|
+
forget(scopeId: string): void;
|
|
37
|
+
}
|
|
38
|
+
export declare const useUsageStore: import("zustand").UseBoundStore<import("zustand").StoreApi<UsageStore>>;
|
|
39
|
+
export type UsageView = Preferences & UsageRow;
|
|
40
|
+
export declare const useUsage: <T>(select: (view: UsageView) => T) => T;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { localTimeZone } from '@ruimte/ui/format/time-zone';
|
|
3
|
+
import { useChatScope } from '../scope';
|
|
4
|
+
/* Only the id and the length. A store has no words, so the page names a period in its own language. */
|
|
5
|
+
export const USAGE_PERIODS = [
|
|
6
|
+
{ id: 'today', days: 1 },
|
|
7
|
+
{ id: '7d', days: 7 },
|
|
8
|
+
{ id: '30d', days: 30 },
|
|
9
|
+
{ id: '90d', days: 90 }
|
|
10
|
+
];
|
|
11
|
+
const STORAGE_KEY = 'ruimte.usage';
|
|
12
|
+
const DEFAULTS = { period: '7d', metric: 'cost', currency: 'USD' };
|
|
13
|
+
const readPreferences = () => {
|
|
14
|
+
try {
|
|
15
|
+
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}');
|
|
16
|
+
return {
|
|
17
|
+
period: USAGE_PERIODS.some((period) => period.id === stored.period) ? stored.period : DEFAULTS.period,
|
|
18
|
+
metric: stored.metric === 'tokens' || stored.metric === 'cost' ? stored.metric : DEFAULTS.metric,
|
|
19
|
+
currency: stored.currency === 'EUR' ? 'EUR' : DEFAULTS.currency
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return DEFAULTS;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const persist = (preferences) => {
|
|
27
|
+
try {
|
|
28
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Storage that refuses keeps the choice for this session only.
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
/* The three stored choices out of the store, so a setter writes the other two back unchanged. */
|
|
35
|
+
const chosen = (state) => ({ period: state.period, metric: state.metric, currency: state.currency });
|
|
36
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
37
|
+
/* The viewer's own calendar day; the host may stand in another zone and buckets in this one. */
|
|
38
|
+
export const dayOf = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
39
|
+
/*
|
|
40
|
+
* What a period asks for. Today counts from local midnight in hours, the way the commit log already
|
|
41
|
+
* groups days, so every other period reads as one bar per calendar day.
|
|
42
|
+
*/
|
|
43
|
+
export const windowFor = (period, now = new Date()) => {
|
|
44
|
+
const days = USAGE_PERIODS.find((entry) => entry.id === period)?.days ?? 7;
|
|
45
|
+
const from = new Date(now.getFullYear(), now.getMonth(), now.getDate() - (days - 1));
|
|
46
|
+
return {
|
|
47
|
+
from: dayOf(from),
|
|
48
|
+
to: dayOf(now),
|
|
49
|
+
resolution: period === 'today' ? 'hour' : 'day',
|
|
50
|
+
timeZone: localTimeZone() ?? 'UTC'
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
/* A period, and only the usage of one account when one is picked; null is every account. */
|
|
54
|
+
export const summaryPayload = (period, account, now = new Date()) => account === null ? windowFor(period, now) : { ...windowFor(period, now), accounts: [account] };
|
|
55
|
+
/* What was asked, so an answer to the question before this one is never drawn over a newer one. */
|
|
56
|
+
export const askedKey = (payload) => `${payload.from}\0${payload.to}\0${payload.resolution}\0${payload.timeZone}\0${payload.accounts?.join(',') ?? ''}`;
|
|
57
|
+
/* One object for a host nothing was read from yet, so a selector gets a stable snapshot. */
|
|
58
|
+
const EMPTY = { asked: null, summary: null, loading: false, failed: false, limits: null };
|
|
59
|
+
export const useUsageStore = create((set, get) => ({
|
|
60
|
+
...readPreferences(),
|
|
61
|
+
byScope: {},
|
|
62
|
+
chosen: null,
|
|
63
|
+
choose(scopeId) {
|
|
64
|
+
set({ chosen: scopeId });
|
|
65
|
+
},
|
|
66
|
+
setPeriod(period) {
|
|
67
|
+
persist({ ...chosen(get()), period });
|
|
68
|
+
set({ period });
|
|
69
|
+
},
|
|
70
|
+
setMetric(metric) {
|
|
71
|
+
persist({ ...chosen(get()), metric });
|
|
72
|
+
set({ metric });
|
|
73
|
+
},
|
|
74
|
+
setCurrency(currency) {
|
|
75
|
+
persist({ ...chosen(get()), currency });
|
|
76
|
+
set({ currency });
|
|
77
|
+
},
|
|
78
|
+
setLoading(scopeId, loading) {
|
|
79
|
+
set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), loading } } });
|
|
80
|
+
},
|
|
81
|
+
receive(scopeId, asked, summary) {
|
|
82
|
+
set({
|
|
83
|
+
byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), asked, summary, loading: false, failed: false } }
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
fail(scopeId) {
|
|
87
|
+
set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), loading: false, failed: true } } });
|
|
88
|
+
},
|
|
89
|
+
setLimits(scopeId, limits) {
|
|
90
|
+
set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), limits } } });
|
|
91
|
+
},
|
|
92
|
+
forget(scopeId) {
|
|
93
|
+
const { [scopeId]: _gone, ...rest } = get().byScope;
|
|
94
|
+
set({ byScope: rest });
|
|
95
|
+
}
|
|
96
|
+
}));
|
|
97
|
+
export const useUsage = (select) => {
|
|
98
|
+
const { id } = useChatScope();
|
|
99
|
+
return useUsageStore((s) => select({ period: s.period, metric: s.metric, currency: s.currency, ...(s.byScope[id] ?? EMPTY) }));
|
|
100
|
+
};
|