@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,104 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { checkAttachmentLimits, uploadBytes } from './attachments';
|
|
3
|
+
import { persistedJson } from './persisted-json';
|
|
4
|
+
const STORAGE_KEY = 'ruimte.chat.drafts';
|
|
5
|
+
export const EMPTY_DRAFT = { text: '', mentions: [], skills: [], chats: [], attachments: [], quote: '' };
|
|
6
|
+
const storage = persistedJson(STORAGE_KEY, (raw) => (raw ? JSON.parse(raw) : {}), {});
|
|
7
|
+
const readAll = () => storage.read();
|
|
8
|
+
const store = (drafts) => storage.write(drafts);
|
|
9
|
+
export const isEmptyDraft = (draft) => draft.text.trim() === '' && draft.attachments.length === 0 && draft.quote === '';
|
|
10
|
+
/*
|
|
11
|
+
* Which chats hold an unsent prompt, for a dot beside them in a list of chats. The drafts themselves
|
|
12
|
+
* stay in storage: a node that is not on screen has no composer, and this is all anyone else needs.
|
|
13
|
+
*/
|
|
14
|
+
export const useDrafts = create(() => ({ ids: Object.keys(readAll()) }));
|
|
15
|
+
export const useHasDraft = (chatId) => useDrafts((s) => s.ids.includes(chatId));
|
|
16
|
+
const trackDraft = (chatId, held) => {
|
|
17
|
+
const { ids } = useDrafts.getState();
|
|
18
|
+
if (ids.includes(chatId) === held) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
useDrafts.setState({ ids: held ? [...ids, chatId] : ids.filter((id) => id !== chatId) });
|
|
22
|
+
};
|
|
23
|
+
/* An unsent prompt per chat node, kept across reloads so a half-written message is never lost. */
|
|
24
|
+
export const readDraft = (chatId) => {
|
|
25
|
+
const record = readAll()[chatId];
|
|
26
|
+
return record
|
|
27
|
+
? {
|
|
28
|
+
text: record.text,
|
|
29
|
+
mentions: record.mentions ?? [],
|
|
30
|
+
skills: record.skills ?? [],
|
|
31
|
+
chats: record.chats ?? [],
|
|
32
|
+
attachments: record.attachments ?? [],
|
|
33
|
+
quote: record.quote ?? ''
|
|
34
|
+
}
|
|
35
|
+
: EMPTY_DRAFT;
|
|
36
|
+
};
|
|
37
|
+
export const writeDraft = (chatId, draft) => {
|
|
38
|
+
const drafts = readAll();
|
|
39
|
+
trackDraft(chatId, !isEmptyDraft(draft));
|
|
40
|
+
if (isEmptyDraft(draft)) {
|
|
41
|
+
delete drafts[chatId];
|
|
42
|
+
store(drafts);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
drafts[chatId] = {
|
|
46
|
+
text: draft.text,
|
|
47
|
+
mentions: draft.mentions,
|
|
48
|
+
skills: draft.skills,
|
|
49
|
+
chats: draft.chats,
|
|
50
|
+
attachments: draft.attachments,
|
|
51
|
+
quote: draft.quote
|
|
52
|
+
};
|
|
53
|
+
if (store(drafts)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// A file can outgrow the storage quota; the text is the part worth keeping then.
|
|
57
|
+
drafts[chatId] = { text: draft.text, mentions: draft.mentions, skills: draft.skills, chats: draft.chats, quote: draft.quote };
|
|
58
|
+
store(drafts);
|
|
59
|
+
};
|
|
60
|
+
/* Text handed to a draft from outside the composer goes under what was already typed, never over it. */
|
|
61
|
+
export const joinDraftText = (current, added) => (current.trim() === '' ? added : `${current.replace(/\s+$/, '')}\n\n${added}`);
|
|
62
|
+
const union = (first, second) => [...first, ...second.filter((entry) => !first.includes(entry))];
|
|
63
|
+
/*
|
|
64
|
+
* A queued message taken back to edit goes above what was typed since. Its files join the draft's as
|
|
65
|
+
* far as the limits let them, and the rest come back rejected, the way a dropped file would. A quote
|
|
66
|
+
* it was sent with is part of its text; the draft keeps the one it holds.
|
|
67
|
+
*/
|
|
68
|
+
export const takeBackIntoDraft = (current, taken) => {
|
|
69
|
+
const checked = checkAttachmentLimits(current.attachments.length, taken.attachments.map((upload) => ({ name: upload.name, mime: upload.mime, bytes: uploadBytes(upload), upload })), current.attachments.reduce((bytes, upload) => bytes + uploadBytes(upload), 0));
|
|
70
|
+
return {
|
|
71
|
+
draft: {
|
|
72
|
+
text: current.text.trim() === '' ? taken.text : joinDraftText(taken.text, current.text),
|
|
73
|
+
mentions: union(taken.mentions, current.mentions),
|
|
74
|
+
skills: union(taken.skills, current.skills),
|
|
75
|
+
chats: union(taken.chats, current.chats),
|
|
76
|
+
attachments: [...checked.accepted.map((entry) => entry.upload), ...current.attachments],
|
|
77
|
+
quote: current.quote
|
|
78
|
+
},
|
|
79
|
+
rejected: checked.rejected
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
/* The composers on screen, by chat. One that is not mounted reads what was offered from storage when it is. */
|
|
83
|
+
const takers = new Map();
|
|
84
|
+
export const takeDraftOffers = (chatId, take) => {
|
|
85
|
+
takers.set(chatId, take);
|
|
86
|
+
return () => {
|
|
87
|
+
if (takers.get(chatId) === take) {
|
|
88
|
+
takers.delete(chatId);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
/*
|
|
93
|
+
* Puts text in a chat's prompt without sending it: the person reads it and presses Enter. A mounted
|
|
94
|
+
* composer owns its draft and writes it back to storage itself, so it has to be the one to take it.
|
|
95
|
+
*/
|
|
96
|
+
export const offerDraft = (chatId, text) => {
|
|
97
|
+
const take = takers.get(chatId);
|
|
98
|
+
if (take) {
|
|
99
|
+
take(text);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const current = readDraft(chatId);
|
|
103
|
+
writeDraft(chatId, { ...current, text: joinDraftText(current.text, text) });
|
|
104
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { forkIdsAfter, forkedTurnIds } from './logic/fork';
|
|
3
|
+
import { useChatScope } from '../scope';
|
|
4
|
+
import { useChats } from '../state/chats';
|
|
5
|
+
/* The chats this client holds of one host, which is where the forks it knows of are. */
|
|
6
|
+
function* forkInfosOn(byKey, owns) {
|
|
7
|
+
for (const [key, state] of Object.entries(byKey)) {
|
|
8
|
+
if (state.info.forkOf !== undefined && owns(key)) {
|
|
9
|
+
yield state.info;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/* Selectors answer a joined string, never a new array or set, and split it again in a memo. */
|
|
14
|
+
const splitIds = (joined) => (joined === '' ? [] : joined.split('\n'));
|
|
15
|
+
/* The turns of this chat a fork went on after, for the thread to put a line under. */
|
|
16
|
+
export const useForkedTurns = (chatId) => {
|
|
17
|
+
const { owns } = useChatScope();
|
|
18
|
+
const joined = useChats((s) => [...forkedTurnIds(forkInfosOn(s.byKey, owns), chatId)].join('\n'));
|
|
19
|
+
return useMemo(() => new Set(splitIds(joined)), [joined]);
|
|
20
|
+
};
|
|
21
|
+
/* The forks that went on after this turn of this chat. */
|
|
22
|
+
export const useForkIdsAfter = (chatId, turnId) => {
|
|
23
|
+
const { owns } = useChatScope();
|
|
24
|
+
const joined = useChats((s) => forkIdsAfter(forkInfosOn(s.byKey, owns), chatId, turnId).join('\n'));
|
|
25
|
+
return useMemo(() => splitIds(joined), [joined]);
|
|
26
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const PROMPT_MAX_CHARS = 120000;
|
|
2
|
+
export declare const PROMPT_COUNTER_FROM = 100000;
|
|
3
|
+
export declare const PASTE_ATTACHMENT_FROM_BYTES: number;
|
|
4
|
+
export declare const pasteBecomesAttachment: (text: string) => boolean;
|
|
5
|
+
export declare const pastedTextName: (taken: string[]) => string;
|
|
6
|
+
export interface PromptGuard {
|
|
7
|
+
count: number;
|
|
8
|
+
visible: boolean;
|
|
9
|
+
tooLong: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const promptGuard: (text: string) => PromptGuard;
|
|
12
|
+
export declare const usableSlashCommands: (names: string[]) => string[];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// What a CLI still accepts without the turn becoming slower than the answer is worth; a Claude user
|
|
2
|
+
// frame this large is legal but sluggish.
|
|
3
|
+
export const PROMPT_MAX_CHARS = 120000;
|
|
4
|
+
// Below this the counter would only be noise; a prompt this long is already unusual.
|
|
5
|
+
export const PROMPT_COUNTER_FROM = 100000;
|
|
6
|
+
/*
|
|
7
|
+
* From this many bytes a paste of text becomes a file beside the prompt instead of text in it. A log
|
|
8
|
+
* or a file that large is something to read with a tool, not words for the model to take in on
|
|
9
|
+
* every turn, and in the textarea it buries what the person actually wants to say.
|
|
10
|
+
*/
|
|
11
|
+
export const PASTE_ATTACHMENT_FROM_BYTES = 32 * 1024;
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
/* Whether pasted text is large enough to go in as an attachment. Bytes, since that is what the file weighs. */
|
|
14
|
+
export const pasteBecomesAttachment = (text) => text.length * 3 >= PASTE_ATTACHMENT_FROM_BYTES && encoder.encode(text).length >= PASTE_ATTACHMENT_FROM_BYTES;
|
|
15
|
+
/* A name for pasted text that no attachment of the draft has yet: paste-1.txt, paste-2.txt, ... */
|
|
16
|
+
export const pastedTextName = (taken) => {
|
|
17
|
+
const names = new Set(taken);
|
|
18
|
+
let index = 1;
|
|
19
|
+
while (names.has(`paste-${index}.txt`)) {
|
|
20
|
+
index += 1;
|
|
21
|
+
}
|
|
22
|
+
return `paste-${index}.txt`;
|
|
23
|
+
};
|
|
24
|
+
export const promptGuard = (text) => ({
|
|
25
|
+
count: text.length,
|
|
26
|
+
visible: text.length >= PROMPT_COUNTER_FROM,
|
|
27
|
+
tooLong: text.length > PROMPT_MAX_CHARS
|
|
28
|
+
});
|
|
29
|
+
/*
|
|
30
|
+
* Commands the CLI lists because its own terminal has them: they change the terminal, the login or
|
|
31
|
+
* the config, or they end a session the chat node does not own.
|
|
32
|
+
*/
|
|
33
|
+
const TUI_ONLY = new Set([
|
|
34
|
+
'bug',
|
|
35
|
+
'color',
|
|
36
|
+
'config',
|
|
37
|
+
'doctor',
|
|
38
|
+
'exit',
|
|
39
|
+
'heapdump',
|
|
40
|
+
'help',
|
|
41
|
+
'hooks',
|
|
42
|
+
'ide',
|
|
43
|
+
'install-github-app',
|
|
44
|
+
'keybindings',
|
|
45
|
+
'login',
|
|
46
|
+
'logout',
|
|
47
|
+
'migrate-installer',
|
|
48
|
+
'privacy-settings',
|
|
49
|
+
'quit',
|
|
50
|
+
'release-notes',
|
|
51
|
+
'reload-plugins',
|
|
52
|
+
'reload-skills',
|
|
53
|
+
'rename',
|
|
54
|
+
'resume',
|
|
55
|
+
'status',
|
|
56
|
+
'statusline',
|
|
57
|
+
'terminal-setup',
|
|
58
|
+
'theme',
|
|
59
|
+
'upgrade',
|
|
60
|
+
'vim'
|
|
61
|
+
]);
|
|
62
|
+
/* The commands of the CLI's list that mean something in a chat node. */
|
|
63
|
+
export const usableSlashCommands = (names) => names.filter((name) => !TUI_ONLY.has(name));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/* The separator between several choices of one question, once they are one string. */
|
|
2
|
+
const SEPARATOR = ', ';
|
|
3
|
+
/*
|
|
4
|
+
* Add or remove one choice of a multi select question. The picks stay a list here because a label
|
|
5
|
+
* may hold the separator itself: joined too early, such a label no longer matches itself and the
|
|
6
|
+
* choice would read as unpicked on screen.
|
|
7
|
+
*/
|
|
8
|
+
export function toggleChoice(picked, label) {
|
|
9
|
+
return picked.includes(label) ? picked.filter((choice) => choice !== label) : [...picked, label];
|
|
10
|
+
}
|
|
11
|
+
/* The wire takes one string per question, so several choices travel as one line. */
|
|
12
|
+
export function answersToWire(picked) {
|
|
13
|
+
return Object.fromEntries(Object.entries(picked).map(([id, labels]) => [id, labels.join(SEPARATOR)]));
|
|
14
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ChatBookmark, ChatItem } from '@ruimte/agent-contracts';
|
|
2
|
+
import type { TimelineRow } from './timeline';
|
|
3
|
+
export declare const bookmarkLabel: (bookmark: ChatBookmark) => string;
|
|
4
|
+
export declare const bookmarksInThreadOrder: (bookmarks: readonly ChatBookmark[], order: readonly string[]) => ChatBookmark[];
|
|
5
|
+
export declare const bookmarkRows: (rows: readonly TimelineRow[], bookmarks: readonly ChatBookmark[], items: Readonly<Record<string, ChatItem>>) => Map<string, ChatBookmark>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* What a list calls a bookmark: its name, else the start of the message. */
|
|
2
|
+
export const bookmarkLabel = (bookmark) => bookmark.name ?? bookmark.excerpt;
|
|
3
|
+
/* The bookmarks in the order of the thread; one whose message this client does not hold goes last. */
|
|
4
|
+
export const bookmarksInThreadOrder = (bookmarks, order) => {
|
|
5
|
+
const places = new Map(order.map((id, index) => [id, index]));
|
|
6
|
+
const place = (bookmark) => places.get(bookmark.itemId) ?? order.length;
|
|
7
|
+
return [...bookmarks].sort((left, right) => place(left) - place(right) || left.createdAt - right.createdAt);
|
|
8
|
+
};
|
|
9
|
+
/*
|
|
10
|
+
* Which row each bookmark is drawn at, by row id: the message's own row, or the fold of a settled
|
|
11
|
+
* turn that hides a reply before its last one. A bookmark on a message this client does not hold
|
|
12
|
+
* has no row.
|
|
13
|
+
*/
|
|
14
|
+
export const bookmarkRows = (rows, bookmarks, items) => {
|
|
15
|
+
const rowIds = new Set(rows.map((row) => row.id));
|
|
16
|
+
const folds = new Map();
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
if (row.kind === 'turn-fold') {
|
|
19
|
+
folds.set(row.turn.id, row.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const marked = new Map();
|
|
23
|
+
for (const bookmark of bookmarks) {
|
|
24
|
+
const turnId = items[bookmark.itemId]?.turnId ?? null;
|
|
25
|
+
const rowId = rowIds.has(bookmark.itemId) ? bookmark.itemId : turnId === null ? undefined : folds.get(turnId);
|
|
26
|
+
if (rowId !== undefined && !marked.has(rowId)) {
|
|
27
|
+
marked.set(rowId, bookmark);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return marked;
|
|
31
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ChatContextBreakdown, ChatUsage, ModelOptionDescriptor } from '@ruimte/agent-contracts';
|
|
2
|
+
export declare const CONTEXT_OPTION = "contextWindow";
|
|
3
|
+
export type ContextPart = keyof ChatContextBreakdown;
|
|
4
|
+
export declare const CONTEXT_PARTS: readonly ContextPart[];
|
|
5
|
+
export interface ContextSegment {
|
|
6
|
+
part: ContextPart;
|
|
7
|
+
tokens: number;
|
|
8
|
+
fraction: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const contextFraction: (usage: ChatUsage) => number;
|
|
11
|
+
export declare const contextSegments: (usage: ChatUsage) => ContextSegment[] | null;
|
|
12
|
+
export declare const orderOptions: (options: readonly ModelOptionDescriptor[]) => ModelOptionDescriptor[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const CONTEXT_OPTION = 'contextWindow';
|
|
2
|
+
export const CONTEXT_PARTS = ['toolOutput', 'filesRead', 'conversation', 'system'];
|
|
3
|
+
export const contextFraction = (usage) => (usage.contextWindow ? Math.min(1, usage.contextTokens / usage.contextWindow) : 0);
|
|
4
|
+
/* The stretches of the bar, in the legend's order; null from a host that does not estimate, which draws the plain bar. */
|
|
5
|
+
export const contextSegments = (usage) => {
|
|
6
|
+
const breakdown = usage.breakdown;
|
|
7
|
+
if (!breakdown || usage.contextTokens === 0) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const whole = Math.max(usage.contextWindow ?? 0, usage.contextTokens);
|
|
11
|
+
return CONTEXT_PARTS.map((part) => ({ part, tokens: breakdown[part], fraction: breakdown[part] / whole }));
|
|
12
|
+
};
|
|
13
|
+
/*
|
|
14
|
+
* The model's own knobs with the context window last: its details sit in the group right under it,
|
|
15
|
+
* and a switch like Fast mode belongs with the reasoning choice above rather than below the size.
|
|
16
|
+
*/
|
|
17
|
+
export const orderOptions = (options) => [
|
|
18
|
+
...options.filter((option) => option.id !== CONTEXT_OPTION),
|
|
19
|
+
...options.filter((option) => option.id === CONTEXT_OPTION)
|
|
20
|
+
];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AgentKind, ChatForkPayload, ChatInfo, ChatItem, ModelSelection } from '@ruimte/agent-contracts';
|
|
2
|
+
import type { TimelineRow } from './timeline';
|
|
3
|
+
export declare const FORKABLE_PROVIDERS: ReadonlySet<string>;
|
|
4
|
+
export interface ForkPoint {
|
|
5
|
+
turnId: string;
|
|
6
|
+
number: number;
|
|
7
|
+
total: number;
|
|
8
|
+
last: boolean;
|
|
9
|
+
prompt: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare const forkRefusal: (info: ChatInfo | null, turn: ChatItem | undefined) => string | null;
|
|
12
|
+
export declare const summaryRefusal: (state: {
|
|
13
|
+
busy: boolean;
|
|
14
|
+
originalPresent: boolean;
|
|
15
|
+
}) => string | null;
|
|
16
|
+
export declare const forkIdsAfter: (infos: Iterable<ChatInfo>, chatId: string, turnId: string) => string[];
|
|
17
|
+
export declare const forkedTurnIds: (infos: Iterable<ChatInfo>, chatId: string) => Set<string>;
|
|
18
|
+
export declare const lastSettledTurn: (items: Record<string, ChatItem>, order: readonly string[]) => string | null;
|
|
19
|
+
export declare const forkPointOf: (items: Record<string, ChatItem>, order: readonly string[], turnId: string) => ForkPoint | null;
|
|
20
|
+
export declare const forkPointLabel: (point: ForkPoint, maxPrompt?: number) => string;
|
|
21
|
+
export declare const turnIdOfRow: (row: TimelineRow) => string | null;
|
|
22
|
+
export type ForkShape = 'node' | 'view';
|
|
23
|
+
export declare const forkShapes: (origin: ForkShape) => ForkShape[];
|
|
24
|
+
export interface ForkWorktreeChoice {
|
|
25
|
+
branch: string;
|
|
26
|
+
filesAfterTurn: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface ForkCliChoice {
|
|
29
|
+
provider: AgentKind;
|
|
30
|
+
selection: ModelSelection;
|
|
31
|
+
account?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare const forkPayload: (input: {
|
|
34
|
+
chatId: string;
|
|
35
|
+
turnId: string;
|
|
36
|
+
title: string;
|
|
37
|
+
shape: ForkShape;
|
|
38
|
+
worktree?: ForkWorktreeChoice | null;
|
|
39
|
+
cli?: {
|
|
40
|
+
original: ForkCliChoice;
|
|
41
|
+
chosen: ForkCliChoice;
|
|
42
|
+
};
|
|
43
|
+
}) => ChatForkPayload;
|
|
44
|
+
export declare const branchRefusal: (branch: string, taken: readonly string[]) => string | null;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
/* The providers whose chat a machine can fork, and go on with in a fork, are the ones with a conversation the CLI keeps. */
|
|
3
|
+
export const FORKABLE_PROVIDERS = new Set(['claude', 'codex']);
|
|
4
|
+
const turnsOf = (items, order) => order.flatMap((id) => {
|
|
5
|
+
const item = items[id];
|
|
6
|
+
return item?.kind === 'turn' ? [item] : [];
|
|
7
|
+
});
|
|
8
|
+
/*
|
|
9
|
+
* Why a chat cannot be forked after this turn right now, or null when it can. A turn still running,
|
|
10
|
+
* or any turn in the chat, is a transcript that grows while it would be copied.
|
|
11
|
+
*/
|
|
12
|
+
export const forkRefusal = (info, turn) => {
|
|
13
|
+
if (info === null || turn?.kind !== 'turn') {
|
|
14
|
+
return i18next.t('agent-chat:fork.refusal.notInConversation');
|
|
15
|
+
}
|
|
16
|
+
if (!FORKABLE_PROVIDERS.has(info.provider)) {
|
|
17
|
+
return i18next.t('agent-chat:fork.refusal.cliCannotFork');
|
|
18
|
+
}
|
|
19
|
+
if (info.agentSessionId === null) {
|
|
20
|
+
return i18next.t('agent-chat:fork.refusal.noConversation');
|
|
21
|
+
}
|
|
22
|
+
if (turn.state === 'running' || info.activeTurnId !== null) {
|
|
23
|
+
return i18next.t('agent-chat:fork.refusal.turnRunning');
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
};
|
|
27
|
+
/* Why a fork cannot write a summary for its original right now, or null when it can. */
|
|
28
|
+
export const summaryRefusal = (state) => {
|
|
29
|
+
if (!state.originalPresent) {
|
|
30
|
+
return i18next.t('agent-chat:fork.refusal.originalGone');
|
|
31
|
+
}
|
|
32
|
+
if (state.busy) {
|
|
33
|
+
return i18next.t('agent-chat:fork.refusal.turnRunning');
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
/* The forks this client knows of that went on after this turn of this chat, in the order it holds them. */
|
|
38
|
+
export const forkIdsAfter = (infos, chatId, turnId) => {
|
|
39
|
+
const ids = [];
|
|
40
|
+
for (const info of infos) {
|
|
41
|
+
if (info.forkOf?.chatId === chatId && info.forkOf.turnId === turnId) {
|
|
42
|
+
ids.push(info.chatId);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return ids;
|
|
46
|
+
};
|
|
47
|
+
/* The turns of this chat that at least one fork this client knows of went on after. */
|
|
48
|
+
export const forkedTurnIds = (infos, chatId) => {
|
|
49
|
+
const turns = new Set();
|
|
50
|
+
for (const info of infos) {
|
|
51
|
+
if (info.forkOf?.chatId === chatId) {
|
|
52
|
+
turns.add(info.forkOf.turnId);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return turns;
|
|
56
|
+
};
|
|
57
|
+
/* The last turn that ended, which is where a fork from the node's menu goes on after. */
|
|
58
|
+
export const lastSettledTurn = (items, order) => turnsOf(items, order).findLast((turn) => turn.state !== 'running')?.id ?? null;
|
|
59
|
+
export const forkPointOf = (items, order, turnId) => {
|
|
60
|
+
const turns = turnsOf(items, order);
|
|
61
|
+
const index = turns.findIndex((turn) => turn.id === turnId);
|
|
62
|
+
const turn = turns[index];
|
|
63
|
+
if (turn === undefined) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const asked = order.map((id) => items[id]).find((item) => item?.kind === 'user' && item.turnId === turnId);
|
|
67
|
+
const text = asked?.kind === 'user' ? asked.text : (turn.label ?? '');
|
|
68
|
+
const prompt = text
|
|
69
|
+
.split('\n')
|
|
70
|
+
.find((line) => line.trim() !== '')
|
|
71
|
+
?.trim() ?? null;
|
|
72
|
+
return { turnId, number: index + 1, total: turns.length, last: index === turns.length - 1, prompt };
|
|
73
|
+
};
|
|
74
|
+
/* The line at the top of the dialog: which turn, and what it was about. */
|
|
75
|
+
export const forkPointLabel = (point, maxPrompt = 60) => {
|
|
76
|
+
const where = point.last
|
|
77
|
+
? i18next.t('agent-chat:fork.point.afterLast')
|
|
78
|
+
: i18next.t('agent-chat:fork.point.afterTurn', { number: point.number, total: point.total });
|
|
79
|
+
if (point.prompt === null) {
|
|
80
|
+
return where;
|
|
81
|
+
}
|
|
82
|
+
const prompt = point.prompt.length > maxPrompt ? `${point.prompt.slice(0, maxPrompt - 1)}…` : point.prompt;
|
|
83
|
+
return i18next.t('agent-chat:fork.point.about', { where, prompt });
|
|
84
|
+
};
|
|
85
|
+
/* The turn a row of the thread belongs to, which is the turn "Fork from here" on that row goes on after. */
|
|
86
|
+
export const turnIdOfRow = (row) => {
|
|
87
|
+
switch (row.kind) {
|
|
88
|
+
case 'turn-start':
|
|
89
|
+
case 'turn-fold':
|
|
90
|
+
return row.turn.id;
|
|
91
|
+
case 'changed-files':
|
|
92
|
+
case 'forks':
|
|
93
|
+
return row.turnId;
|
|
94
|
+
case 'work':
|
|
95
|
+
case 'work-live':
|
|
96
|
+
case 'workflow':
|
|
97
|
+
return row.tool.turnId;
|
|
98
|
+
case 'work-group':
|
|
99
|
+
return row.tools[0]?.turnId ?? null;
|
|
100
|
+
case 'user':
|
|
101
|
+
case 'assistant':
|
|
102
|
+
case 'thinking':
|
|
103
|
+
case 'subagent':
|
|
104
|
+
case 'approval':
|
|
105
|
+
case 'question':
|
|
106
|
+
return row.item.turnId;
|
|
107
|
+
default:
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/* What the dialog offers, first the default: a view forks into a view, and a node into a node or, when asked, a view. */
|
|
112
|
+
export const forkShapes = (origin) => (origin === 'view' ? ['view'] : ['node', 'view']);
|
|
113
|
+
export const forkPayload = (input) => ({
|
|
114
|
+
chatId: input.chatId,
|
|
115
|
+
turnId: input.turnId,
|
|
116
|
+
title: input.title,
|
|
117
|
+
...(input.shape === 'view' ? { asView: true } : {}),
|
|
118
|
+
...(input.cli && input.cli.chosen.provider !== input.cli.original.provider ? { provider: input.cli.chosen.provider } : {}),
|
|
119
|
+
...(input.cli && (input.cli.chosen.provider !== input.cli.original.provider || input.cli.chosen.selection.model !== input.cli.original.selection.model)
|
|
120
|
+
? { selection: input.cli.chosen.selection }
|
|
121
|
+
: {}),
|
|
122
|
+
...(input.cli?.chosen.account !== undefined &&
|
|
123
|
+
(input.cli.chosen.provider !== input.cli.original.provider || input.cli.chosen.account !== input.cli.original.account)
|
|
124
|
+
? { account: input.cli.chosen.account }
|
|
125
|
+
: {}),
|
|
126
|
+
...(input.worktree ? { worktree: { branch: input.worktree.branch }, ...(input.worktree.filesAfterTurn ? { filesAfterTurn: true } : {}) } : {})
|
|
127
|
+
});
|
|
128
|
+
/* Why a branch cannot be the fork's, or null when it can. */
|
|
129
|
+
export const branchRefusal = (branch, taken) => {
|
|
130
|
+
const name = branch.trim();
|
|
131
|
+
if (name === '') {
|
|
132
|
+
return i18next.t('agent-chat:fork.branch.unnamed');
|
|
133
|
+
}
|
|
134
|
+
if (taken.includes(name)) {
|
|
135
|
+
return i18next.t('agent-chat:fork.branch.taken');
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* The report a `SubagentHandback` call carries in `input.message` (Claude Code 2.1.273), or null for any other item. */
|
|
2
|
+
export const handbackReportOf = (item) => {
|
|
3
|
+
if (item.kind !== 'tool' || item.name !== 'SubagentHandback' || typeof item.input !== 'object' || item.input === null) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const message = item.input.message;
|
|
7
|
+
return typeof message === 'string' && message.trim() !== '' ? message : null;
|
|
8
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChatInfo } from '@ruimte/agent-contracts';
|
|
2
|
+
export interface LimitView {
|
|
3
|
+
pill: string;
|
|
4
|
+
title: string;
|
|
5
|
+
detail: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare const limitView: (info: Pick<ChatInfo, 'limit' | 'resumeAt' | 'activeTurnId'>, now: number) => LimitView | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
import { formatMoment } from '@ruimte/ui/format/datetime';
|
|
3
|
+
/*
|
|
4
|
+
* What a chat that stopped on a limit says about it, from its info alone, so a header that never holds
|
|
5
|
+
* the thread says the same as the chat. Null while the last turn stopped on nothing, or a turn runs.
|
|
6
|
+
*/
|
|
7
|
+
export const limitView = (info, now) => {
|
|
8
|
+
const limit = info.limit;
|
|
9
|
+
if (limit === undefined || info.activeTurnId !== null) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const resumes = info.resumeAt === undefined ? null : formatMoment(info.resumeAt, now);
|
|
13
|
+
if (limit.kind === 'overload') {
|
|
14
|
+
return {
|
|
15
|
+
pill: resumes === null ? i18next.t('agent-chat:limit.overload.pill') : i18next.t('agent-chat:limit.overload.pillRetry', { time: resumes }),
|
|
16
|
+
title: i18next.t('agent-chat:limit.overload.title'),
|
|
17
|
+
detail: resumes === null ? i18next.t('agent-chat:limit.overload.sendAgain') : i18next.t('agent-chat:limit.overload.retries', { time: resumes })
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const resets = limit.resetsAt === undefined ? null : formatMoment(limit.resetsAt, now);
|
|
21
|
+
return {
|
|
22
|
+
pill: resumes !== null
|
|
23
|
+
? i18next.t('agent-chat:limit.usage.pillResumes', { time: resumes })
|
|
24
|
+
: resets !== null
|
|
25
|
+
? i18next.t('agent-chat:limit.usage.pillUntil', { time: resets })
|
|
26
|
+
: i18next.t('agent-chat:limit.usage.pill'),
|
|
27
|
+
title: i18next.t('agent-chat:limit.usage.title'),
|
|
28
|
+
detail: resumes !== null
|
|
29
|
+
? i18next.t('agent-chat:limit.usage.resumes', { time: resumes })
|
|
30
|
+
: resets !== null
|
|
31
|
+
? i18next.t('agent-chat:limit.usage.resets', { time: resets })
|
|
32
|
+
: null
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChatInfo, ChatItem, ProviderCapabilities } from '@ruimte/agent-contracts';
|
|
2
|
+
export declare const RESUME_COMPACTION_TOKENS = 100000;
|
|
3
|
+
export declare const RESUME_COMPACTION_IDLE_MS: number;
|
|
4
|
+
export interface ResumeCompactionOffer {
|
|
5
|
+
turnId: string;
|
|
6
|
+
tokens: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const resumeCompactionOffer: ({ info, compaction, items, dismissedTurnId, now }: {
|
|
9
|
+
info: ChatInfo;
|
|
10
|
+
compaction: ProviderCapabilities['compaction'] | undefined;
|
|
11
|
+
items: readonly ChatItem[];
|
|
12
|
+
dismissedTurnId: string | null;
|
|
13
|
+
now: number;
|
|
14
|
+
}) => ResumeCompactionOffer | null;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* An absolute count, not a share of the window: reading a thread again costs per token, so 100k in a
|
|
3
|
+
* window of 1M is as expensive as 100k in a window of 200k.
|
|
4
|
+
*/
|
|
5
|
+
export const RESUME_COMPACTION_TOKENS = 100_000;
|
|
6
|
+
/* The longest prompt cache Claude keeps is an hour, so past this the whole thread is read again anyway. */
|
|
7
|
+
export const RESUME_COMPACTION_IDLE_MS = 70 * 60_000;
|
|
8
|
+
const lastTurn = (items) => {
|
|
9
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
10
|
+
const item = items[i];
|
|
11
|
+
if (item.kind === 'turn') {
|
|
12
|
+
return item;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
};
|
|
17
|
+
/*
|
|
18
|
+
* Whether to offer compacting before the next message. The caller passes `now`, so this stays pure
|
|
19
|
+
* and the test needs no clock.
|
|
20
|
+
*/
|
|
21
|
+
export const resumeCompactionOffer = ({ info, compaction, items, dismissedTurnId, now }) => {
|
|
22
|
+
if (compaction === undefined || compaction === 'none') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (info.activeTurnId !== null || (info.queue ?? []).length > 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (info.usage.contextTokens < RESUME_COMPACTION_TOKENS) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const turn = lastTurn(items);
|
|
32
|
+
if (turn === null || turn.endedAt === null || turn.id === dismissedTurnId) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (now - turn.endedAt < RESUME_COMPACTION_IDLE_MS) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
/*
|
|
39
|
+
* `contextTokens` keeps the old number until the next request, so without this the offer would
|
|
40
|
+
* come straight back after a compaction. A native compaction is a turn without a message of the
|
|
41
|
+
* person, which is why this looks at the turn and not at the last thing that was said.
|
|
42
|
+
*/
|
|
43
|
+
if (items.some((item) => item.kind === 'compaction' && item.turnId === turn.id)) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return { turnId: turn.id, tokens: info.usage.contextTokens };
|
|
47
|
+
};
|