@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,150 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
import { errorCode, isConnectionError } from '../transport';
|
|
3
|
+
// What the panel opens on: the newest end of the conversation, which is where an agent that works is.
|
|
4
|
+
export const SUBAGENT_PAGE = 60;
|
|
5
|
+
export const INITIAL_CONVERSATION = {
|
|
6
|
+
status: 'loading',
|
|
7
|
+
items: [],
|
|
8
|
+
cursor: null,
|
|
9
|
+
live: false,
|
|
10
|
+
loadingEarlier: false,
|
|
11
|
+
error: null,
|
|
12
|
+
unsupported: false
|
|
13
|
+
};
|
|
14
|
+
/*
|
|
15
|
+
* The newest page laid over what is on screen. An item that is already there is replaced where it
|
|
16
|
+
* stands and a new one goes after them all. A page that shares nothing with what is held means more
|
|
17
|
+
* happened than one page holds, so what is held is dropped for it rather than shown with a hole.
|
|
18
|
+
*/
|
|
19
|
+
export const mergeNewest = (current, currentCursor, page, pageCursor) => {
|
|
20
|
+
const known = new Map(current.map((item, index) => [item.id, index]));
|
|
21
|
+
if (current.length > 0 && page.length > 0 && !page.some((item) => known.has(item.id))) {
|
|
22
|
+
return { items: page, cursor: pageCursor };
|
|
23
|
+
}
|
|
24
|
+
const items = [...current];
|
|
25
|
+
for (const item of page) {
|
|
26
|
+
const index = known.get(item.id);
|
|
27
|
+
if (index === undefined) {
|
|
28
|
+
items.push(item);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
items[index] = item;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { items, cursor: current.length === 0 ? pageCursor : currentCursor };
|
|
35
|
+
};
|
|
36
|
+
const messageOf = (error) => (error instanceof Error ? error.message : i18next.t('agent-chat:subagents.noAnswer'));
|
|
37
|
+
/*
|
|
38
|
+
* One subagent's conversation as a panel follows it. Opening asks for the newest page and holds the
|
|
39
|
+
* conversation on the machine, which then says whenever it grew; each time, the newest page is asked
|
|
40
|
+
* again and laid over what is there. A socket that comes back holds it again, and letting go is the
|
|
41
|
+
* last thing it says.
|
|
42
|
+
*/
|
|
43
|
+
export class SubagentConversation {
|
|
44
|
+
transport;
|
|
45
|
+
chatId;
|
|
46
|
+
toolUseId;
|
|
47
|
+
onChange;
|
|
48
|
+
unsubscribe = [];
|
|
49
|
+
state = INITIAL_CONVERSATION;
|
|
50
|
+
disposed = false;
|
|
51
|
+
// One request at a time, so a refresh never lands under an older page that arrived after it.
|
|
52
|
+
queue = Promise.resolve();
|
|
53
|
+
refreshWaiting = false;
|
|
54
|
+
constructor(transport, chatId, toolUseId, onChange) {
|
|
55
|
+
this.transport = transport;
|
|
56
|
+
this.chatId = chatId;
|
|
57
|
+
this.toolUseId = toolUseId;
|
|
58
|
+
this.onChange = onChange;
|
|
59
|
+
}
|
|
60
|
+
get current() {
|
|
61
|
+
return this.state;
|
|
62
|
+
}
|
|
63
|
+
start() {
|
|
64
|
+
this.unsubscribe.push(this.transport.on('chat.subagentChanged', (event) => {
|
|
65
|
+
if (event.chatId === this.chatId && event.toolUseId === this.toolUseId) {
|
|
66
|
+
void this.refresh();
|
|
67
|
+
}
|
|
68
|
+
}), this.transport.subscribeStatus((status) => {
|
|
69
|
+
if (status === 'open') {
|
|
70
|
+
void this.enqueue(() => this.readNewest(true));
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
return this.enqueue(() => this.readNewest(true));
|
|
74
|
+
}
|
|
75
|
+
/* The newest page again; calls that arrive while one waits become that one. */
|
|
76
|
+
refresh() {
|
|
77
|
+
if (this.refreshWaiting) {
|
|
78
|
+
return this.queue;
|
|
79
|
+
}
|
|
80
|
+
this.refreshWaiting = true;
|
|
81
|
+
return this.enqueue(() => {
|
|
82
|
+
this.refreshWaiting = false;
|
|
83
|
+
return this.readNewest(false);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
loadEarlier() {
|
|
87
|
+
return this.enqueue(async () => {
|
|
88
|
+
const cursor = this.state.cursor;
|
|
89
|
+
if (cursor === null) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.set({ loadingEarlier: true });
|
|
93
|
+
try {
|
|
94
|
+
const page = await this.ask({ cursor, limit: SUBAGENT_PAGE });
|
|
95
|
+
this.set({ items: [...page.items, ...this.state.items], cursor: page.history.cursor, live: page.live, loadingEarlier: false });
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (errorCode(error) === 'history-expired') {
|
|
99
|
+
// The record was written again under this panel; what it holds now is the only truth.
|
|
100
|
+
this.set({ items: [], cursor: null, loadingEarlier: false });
|
|
101
|
+
await this.readNewest(false);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.set({ loadingEarlier: false, error: messageOf(error) });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
dispose() {
|
|
109
|
+
if (this.disposed) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.disposed = true;
|
|
113
|
+
for (const off of this.unsubscribe.splice(0)) {
|
|
114
|
+
off();
|
|
115
|
+
}
|
|
116
|
+
if (this.transport.status === 'open') {
|
|
117
|
+
void this.transport.request('chat.subagent', { chatId: this.chatId, toolUseId: this.toolUseId, limit: 1, watch: false }).catch(() => undefined);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async readNewest(watch) {
|
|
121
|
+
try {
|
|
122
|
+
const page = await this.ask({ limit: SUBAGENT_PAGE, ...(watch ? { watch: true } : {}) });
|
|
123
|
+
const merged = mergeNewest(this.state.items, this.state.cursor, page.items, page.history.cursor);
|
|
124
|
+
this.set({ status: 'ready', items: merged.items, cursor: merged.cursor, live: page.live, error: null });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (this.state.status === 'ready' && isConnectionError(error)) {
|
|
128
|
+
// What is on screen stays; the socket that comes back asks again.
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const unsupported = errorCode(error) === 'unknown-request';
|
|
132
|
+
this.set({ status: 'failed', error: messageOf(error), unsupported });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
ask(extra) {
|
|
136
|
+
return this.transport.request('chat.subagent', { chatId: this.chatId, toolUseId: this.toolUseId, ...extra });
|
|
137
|
+
}
|
|
138
|
+
enqueue(work) {
|
|
139
|
+
const next = this.queue.then(() => (this.disposed ? undefined : work()));
|
|
140
|
+
this.queue = next.catch(() => undefined);
|
|
141
|
+
return next;
|
|
142
|
+
}
|
|
143
|
+
set(patch) {
|
|
144
|
+
if (this.disposed) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.state = { ...this.state, ...patch };
|
|
148
|
+
this.onChange(this.state);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ChatItem, ChatSubagentItem } from '@ruimte/agent-contracts';
|
|
2
|
+
import type { StatusWord } from '../agents/status-look';
|
|
3
|
+
import type { SubagentTask } from '../host';
|
|
4
|
+
export declare const subagentTitle: (item: ChatSubagentItem) => string;
|
|
5
|
+
export declare const taskIdOf: (item: ChatSubagentItem) => string | null;
|
|
6
|
+
export declare const statusWordOf: (item: ChatSubagentItem, task: SubagentTask | null) => StatusWord;
|
|
7
|
+
export declare const flyoutSubagents: (order: readonly string[], structure: Readonly<Record<string, ChatItem>>) => ChatSubagentItem[];
|
|
8
|
+
export declare const summaryWordOf: (words: readonly StatusWord[]) => StatusWord;
|
|
9
|
+
export declare const badgeCountOf: (words: readonly StatusWord[]) => number;
|
|
10
|
+
export declare const entryTimeOf: (item: ChatSubagentItem, task: SubagentTask | null, now: number) => string;
|
|
11
|
+
export type SubagentStop = 'task' | 'mark';
|
|
12
|
+
export declare const stopOf: (item: ChatSubagentItem, turnRunning: boolean) => SubagentStop | null;
|
|
13
|
+
export declare const stopLabel: (stop: SubagentStop) => string;
|
|
14
|
+
export type ComposerStop = 'turn' | 'turn-and-subagents';
|
|
15
|
+
export declare const composerStopOf: (shiftKey: boolean) => ComposerStop;
|
|
16
|
+
export declare const composerStopLabel: () => string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
import { formatMoment } from '@ruimte/ui/format/datetime';
|
|
3
|
+
import { formatElapsedShort } from '@ruimte/ui/format/duration';
|
|
4
|
+
export const subagentTitle = (item) => item.description || item.summary || item.subagentType || i18next.t('agent-chat:rows.subagent.label');
|
|
5
|
+
/* The task a row stands for, whose id the host wrote into the row's own. */
|
|
6
|
+
export const taskIdOf = (item) => (item.origin === 'ruimte' && item.id.startsWith('task-') ? item.id.slice(5) : null);
|
|
7
|
+
/* A cancelled task is a failed row on the wire and a paused one a running row, and only the task itself still says which it is. */
|
|
8
|
+
export const statusWordOf = (item, task) => {
|
|
9
|
+
if (item.status === 'failed' && task?.status === 'cancelled') {
|
|
10
|
+
return 'cancelled';
|
|
11
|
+
}
|
|
12
|
+
if (item.status === 'running' && task?.status === 'open' && task.paused !== undefined) {
|
|
13
|
+
return 'paused';
|
|
14
|
+
}
|
|
15
|
+
return item.status;
|
|
16
|
+
};
|
|
17
|
+
/*
|
|
18
|
+
* What the flyout over the composer lists: every sub-agent still at work, and the ones that settled
|
|
19
|
+
* since the last message, so a finished batch stays until the next message is sent. That includes an
|
|
20
|
+
* older row a message woke again. A settled one from before that message stays too while a sibling of
|
|
21
|
+
* its turn still runs. Older ones are only in the thread.
|
|
22
|
+
*/
|
|
23
|
+
export const flyoutSubagents = (order, structure) => {
|
|
24
|
+
const lastMessage = order.findLastIndex((id) => structure[id]?.kind === 'user');
|
|
25
|
+
const lastMessageAt = lastMessage < 0 ? null : (structure[order[lastMessage]]?.createdAt ?? null);
|
|
26
|
+
const items = order.flatMap((id, index) => {
|
|
27
|
+
const item = structure[id];
|
|
28
|
+
return item?.kind === 'subagent' ? [{ item, index }] : [];
|
|
29
|
+
});
|
|
30
|
+
const turns = new Set(items.flatMap(({ item }) => (item.status === 'running' && item.turnId !== null ? [item.turnId] : [])));
|
|
31
|
+
return items
|
|
32
|
+
.filter(({ item, index }) => item.status === 'running' ||
|
|
33
|
+
index > lastMessage ||
|
|
34
|
+
(lastMessageAt !== null && item.finishedAt !== null && item.finishedAt > lastMessageAt) ||
|
|
35
|
+
(item.turnId !== null && turns.has(item.turnId)))
|
|
36
|
+
.map(({ item }) => item);
|
|
37
|
+
};
|
|
38
|
+
/* The one state the badge shows for all of them: work in progress first, then work held up, then whatever went wrong. */
|
|
39
|
+
export const summaryWordOf = (words) => ['running', 'paused', 'failed', 'cancelled'].find((word) => words.includes(word)) ?? 'done';
|
|
40
|
+
/* The number on the badge: the ones still at work, a paused one included, and all of them once none is. */
|
|
41
|
+
export const badgeCountOf = (words) => {
|
|
42
|
+
const active = words.filter((word) => word === 'running' || word === 'paused').length;
|
|
43
|
+
return active > 0 ? active : words.length;
|
|
44
|
+
};
|
|
45
|
+
/* The time on the right of an entry: how long it has run so far, or how long it took once it settled. */
|
|
46
|
+
export const entryTimeOf = (item, task, now) => {
|
|
47
|
+
// A task's own record says when it was given and settled; the row copies those, but may lag behind it.
|
|
48
|
+
const startedAt = task?.createdAt ?? item.startedAt;
|
|
49
|
+
const finishedAt = task === null ? item.finishedAt : (task.settledAt ?? item.finishedAt);
|
|
50
|
+
const word = statusWordOf(item, task);
|
|
51
|
+
if (word === 'paused') {
|
|
52
|
+
const until = task?.paused?.until;
|
|
53
|
+
return until === undefined
|
|
54
|
+
? i18next.t('agent-chat:common.status.paused')
|
|
55
|
+
: i18next.t('agent-chat:activity.pausedUntil', { time: formatMoment(until, now) });
|
|
56
|
+
}
|
|
57
|
+
if (item.status === 'running') {
|
|
58
|
+
return startedAt > 0 ? formatElapsedShort(now - startedAt) : '';
|
|
59
|
+
}
|
|
60
|
+
if (startedAt <= 0 || finishedAt === null || finishedAt < startedAt) {
|
|
61
|
+
return i18next.t(`agent-chat:common.status.${word}`);
|
|
62
|
+
}
|
|
63
|
+
return i18next.t(`agent-chat:activity.took.${word}`, { duration: formatElapsedShort(finishedAt - startedAt) });
|
|
64
|
+
};
|
|
65
|
+
export const stopOf = (item, turnRunning) => {
|
|
66
|
+
if (item.status !== 'running') {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (item.origin === 'ruimte') {
|
|
70
|
+
return item.childId === undefined ? null : 'task';
|
|
71
|
+
}
|
|
72
|
+
return turnRunning ? null : 'mark';
|
|
73
|
+
};
|
|
74
|
+
export const stopLabel = (stop) => stop === 'task' ? i18next.t('agent-chat:subagents.stop.task') : i18next.t('agent-chat:subagents.stop.mark');
|
|
75
|
+
export const composerStopOf = (shiftKey) => (shiftKey ? 'turn-and-subagents' : 'turn');
|
|
76
|
+
export const composerStopLabel = () => i18next.t('agent-chat:composer.stop');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
/*
|
|
3
|
+
* Whether a host can open a subagent's conversation is learned by asking once rather than from a
|
|
4
|
+
* version: the first refusal hides the button on that host's rows that carry no pointer of their own.
|
|
5
|
+
*/
|
|
6
|
+
export const useSubagentSupport = create((set, get) => ({
|
|
7
|
+
unsupported: {},
|
|
8
|
+
markUnsupported(scopeId) {
|
|
9
|
+
if (get().unsupported[scopeId]) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
set({ unsupported: { ...get().unsupported, [scopeId]: true } });
|
|
13
|
+
}
|
|
14
|
+
}));
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ChatItem, ChatSubagentItem } from '@ruimte/agent-contracts';
|
|
2
|
+
export interface SubagentStep {
|
|
3
|
+
toolUseId: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
export type SubagentTrail = readonly SubagentStep[];
|
|
7
|
+
export interface BreadcrumbStep {
|
|
8
|
+
label: string;
|
|
9
|
+
depth: number;
|
|
10
|
+
current: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const MAIN_AGENT: SubagentTrail;
|
|
13
|
+
export declare const crumbOf: (item: ChatSubagentItem) => SubagentStep;
|
|
14
|
+
export declare const openFromMain: (step: SubagentStep) => SubagentTrail;
|
|
15
|
+
export declare const openBelow: (trail: SubagentTrail, step: SubagentStep) => SubagentTrail;
|
|
16
|
+
export declare const trailTo: (trail: SubagentTrail, depth: number) => SubagentTrail;
|
|
17
|
+
export declare const stepBack: (trail: SubagentTrail) => SubagentTrail;
|
|
18
|
+
export declare const showsComposer: (trail: SubagentTrail) => boolean;
|
|
19
|
+
export declare const breadcrumbOf: (trail: SubagentTrail) => BreadcrumbStep[];
|
|
20
|
+
export declare const canOpenSubagent: (item: ChatSubagentItem, machineRefused: boolean) => boolean;
|
|
21
|
+
export declare const openableSubagents: (order: readonly string[], structure: Record<string, ChatItem>, machineRefused: boolean) => ChatSubagentItem[];
|
|
22
|
+
interface SubagentViewStore {
|
|
23
|
+
trails: Record<string, SubagentTrail>;
|
|
24
|
+
show(key: string, trail: SubagentTrail): void;
|
|
25
|
+
back(key: string): boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare const useSubagentView: import("zustand").UseBoundStore<import("zustand").StoreApi<SubagentViewStore>>;
|
|
28
|
+
export declare const useSubagentTrail: (chatId: string) => {
|
|
29
|
+
trail: SubagentTrail;
|
|
30
|
+
show(trail: SubagentTrail): void;
|
|
31
|
+
};
|
|
32
|
+
export declare const useOpenableSubagents: (chatId: string) => ChatSubagentItem[];
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { create } from 'zustand';
|
|
4
|
+
import { useChatScope } from '../scope';
|
|
5
|
+
import { useChatRow } from '../state/chats';
|
|
6
|
+
import { useSubagentSupport } from './subagent-support';
|
|
7
|
+
export const MAIN_AGENT = [];
|
|
8
|
+
const NO_ITEMS = [];
|
|
9
|
+
export const crumbOf = (item) => ({ toolUseId: item.toolUseId, description: item.description });
|
|
10
|
+
const stepLabel = (step) => step.description || i18next.t('agent-chat:rows.subagent.label');
|
|
11
|
+
/* A row in the main agent's thread or an entry of the flyout opens its conversation straight away. */
|
|
12
|
+
export const openFromMain = (step) => [step];
|
|
13
|
+
export const openBelow = (trail, step) => [...trail, step];
|
|
14
|
+
export const trailTo = (trail, depth) => (depth <= 0 ? MAIN_AGENT : trail.slice(0, depth));
|
|
15
|
+
export const stepBack = (trail) => trailTo(trail, trail.length - 1);
|
|
16
|
+
/* A sub-agent's conversation is read back, so there is nobody to write to. */
|
|
17
|
+
export const showsComposer = (trail) => trail.length === 0;
|
|
18
|
+
/* The crumbs after the chat's own title, which the bar draws as the first crumb and the way back. */
|
|
19
|
+
export const breadcrumbOf = (trail) => trail.map((step, index) => ({ label: stepLabel(step), depth: index + 1, current: index === trail.length - 1 }));
|
|
20
|
+
/* A row without a pointer of its own can only be opened by a machine that answers `chat.subagent` for it. */
|
|
21
|
+
export const canOpenSubagent = (item, machineRefused) => item.native !== undefined || !machineRefused;
|
|
22
|
+
/* The subagents a chat can open, in the order the thread has them. */
|
|
23
|
+
export const openableSubagents = (order, structure, machineRefused) => order.flatMap((id) => {
|
|
24
|
+
const item = structure[id];
|
|
25
|
+
return item?.kind === 'subagent' && canOpenSubagent(item, machineRefused) ? [item] : [];
|
|
26
|
+
});
|
|
27
|
+
/*
|
|
28
|
+
* What a chat shows in its place, for this page's life only: a look into a subagent is a moment
|
|
29
|
+
* rather than a place, so it is never written to the project or to the host.
|
|
30
|
+
*/
|
|
31
|
+
export const useSubagentView = create((set, get) => ({
|
|
32
|
+
trails: {},
|
|
33
|
+
show(key, trail) {
|
|
34
|
+
const trails = { ...get().trails };
|
|
35
|
+
if (trail.length === 0) {
|
|
36
|
+
delete trails[key];
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
trails[key] = trail;
|
|
40
|
+
}
|
|
41
|
+
set({ trails });
|
|
42
|
+
},
|
|
43
|
+
back(key) {
|
|
44
|
+
const trail = get().trails[key];
|
|
45
|
+
if (trail === undefined || trail.length === 0) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
get().show(key, stepBack(trail));
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
/* The trail of a chat in the scope this is rendered in, and a way to set it. */
|
|
53
|
+
export const useSubagentTrail = (chatId) => {
|
|
54
|
+
const key = useChatScope().keyOf(chatId);
|
|
55
|
+
const trail = useSubagentView((s) => s.trails[key] ?? MAIN_AGENT);
|
|
56
|
+
return { trail, show: (next) => useSubagentView.getState().show(key, next) };
|
|
57
|
+
};
|
|
58
|
+
export const useOpenableSubagents = (chatId) => {
|
|
59
|
+
const { id } = useChatScope();
|
|
60
|
+
const refused = useSubagentSupport((s) => s.unsupported[id] === true);
|
|
61
|
+
// The structure and not the items, so a delta growing a reply does not walk the thread again.
|
|
62
|
+
const order = useChatRow(chatId, (row) => row?.order);
|
|
63
|
+
const structure = useChatRow(chatId, (row) => row?.structure);
|
|
64
|
+
return useMemo(() => (structure === undefined ? [] : openableSubagents(order ?? NO_ITEMS, structure, refused)), [order, structure, refused]);
|
|
65
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChatAttachmentUpload } from '@ruimte/agent-contracts';
|
|
2
|
+
export declare const THUMBNAIL_PX = 256;
|
|
3
|
+
export interface ThumbnailCrop {
|
|
4
|
+
sx: number;
|
|
5
|
+
sy: number;
|
|
6
|
+
size: number;
|
|
7
|
+
out: number;
|
|
8
|
+
}
|
|
9
|
+
export declare const thumbnailCrop: (width: number, height: number, max?: number) => ThumbnailCrop;
|
|
10
|
+
export declare const useUploadThumbnail: (upload: ChatAttachmentUpload) => string | null;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { uploadPreviewUrl } from './attachments';
|
|
4
|
+
/* The edge of a composer thumbnail: a few times the 56 px it is drawn at, so a sharp screen still gets a sharp one. */
|
|
5
|
+
export const THUMBNAIL_PX = 256;
|
|
6
|
+
/* The square in the middle of an image that a thumbnail shows, the same part `object-cover` would. */
|
|
7
|
+
export const thumbnailCrop = (width, height, max = THUMBNAIL_PX) => {
|
|
8
|
+
const size = Math.max(1, Math.min(width, height));
|
|
9
|
+
return {
|
|
10
|
+
sx: Math.max(0, Math.floor((width - size) / 2)),
|
|
11
|
+
sy: Math.max(0, Math.floor((height - size) / 2)),
|
|
12
|
+
size,
|
|
13
|
+
out: Math.min(max, size)
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
const bytesOf = (base64) => {
|
|
17
|
+
const binary = atob(base64);
|
|
18
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
19
|
+
for (let i = 0; i < binary.length; i++) {
|
|
20
|
+
bytes[i] = binary.charCodeAt(i);
|
|
21
|
+
}
|
|
22
|
+
return bytes;
|
|
23
|
+
};
|
|
24
|
+
const dataUrlOf = (blob) => new Promise((resolve, reject) => {
|
|
25
|
+
const reader = new FileReader();
|
|
26
|
+
reader.onerror = () => reject(reader.error ?? new Error(i18next.t('agent-chat:attachments.unreadableThumbnail')));
|
|
27
|
+
reader.onload = () => resolve(String(reader.result));
|
|
28
|
+
reader.readAsDataURL(blob);
|
|
29
|
+
});
|
|
30
|
+
/*
|
|
31
|
+
* A still of the middle of an image, decoded once. The composer used to draw the full data URL in a
|
|
32
|
+
* 56 px box, which a browser decodes again for every paint, so a tall screenshot made typing stutter.
|
|
33
|
+
* An animated image gives its first frame. Whatever cannot be decoded this way (an SVG, a browser
|
|
34
|
+
* without `OffscreenCanvas`) falls back to the image itself, which is what it was before.
|
|
35
|
+
*/
|
|
36
|
+
const makeThumbnail = async (upload) => {
|
|
37
|
+
try {
|
|
38
|
+
const bitmap = await createImageBitmap(new Blob([bytesOf(upload.data)], { type: upload.mime }));
|
|
39
|
+
const crop = thumbnailCrop(bitmap.width, bitmap.height);
|
|
40
|
+
const canvas = new OffscreenCanvas(crop.out, crop.out);
|
|
41
|
+
const context = canvas.getContext('2d');
|
|
42
|
+
if (!context) {
|
|
43
|
+
bitmap.close();
|
|
44
|
+
return uploadPreviewUrl(upload);
|
|
45
|
+
}
|
|
46
|
+
context.drawImage(bitmap, crop.sx, crop.sy, crop.size, crop.size, 0, 0, crop.out, crop.out);
|
|
47
|
+
bitmap.close();
|
|
48
|
+
return await dataUrlOf(await canvas.convertToBlob({ type: 'image/png' }));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return uploadPreviewUrl(upload);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
// Keyed by the upload itself, so a composer that mounts again finds it and a removed file lets it go.
|
|
55
|
+
const made = new WeakMap();
|
|
56
|
+
const making = new WeakMap();
|
|
57
|
+
const thumbnailOf = (upload) => {
|
|
58
|
+
let promise = making.get(upload);
|
|
59
|
+
if (!promise) {
|
|
60
|
+
promise = makeThumbnail(upload).then((url) => {
|
|
61
|
+
made.set(upload, url);
|
|
62
|
+
return url;
|
|
63
|
+
});
|
|
64
|
+
making.set(upload, promise);
|
|
65
|
+
}
|
|
66
|
+
return promise;
|
|
67
|
+
};
|
|
68
|
+
/* The thumbnail of an image the composer holds, or null while it is being made. */
|
|
69
|
+
export const useUploadThumbnail = (upload) => {
|
|
70
|
+
const [ready, setReady] = useState(null);
|
|
71
|
+
const cached = made.get(upload);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (made.has(upload)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
void thumbnailOf(upload).then((url) => {
|
|
78
|
+
if (!cancelled) {
|
|
79
|
+
setReady({ upload, url });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
return () => {
|
|
83
|
+
cancelled = true;
|
|
84
|
+
};
|
|
85
|
+
}, [upload]);
|
|
86
|
+
return cached ?? (ready?.upload === upload ? ready.url : null);
|
|
87
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
type Scroller = {
|
|
2
|
+
element: HTMLElement;
|
|
3
|
+
follow: (enabled: boolean) => void;
|
|
4
|
+
coveredHeight: () => number;
|
|
5
|
+
};
|
|
6
|
+
export declare const registerTimeline: (chatId: string, element: HTMLElement | null, follow: Scroller['follow'], coveredHeight: Scroller['coveredHeight']) => (() => void);
|
|
7
|
+
export declare const setTimelineAtEnd: (chatId: string, atEnd: boolean) => void;
|
|
8
|
+
export declare const subscribeTimelineEnd: (listener: () => void) => (() => void);
|
|
9
|
+
export declare const timelineAtEnd: (chatId: string) => boolean;
|
|
10
|
+
export declare const scrollTimelineToEnd: (chatId: string) => void;
|
|
11
|
+
export declare const pageTimeline: (chatId: string, direction: -1 | 1) => boolean;
|
|
12
|
+
export declare const registerMessageStepper: (key: string, step: (direction: -1 | 1) => boolean) => (() => void);
|
|
13
|
+
export declare const stepTimelineMessage: (key: string, direction: -1 | 1) => boolean;
|
|
14
|
+
export declare const registerItemJumper: (key: string, jump: (itemId: string) => void) => (() => void);
|
|
15
|
+
export declare const jumpToTimelineItem: (key: string, itemId: string) => boolean;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// A page is a screen minus a little, so the line you were reading is still on it after the jump.
|
|
2
|
+
const PAGE_OVERLAP = 0.9;
|
|
3
|
+
const scrollers = new Map();
|
|
4
|
+
/* The timeline hands its scroller over so the composer, which never sees it, can page through it. */
|
|
5
|
+
export const registerTimeline = (chatId, element, follow, coveredHeight) => {
|
|
6
|
+
if (element === null) {
|
|
7
|
+
return () => undefined;
|
|
8
|
+
}
|
|
9
|
+
scrollers.set(chatId, { element, follow, coveredHeight });
|
|
10
|
+
return () => {
|
|
11
|
+
if (scrollers.get(chatId)?.element === element) {
|
|
12
|
+
scrollers.delete(chatId);
|
|
13
|
+
ends.delete(chatId);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
/* The timeline owns following; the composer subscribes here to show its jump-to-end button. */
|
|
18
|
+
const ends = new Map();
|
|
19
|
+
const listeners = new Set();
|
|
20
|
+
export const setTimelineAtEnd = (chatId, atEnd) => {
|
|
21
|
+
if (ends.get(chatId) === atEnd) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
ends.set(chatId, atEnd);
|
|
25
|
+
for (const listener of listeners) {
|
|
26
|
+
listener();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export const subscribeTimelineEnd = (listener) => {
|
|
30
|
+
listeners.add(listener);
|
|
31
|
+
return () => {
|
|
32
|
+
listeners.delete(listener);
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/* A thread nobody has scrolled is at its end, so the button stays away until there is a way back. */
|
|
36
|
+
export const timelineAtEnd = (chatId) => ends.get(chatId) ?? true;
|
|
37
|
+
export const scrollTimelineToEnd = (chatId) => {
|
|
38
|
+
const scroller = scrollers.get(chatId);
|
|
39
|
+
if (scroller) {
|
|
40
|
+
scroller.follow(true);
|
|
41
|
+
scroller.element.scrollTo({ top: scroller.element.scrollHeight, behavior: 'smooth' });
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
/* Pages the thread of this chat up or down; false when it has no timeline on screen. */
|
|
45
|
+
export const pageTimeline = (chatId, direction) => {
|
|
46
|
+
const scroller = scrollers.get(chatId);
|
|
47
|
+
if (!scroller) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
scroller.follow(false);
|
|
51
|
+
const { element } = scroller;
|
|
52
|
+
element.scrollBy({ top: direction * Math.max(0, element.clientHeight - scroller.coveredHeight()) * PAGE_OVERLAP, behavior: 'smooth' });
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
/*
|
|
56
|
+
* The keyboard steps from one message of the person to the next through the timeline on screen,
|
|
57
|
+
* which is the only side that knows where its rows start. Keyed with the scope's `keyOf`, the way
|
|
58
|
+
* the app's shortcuts name the chat that has the focus.
|
|
59
|
+
*/
|
|
60
|
+
const steppers = new Map();
|
|
61
|
+
export const registerMessageStepper = (key, step) => {
|
|
62
|
+
steppers.set(key, step);
|
|
63
|
+
return () => {
|
|
64
|
+
if (steppers.get(key) === step) {
|
|
65
|
+
steppers.delete(key);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/* False when that chat has no timeline on screen or no message in that direction. */
|
|
70
|
+
export const stepTimelineMessage = (key, direction) => steppers.get(key)?.(direction) ?? false;
|
|
71
|
+
/*
|
|
72
|
+
* A jump to one message of a chat, asked from outside its thread (the chat's menu). A thread that is
|
|
73
|
+
* not on screen yet gets it when it registers, since the menu may have to show the chat first.
|
|
74
|
+
* Keyed with the scope's `keyOf`, like the steppers.
|
|
75
|
+
*/
|
|
76
|
+
const jumpers = new Map();
|
|
77
|
+
const waitingJumps = new Map();
|
|
78
|
+
export const registerItemJumper = (key, jump) => {
|
|
79
|
+
jumpers.set(key, jump);
|
|
80
|
+
const waiting = waitingJumps.get(key);
|
|
81
|
+
if (waiting !== undefined) {
|
|
82
|
+
waitingJumps.delete(key);
|
|
83
|
+
jump(waiting);
|
|
84
|
+
}
|
|
85
|
+
return () => {
|
|
86
|
+
if (jumpers.get(key) === jump) {
|
|
87
|
+
jumpers.delete(key);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
/* False when that chat has no thread on screen; the jump then waits for the next one that registers. */
|
|
92
|
+
export const jumpToTimelineItem = (key, itemId) => {
|
|
93
|
+
const jump = jumpers.get(key);
|
|
94
|
+
if (jump === undefined) {
|
|
95
|
+
waitingJumps.set(key, itemId);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
jump(itemId);
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { AccountDot } from '../../agents/AccountDot';
|
|
4
|
+
import { useAccountChoice } from '../account-choice';
|
|
5
|
+
import { useChats } from '../../state/chats';
|
|
6
|
+
import { useChatScope } from '../../scope';
|
|
7
|
+
import { Pill } from '@ruimte/ui/Pill';
|
|
8
|
+
import { Tooltip } from '@ruimte/ui/Tooltip';
|
|
9
|
+
function AccountPillOf({ provider, account }) {
|
|
10
|
+
const { t } = useTranslation('agent-chat');
|
|
11
|
+
const choice = useAccountChoice(provider, account);
|
|
12
|
+
if (choice === null) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const name = choice.current === null ? choice.currentId : choice.nameOf(choice.current);
|
|
16
|
+
return (_jsx(Tooltip, { label: t('account.pill', { provider: choice.providerName, account: name }), children: _jsx(Pill, { icon: _jsx(AccountDot, { color: choice.current?.account.color, className: "size-1.75" }), children: _jsx("span", { className: "max-w-32 truncate", children: name }) }) }));
|
|
17
|
+
}
|
|
18
|
+
/* Which account a chat node runs under, from the status every client gets; only while its CLI has a choice of accounts. */
|
|
19
|
+
export function AccountPill({ chatId }) {
|
|
20
|
+
const { keyOf } = useChatScope();
|
|
21
|
+
const provider = useChats((s) => s.statusByKey[keyOf(chatId)]?.info.provider);
|
|
22
|
+
const account = useChats((s) => s.statusByKey[keyOf(chatId)]?.info.account);
|
|
23
|
+
return provider === undefined ? null : _jsx(AccountPillOf, { provider: provider, account: account });
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ChatBookmark } from '@ruimte/agent-contracts';
|
|
2
|
+
interface BookmarkMarkerProps {
|
|
3
|
+
chatId: string;
|
|
4
|
+
itemId: string;
|
|
5
|
+
bookmark: ChatBookmark | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function BookmarkMarker({ chatId, itemId, bookmark }: BookmarkMarkerProps): import("react").JSX.Element | null;
|
|
8
|
+
export {};
|