@adecore/agents-react 0.23.0 → 0.24.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/README.md +14 -11
- package/dist/agents/AccountDot.js +3 -1
- package/dist/agents/accounts.d.ts +3 -2
- package/dist/agents/accounts.js +5 -6
- package/dist/chat/actions.d.ts +2 -2
- package/dist/chat/actions.js +8 -5
- package/dist/chat/bookmarks.d.ts +5 -3
- package/dist/chat/bookmarks.js +13 -14
- package/dist/chat/chat-client.d.ts +1 -0
- package/dist/chat/chat-client.js +7 -0
- package/dist/chat/composer-send.d.ts +3 -0
- package/dist/chat/composer-send.js +22 -0
- package/dist/chat/forks.js +22 -5
- package/dist/chat/index.d.ts +31 -0
- package/dist/chat/index.js +28 -0
- package/dist/chat/logic/embedded-code.d.ts +8 -0
- package/dist/chat/logic/embedded-code.js +371 -0
- package/dist/chat/logic/fork.js +1 -0
- package/dist/chat/logic/goal.d.ts +6 -0
- package/dist/chat/logic/goal.js +26 -0
- package/dist/chat/logic/scrubber.d.ts +1 -1
- package/dist/chat/logic/scrubber.js +6 -5
- package/dist/chat/logic/thought.d.ts +3 -0
- package/dist/chat/logic/thought.js +35 -0
- package/dist/chat/logic/thread-cards.js +2 -0
- package/dist/chat/logic/timeline.d.ts +5 -0
- package/dist/chat/logic/timeline.js +22 -1
- package/dist/chat/logic/tool-catalog.d.ts +1 -0
- package/dist/chat/logic/tool-catalog.js +3 -1
- package/dist/chat/logic/tools.d.ts +14 -0
- package/dist/chat/logic/tools.js +57 -0
- package/dist/chat/preferences.d.ts +6 -0
- package/dist/chat/preferences.js +19 -3
- package/dist/chat/reading-width.d.ts +9 -0
- package/dist/chat/reading-width.js +27 -0
- package/dist/chat/send-key.d.ts +10 -0
- package/dist/chat/send-key.js +24 -0
- package/dist/chat/ui/BookmarkMarker.js +4 -2
- package/dist/chat/ui/BookmarkSubmenu.js +4 -3
- package/dist/chat/ui/ChatActivity.js +14 -10
- package/dist/chat/ui/ChatReferenceChip.js +3 -4
- package/dist/chat/ui/CodeBlock.d.ts +2 -1
- package/dist/chat/ui/CodeBlock.js +14 -96
- package/dist/chat/ui/Composer.d.ts +1 -1
- package/dist/chat/ui/Composer.js +117 -906
- package/dist/chat/ui/ComposerInput.js +5 -4
- package/dist/chat/ui/EditDiff.js +3 -2
- package/dist/chat/ui/GoalDock.d.ts +9 -0
- package/dist/chat/ui/GoalDock.js +63 -0
- package/dist/chat/ui/LimitState.js +5 -3
- package/dist/chat/ui/Markdown.d.ts +2 -1
- package/dist/chat/ui/Markdown.js +70 -31
- package/dist/chat/ui/MermaidBlock.d.ts +3 -0
- package/dist/chat/ui/MermaidBlock.js +73 -0
- package/dist/chat/ui/MessageActions.js +12 -7
- package/dist/chat/ui/Pickers.js +6 -5
- package/dist/chat/ui/PromptComposer.js +4 -3
- package/dist/chat/ui/RowTime.d.ts +3 -0
- package/dist/chat/ui/RowTime.js +12 -0
- package/dist/chat/ui/RunSettings.js +5 -4
- package/dist/chat/ui/ShellCommand.d.ts +3 -0
- package/dist/chat/ui/ShellCommand.js +110 -0
- package/dist/chat/ui/SubagentInfo.js +4 -3
- package/dist/chat/ui/SubagentStopButton.js +4 -3
- package/dist/chat/ui/SubagentTimeline.js +4 -1
- package/dist/chat/ui/Timeline.js +70 -40
- package/dist/chat/ui/TimelineMenu.d.ts +2 -1
- package/dist/chat/ui/TimelineMenu.js +18 -8
- package/dist/chat/ui/UnifiedDiff.js +3 -2
- package/dist/chat/ui/VisualFrame.js +6 -7
- package/dist/chat/ui/code-theme.d.ts +2 -1
- package/dist/chat/ui/code-theme.js +4 -4
- package/dist/chat/ui/composer/ComposerControls.d.ts +27 -0
- package/dist/chat/ui/composer/ComposerControls.js +34 -0
- package/dist/chat/ui/composer/ComposerMenu.d.ts +4 -0
- package/dist/chat/ui/composer/ComposerMenu.js +46 -0
- package/dist/chat/ui/composer/DraftParts.d.ts +18 -0
- package/dist/chat/ui/composer/DraftParts.js +25 -0
- package/dist/chat/ui/composer/QueuedMessages.d.ts +10 -0
- package/dist/chat/ui/composer/QueuedMessages.js +20 -0
- package/dist/chat/ui/composer/key-handler.d.ts +20 -0
- package/dist/chat/ui/composer/key-handler.js +150 -0
- package/dist/chat/ui/composer/keys.d.ts +7 -2
- package/dist/chat/ui/composer/keys.js +19 -9
- package/dist/chat/ui/composer/placeholder.d.ts +9 -0
- package/dist/chat/ui/composer/placeholder.js +38 -0
- package/dist/chat/ui/composer/use-chat-controls.d.ts +28 -0
- package/dist/chat/ui/composer/use-chat-controls.js +112 -0
- package/dist/chat/ui/composer/use-compaction-offer.d.ts +2 -0
- package/dist/chat/ui/composer/use-compaction-offer.js +23 -0
- package/dist/chat/ui/composer/use-composer-draft.d.ts +14 -0
- package/dist/chat/ui/composer/use-composer-draft.js +39 -0
- package/dist/chat/ui/composer/use-composer-menus.d.ts +52 -0
- package/dist/chat/ui/composer/use-composer-menus.js +232 -0
- package/dist/chat/ui/composer/use-composer-submit.d.ts +32 -0
- package/dist/chat/ui/composer/use-composer-submit.js +113 -0
- package/dist/chat/ui/composer/use-draft-files.d.ts +27 -0
- package/dist/chat/ui/composer/use-draft-files.js +111 -0
- package/dist/chat/ui/composer/use-notice.d.ts +6 -0
- package/dist/chat/ui/composer/use-notice.js +18 -0
- package/dist/chat/ui/composer/use-pending-requests.d.ts +2 -0
- package/dist/chat/ui/composer/use-pending-requests.js +22 -0
- package/dist/chat/ui/composer/use-prompt-history.d.ts +8 -0
- package/dist/chat/ui/composer/use-prompt-history.js +57 -0
- package/dist/chat/ui/composer/use-prompt-hold.d.ts +6 -0
- package/dist/chat/ui/composer/use-prompt-hold.js +48 -0
- package/dist/chat/ui/composer/use-take-back.d.ts +13 -0
- package/dist/chat/ui/composer/use-take-back.js +82 -0
- package/dist/chat/ui/diff/EditorEditDiff.js +4 -3
- package/dist/chat/ui/diff/LegacyDiffPool.js +3 -2
- package/dist/chat/ui/diff/LegacyEditDiff.js +3 -2
- package/dist/chat/ui/diff/LegacyUnifiedDiff.js +3 -2
- package/dist/chat/ui/diff/syntax-runtime.d.ts +2 -1
- package/dist/chat/ui/diff/syntax-runtime.js +9 -5
- package/dist/chat/ui/diff-syntax.d.ts +2 -2
- package/dist/chat/ui/diff-syntax.js +1 -2
- package/dist/chat/ui/diff-theme.js +10 -9
- package/dist/chat/ui/file-links.d.ts +1 -1
- package/dist/chat/ui/file-links.js +7 -5
- package/dist/chat/ui/highlighter.d.ts +27 -0
- package/dist/chat/ui/highlighter.js +151 -0
- package/dist/chat/ui/index.d.ts +67 -0
- package/dist/chat/ui/index.js +64 -0
- package/dist/chat/ui/intelligent-ui/UiReply.js +3 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +3 -2
- package/dist/chat/ui/intelligent-ui/use-ui-links.js +2 -2
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +4 -3
- package/dist/chat/ui/markdown-blocks.d.ts +8 -0
- package/dist/chat/ui/markdown-blocks.js +260 -12
- package/dist/chat/ui/mermaid.d.ts +23 -0
- package/dist/chat/ui/mermaid.js +93 -0
- package/dist/chat/ui/rows/ForksRow.js +5 -3
- package/dist/chat/ui/rows/GeneratedImageRow.js +6 -4
- package/dist/chat/ui/rows/MessageRows.d.ts +4 -0
- package/dist/chat/ui/rows/MessageRows.js +36 -13
- package/dist/chat/ui/rows/Rows.js +3 -1
- package/dist/chat/ui/rows/SubagentRow.js +3 -2
- package/dist/chat/ui/rows/VisualRow.js +3 -2
- package/dist/chat/ui/rows/WorkRows.d.ts +6 -3
- package/dist/chat/ui/rows/WorkRows.js +57 -20
- package/dist/chat/ui/rows/WorkflowRow.js +3 -2
- package/dist/chat/ui/timestamp-format.d.ts +1 -0
- package/dist/chat/ui/timestamp-format.js +2 -0
- package/dist/chat/ui/use-diff-source-tokens.js +6 -3
- package/dist/chat/ui/visual-theme.js +3 -2
- package/dist/host.d.ts +9 -6
- package/dist/host.js +19 -8
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/locales/en/agent-chat.d.ts +658 -0
- package/dist/locales/en/agent-chat.js +657 -0
- package/dist/locales/en/agent-prompts.d.ts +44 -0
- package/dist/locales/en/agent-prompts.js +43 -0
- package/dist/locales/en/agent-providers.d.ts +132 -0
- package/dist/locales/en/agent-providers.js +131 -0
- package/dist/locales/en/agent-usage.d.ts +132 -0
- package/dist/locales/en/agent-usage.js +131 -0
- package/dist/locales/nl/agent-chat.d.ts +658 -0
- package/dist/locales/nl/agent-chat.js +657 -0
- package/dist/locales/nl/agent-prompts.d.ts +44 -0
- package/dist/locales/nl/agent-prompts.js +43 -0
- package/dist/locales/nl/agent-providers.d.ts +132 -0
- package/dist/locales/nl/agent-providers.js +131 -0
- package/dist/locales/nl/agent-usage.d.ts +132 -0
- package/dist/locales/nl/agent-usage.js +131 -0
- package/dist/locales.js +9 -8
- package/dist/prompts/index.d.ts +11 -0
- package/dist/prompts/index.js +12 -0
- package/dist/prompts/logic/subjects.d.ts +2 -1
- package/dist/prompts/logic/subjects.js +6 -4
- package/dist/prompts/ui/ApprovalBody.js +6 -4
- package/dist/prompts/ui/PromptCard.js +5 -4
- package/dist/prompts/ui/PromptView.js +3 -2
- package/dist/prompts/ui/QuestionBody.js +3 -2
- package/dist/providers/AccountColors.js +2 -2
- package/dist/providers/AccountDetail.js +7 -6
- package/dist/providers/AccountVariables.js +3 -1
- package/dist/providers/AddAccountForm.js +3 -2
- package/dist/providers/ProvidersPane.js +4 -3
- package/dist/providers/account-actions.d.ts +4 -3
- package/dist/providers/account-actions.js +6 -7
- package/dist/providers/index.d.ts +16 -0
- package/dist/providers/index.js +17 -0
- package/dist/providers/provider-abilities.d.ts +2 -1
- package/dist/providers/provider-abilities.js +4 -0
- package/dist/state/chats.js +4 -2
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/{prompts/logic/prompts.fixtures.d.ts → testing/prompts.d.ts} +1 -1
- package/dist/theme.css +13 -3
- package/dist/usage/UsageBreakdown.js +3 -2
- package/dist/usage/UsageLimits.js +4 -3
- package/dist/usage/UsagePage.js +3 -2
- package/dist/usage/index.d.ts +15 -0
- package/dist/usage/index.js +15 -0
- package/package.json +53 -1031
- package/src/agents/AccountDot.tsx +3 -1
- package/src/agents/accounts.ts +6 -6
- package/src/chat/actions.ts +8 -5
- package/src/chat/bookmarks.ts +23 -14
- package/src/chat/chat-client.ts +8 -0
- package/src/chat/composer-send.ts +27 -0
- package/src/chat/forks.ts +27 -6
- package/src/chat/index.ts +98 -0
- package/src/chat/logic/embedded-code.ts +409 -0
- package/src/chat/logic/fork.ts +1 -0
- package/src/chat/logic/goal.ts +33 -0
- package/src/chat/logic/scrubber.ts +6 -5
- package/src/chat/logic/thought.ts +40 -0
- package/src/chat/logic/thread-cards.ts +2 -0
- package/src/chat/logic/timeline.ts +25 -1
- package/src/chat/logic/tool-catalog.ts +5 -1
- package/src/chat/logic/tools.ts +76 -0
- package/src/chat/preferences.ts +26 -3
- package/src/chat/reading-width.ts +39 -0
- package/src/chat/send-key.ts +38 -0
- package/src/chat/ui/BookmarkMarker.tsx +4 -2
- package/src/chat/ui/BookmarkSubmenu.tsx +4 -3
- package/src/chat/ui/ChatActivity.tsx +14 -10
- package/src/chat/ui/ChatReferenceChip.tsx +3 -4
- package/src/chat/ui/CodeBlock.tsx +33 -124
- package/src/chat/ui/Composer.tsx +162 -1324
- package/src/chat/ui/ComposerInput.tsx +5 -4
- package/src/chat/ui/EditDiff.tsx +3 -2
- package/src/chat/ui/GoalDock.tsx +104 -0
- package/src/chat/ui/LimitState.tsx +5 -3
- package/src/chat/ui/Markdown.tsx +113 -39
- package/src/chat/ui/MermaidBlock.tsx +119 -0
- package/src/chat/ui/MessageActions.tsx +13 -8
- package/src/chat/ui/Pickers.tsx +8 -7
- package/src/chat/ui/PromptComposer.tsx +4 -3
- package/src/chat/ui/RowTime.tsx +21 -0
- package/src/chat/ui/RunSettings.tsx +9 -4
- package/src/chat/ui/ShellCommand.tsx +134 -0
- package/src/chat/ui/SubagentInfo.tsx +4 -3
- package/src/chat/ui/SubagentStopButton.tsx +4 -3
- package/src/chat/ui/SubagentTimeline.tsx +5 -1
- package/src/chat/ui/Timeline.tsx +167 -105
- package/src/chat/ui/TimelineMenu.tsx +42 -7
- package/src/chat/ui/UnifiedDiff.tsx +3 -2
- package/src/chat/ui/VisualFrame.tsx +6 -7
- package/src/chat/ui/code-theme.ts +4 -4
- package/src/chat/ui/composer/ComposerControls.tsx +147 -0
- package/src/chat/ui/composer/ComposerMenu.tsx +138 -0
- package/src/chat/ui/composer/DraftParts.tsx +86 -0
- package/src/chat/ui/composer/QueuedMessages.tsx +64 -0
- package/src/chat/ui/composer/key-handler.ts +174 -0
- package/src/chat/ui/composer/keys.ts +32 -10
- package/src/chat/ui/composer/placeholder.ts +48 -0
- package/src/chat/ui/composer/use-chat-controls.ts +134 -0
- package/src/chat/ui/composer/use-compaction-offer.ts +25 -0
- package/src/chat/ui/composer/use-composer-draft.ts +71 -0
- package/src/chat/ui/composer/use-composer-menus.ts +289 -0
- package/src/chat/ui/composer/use-composer-submit.ts +163 -0
- package/src/chat/ui/composer/use-draft-files.ts +135 -0
- package/src/chat/ui/composer/use-notice.ts +28 -0
- package/src/chat/ui/composer/use-pending-requests.ts +23 -0
- package/src/chat/ui/composer/use-prompt-history.ts +76 -0
- package/src/chat/ui/composer/use-prompt-hold.ts +58 -0
- package/src/chat/ui/composer/use-take-back.ts +91 -0
- package/src/chat/ui/diff/DiffFallbackBoundary.tsx +2 -2
- package/src/chat/ui/diff/EditorEditDiff.tsx +4 -3
- package/src/chat/ui/diff/LegacyDiffPool.tsx +3 -2
- package/src/chat/ui/diff/LegacyEditDiff.tsx +3 -2
- package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +3 -2
- package/src/chat/ui/diff/syntax-runtime.ts +10 -5
- package/src/chat/ui/diff-syntax.ts +2 -2
- package/src/chat/ui/diff-theme.ts +10 -9
- package/src/chat/ui/file-links.ts +7 -5
- package/src/chat/ui/highlighter.ts +185 -0
- package/src/chat/ui/index.ts +109 -0
- package/src/chat/ui/intelligent-ui/UiReply.tsx +5 -4
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +3 -2
- package/src/chat/ui/intelligent-ui/use-ui-links.ts +2 -2
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +4 -3
- package/src/chat/ui/markdown-blocks.ts +300 -12
- package/src/chat/ui/mermaid.ts +118 -0
- package/src/chat/ui/rows/ForksRow.tsx +5 -3
- package/src/chat/ui/rows/GeneratedImageRow.tsx +6 -4
- package/src/chat/ui/rows/MessageRows.tsx +73 -18
- package/src/chat/ui/rows/Rows.tsx +14 -1
- package/src/chat/ui/rows/SubagentRow.tsx +3 -2
- package/src/chat/ui/rows/VisualRow.tsx +3 -2
- package/src/chat/ui/rows/WorkRows.tsx +148 -47
- package/src/chat/ui/rows/WorkflowRow.tsx +3 -2
- package/src/chat/ui/timestamp-format.ts +2 -0
- package/src/chat/ui/use-diff-source-tokens.ts +10 -4
- package/src/chat/ui/visual-theme.ts +3 -2
- package/src/host.ts +31 -15
- package/src/index.ts +49 -0
- package/src/locales/en/agent-chat.ts +657 -0
- package/src/locales/en/agent-prompts.ts +43 -0
- package/src/locales/en/agent-providers.ts +131 -0
- package/src/locales/en/agent-usage.ts +131 -0
- package/src/locales/nl/agent-chat.ts +657 -0
- package/src/locales/nl/agent-prompts.ts +43 -0
- package/src/locales/nl/agent-providers.ts +131 -0
- package/src/locales/nl/agent-usage.ts +132 -0
- package/src/locales.ts +9 -8
- package/src/prompts/index.ts +35 -0
- package/src/prompts/logic/subjects.ts +12 -4
- package/src/prompts/ui/ApprovalBody.tsx +6 -4
- package/src/prompts/ui/PromptCard.tsx +11 -4
- package/src/prompts/ui/PromptView.tsx +3 -2
- package/src/prompts/ui/QuestionBody.tsx +3 -2
- package/src/providers/AccountColors.tsx +2 -2
- package/src/providers/AccountDetail.tsx +13 -7
- package/src/providers/AccountVariables.tsx +3 -1
- package/src/providers/AddAccountForm.tsx +3 -2
- package/src/providers/ProvidersPane.tsx +4 -3
- package/src/providers/account-actions.ts +7 -7
- package/src/providers/index.ts +17 -0
- package/src/providers/provider-abilities.ts +6 -1
- package/src/state/chats.ts +4 -2
- package/src/state/index.ts +39 -0
- package/src/testing/index.ts +2 -0
- package/src/{prompts/logic/prompts.fixtures.ts → testing/prompts.ts} +1 -1
- package/src/theme.css +13 -3
- package/src/usage/UsageBreakdown.tsx +3 -2
- package/src/usage/UsageLimits.tsx +4 -3
- package/src/usage/UsagePage.tsx +3 -2
- package/src/usage/index.ts +16 -0
- package/dist/locales/en/agent-chat.json +0 -597
- package/dist/locales/en/agent-prompts.json +0 -43
- package/dist/locales/en/agent-providers.json +0 -131
- package/dist/locales/en/agent-usage.json +0 -131
- package/dist/locales/nl/agent-chat.json +0 -597
- package/dist/locales/nl/agent-prompts.json +0 -43
- package/dist/locales/nl/agent-providers.json +0 -131
- package/dist/locales/nl/agent-usage.json +0 -131
- package/src/locales/en/agent-chat.json +0 -597
- package/src/locales/en/agent-prompts.json +0 -43
- package/src/locales/en/agent-providers.json +0 -131
- package/src/locales/en/agent-usage.json +0 -131
- package/src/locales/nl/agent-chat.json +0 -597
- package/src/locales/nl/agent-prompts.json +0 -43
- package/src/locales/nl/agent-providers.json +0 -131
- package/src/locales/nl/agent-usage.json +0 -131
- /package/dist/{prompts/logic/prompts.fixtures.js → testing/prompts.js} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useChatHost } from '../../../host.js';
|
|
4
|
+
import { useChatScope } from '../../../scope.js';
|
|
5
|
+
import { errorCode } from '../../../transport.js';
|
|
6
|
+
import { useChatActions } from '../../actions.js';
|
|
7
|
+
import { rememberChatAccount, rememberChatPreferences, rememberChatSelection } from '../../preferences.js';
|
|
8
|
+
import { composerStopOf } from '../../subagent-list.js';
|
|
9
|
+
/* What the composer does to the chat rather than to its draft: its settings, a stop, a clear and a compaction. */
|
|
10
|
+
export function useChatControls({ chatId, info, notice: { setNotice, showError }, onRetarget }) {
|
|
11
|
+
const { t } = useTranslation('agent-chat');
|
|
12
|
+
const host = useChatHost();
|
|
13
|
+
const scope = useChatScope();
|
|
14
|
+
const actions = useChatActions();
|
|
15
|
+
const configuring = useRef(false);
|
|
16
|
+
const [configurationPending, setConfigurationPending] = useState(false);
|
|
17
|
+
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
18
|
+
const [confirmClear, setConfirmClear] = useState(false);
|
|
19
|
+
/* One configuration at a time; a second pick while one is on its way is dropped. */
|
|
20
|
+
const runConfiguration = (work) => {
|
|
21
|
+
if (configuring.current) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
configuring.current = true;
|
|
25
|
+
setConfigurationPending(true);
|
|
26
|
+
void work()
|
|
27
|
+
.catch(showError)
|
|
28
|
+
.finally(() => {
|
|
29
|
+
configuring.current = false;
|
|
30
|
+
setConfigurationPending(false);
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const configure = (patch) => {
|
|
34
|
+
runConfiguration(() => actions.configure(chatId, patch).then(() => {
|
|
35
|
+
if (patch.selection) {
|
|
36
|
+
rememberChatSelection(info.provider, patch.selection);
|
|
37
|
+
}
|
|
38
|
+
if (patch.runtimeMode) {
|
|
39
|
+
rememberChatPreferences({ runtimeMode: patch.runtimeMode });
|
|
40
|
+
}
|
|
41
|
+
if (patch.account) {
|
|
42
|
+
rememberChatAccount(scope.id, info.provider, patch.account);
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
};
|
|
46
|
+
/* A model of another CLI re-points the whole chat; one of this CLI's own is a configure. */
|
|
47
|
+
const chooseModel = (provider, slug) => {
|
|
48
|
+
const selection = { model: slug, options: {} };
|
|
49
|
+
if (provider !== info.provider) {
|
|
50
|
+
runConfiguration(() => onRetarget(provider, selection).then(() => rememberChatSelection(provider, selection)));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
configure({ selection });
|
|
54
|
+
};
|
|
55
|
+
const stop = (shiftKey) => {
|
|
56
|
+
if (composerStopOf(shiftKey) === 'turn') {
|
|
57
|
+
void actions.stopTurn(chatId, false).catch(showError);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
host.confirm.stopSubagents(scope.id, chatId, () => void actions.stopTurn(chatId, true).catch(showError));
|
|
61
|
+
};
|
|
62
|
+
/* The host decides whether a turn is in the way; only its refusal asks the person first. */
|
|
63
|
+
const clearThread = async () => {
|
|
64
|
+
try {
|
|
65
|
+
await actions.clear(chatId, false);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (errorCode(e) === 'chat-busy') {
|
|
69
|
+
setConfirmClear(true);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
setNotice(t('composer.notice.clearFailed'));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const forceClearThread = async () => {
|
|
76
|
+
await actions.clear(chatId, true);
|
|
77
|
+
setConfirmClear(false);
|
|
78
|
+
};
|
|
79
|
+
const compact = () => {
|
|
80
|
+
void actions.compact(chatId).catch(() => setNotice(t('composer.notice.compactFailed')));
|
|
81
|
+
};
|
|
82
|
+
/* Runs one of the composer's own commands; false for a name it does not handle. */
|
|
83
|
+
const runCommand = (name) => {
|
|
84
|
+
switch (name) {
|
|
85
|
+
case 'model':
|
|
86
|
+
setModelPickerOpen(true);
|
|
87
|
+
return true;
|
|
88
|
+
case 'compact':
|
|
89
|
+
void actions.compact(chatId).catch(() => undefined);
|
|
90
|
+
return true;
|
|
91
|
+
case 'clear':
|
|
92
|
+
void clearThread();
|
|
93
|
+
return true;
|
|
94
|
+
default:
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
configurationPending,
|
|
100
|
+
configure,
|
|
101
|
+
chooseModel,
|
|
102
|
+
chooseAccount: (account) => configure({ account }),
|
|
103
|
+
stop,
|
|
104
|
+
compact,
|
|
105
|
+
runCommand,
|
|
106
|
+
modelPickerOpen,
|
|
107
|
+
setModelPickerOpen,
|
|
108
|
+
confirmClear,
|
|
109
|
+
cancelClear: () => setConfirmClear(false),
|
|
110
|
+
forceClearThread
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { ChatInfo, ProviderCapabilities } from '@adecore/agent-contracts';
|
|
2
|
+
export declare function useCompactionOffer(chatId: string, info: ChatInfo, compaction: ProviderCapabilities['compaction'] | undefined): import("../../logic/resume-compaction.js").ResumeCompactionOffer | null;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useNow } from '@adecore/ui';
|
|
2
|
+
import { useChatRow } from '../../../state/chats.js';
|
|
3
|
+
import { RESUME_COMPACTION_TOKENS, resumeCompactionOffer } from '../../logic/resume-compaction.js';
|
|
4
|
+
import { useResumeCompactionDismissal } from '../../resume-compaction-dismissals.js';
|
|
5
|
+
/* The offer to fold a long context before a chat that sat idle goes on, or null when there is none. */
|
|
6
|
+
export function useCompactionOffer(chatId, info, compaction) {
|
|
7
|
+
const order = useChatRow(chatId, (row) => row?.order);
|
|
8
|
+
const items = useChatRow(chatId, (row) => row?.structure);
|
|
9
|
+
const dismissedTurnId = useResumeCompactionDismissal(chatId);
|
|
10
|
+
// The cheap half of the offer, so a chat that can never make one keeps no timer.
|
|
11
|
+
const couldOffer = compaction !== undefined && compaction !== 'none' && info.usage.contextTokens >= RESUME_COMPACTION_TOKENS;
|
|
12
|
+
const now = useNow(60_000, couldOffer);
|
|
13
|
+
if (!couldOffer) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
return resumeCompactionOffer({
|
|
17
|
+
info,
|
|
18
|
+
compaction,
|
|
19
|
+
items: (order ?? []).map((id) => items?.[id]).filter((item) => item !== undefined),
|
|
20
|
+
dismissedTurnId,
|
|
21
|
+
now
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Dispatch, type RefObject, type SetStateAction } from 'react';
|
|
2
|
+
import { type ChatDraft } from '../../drafts.js';
|
|
3
|
+
import type { ComposerInputHandle } from '../ComposerInput.js';
|
|
4
|
+
export interface ComposerDraft {
|
|
5
|
+
draft: ChatDraft;
|
|
6
|
+
setDraft: Dispatch<SetStateAction<ChatDraft>>;
|
|
7
|
+
latest: RefObject<ChatDraft>;
|
|
8
|
+
setText(next: string, mentions?: string[], skills?: string[]): void;
|
|
9
|
+
clear(): void;
|
|
10
|
+
removeQuote(): void;
|
|
11
|
+
removeChat(id: string): void;
|
|
12
|
+
removeAttachment(index: number): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function useComposerDraft(chatId: string, inputRef: RefObject<ComposerInputHandle | null>): ComposerDraft;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { EMPTY_DRAFT, joinDraftText, readDraft, takeDraftOffers, writeDraft } from '../../drafts.js';
|
|
3
|
+
import { QuoteTakerContext } from '../quote-selection.js';
|
|
4
|
+
/* The chat's unsent prompt, kept in storage as it changes and open to text and quotes handed in from outside. */
|
|
5
|
+
export function useComposerDraft(chatId, inputRef) {
|
|
6
|
+
const [draft, setDraft] = useState(() => readDraft(chatId));
|
|
7
|
+
const latest = useRef(draft);
|
|
8
|
+
const registerQuoteTaker = useContext(QuoteTakerContext);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
latest.current = draft;
|
|
11
|
+
writeDraft(chatId, draft);
|
|
12
|
+
}, [chatId, draft]);
|
|
13
|
+
useEffect(() => takeDraftOffers(chatId, (offered) => {
|
|
14
|
+
setDraft((current) => ({ ...current, text: joinDraftText(current.text, offered) }));
|
|
15
|
+
inputRef.current?.focus();
|
|
16
|
+
}), [chatId, inputRef]);
|
|
17
|
+
useEffect(() => registerQuoteTaker?.((selected, itemId) => {
|
|
18
|
+
setDraft((current) => ({ ...current, quote: selected, quoteSource: itemId }));
|
|
19
|
+
inputRef.current?.focus();
|
|
20
|
+
}), [registerQuoteTaker, inputRef]);
|
|
21
|
+
const setText = (next, mentions = draft.mentions, chosen = draft.skills) => {
|
|
22
|
+
setDraft((current) => ({ ...current, text: next, mentions, skills: chosen }));
|
|
23
|
+
};
|
|
24
|
+
const clear = () => {
|
|
25
|
+
setDraft(EMPTY_DRAFT);
|
|
26
|
+
};
|
|
27
|
+
const removeQuote = () => {
|
|
28
|
+
setDraft((current) => ({ ...current, quote: '', quoteSource: null }));
|
|
29
|
+
inputRef.current?.focus();
|
|
30
|
+
};
|
|
31
|
+
const removeChat = (id) => {
|
|
32
|
+
setDraft((current) => ({ ...current, chats: current.chats.filter((entry) => entry !== id) }));
|
|
33
|
+
inputRef.current?.focus();
|
|
34
|
+
};
|
|
35
|
+
const removeAttachment = (index) => {
|
|
36
|
+
setDraft((current) => ({ ...current, attachments: current.attachments.filter((_, i) => i !== index) }));
|
|
37
|
+
};
|
|
38
|
+
return { draft, setDraft, latest, setText, clear, removeQuote, removeChat, removeAttachment };
|
|
39
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { EditorState } from '@codemirror/state';
|
|
3
|
+
import type { ChatInfo, ProviderCapabilities } from '@adecore/agent-contracts';
|
|
4
|
+
import { type MentionQuery, type TextRange } from '../../mentions.js';
|
|
5
|
+
import type { ComposerInputHandle } from '../ComposerInput.js';
|
|
6
|
+
import type { ComposerDraft } from './use-composer-draft.js';
|
|
7
|
+
export declare const LOCAL_COMMANDS: string[];
|
|
8
|
+
export interface ComposerCommand {
|
|
9
|
+
name: string;
|
|
10
|
+
hint: string;
|
|
11
|
+
local: boolean;
|
|
12
|
+
skill: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface MenusInput {
|
|
15
|
+
chatId: string;
|
|
16
|
+
info: ChatInfo;
|
|
17
|
+
capabilities: ProviderCapabilities | undefined;
|
|
18
|
+
providerName: string | undefined;
|
|
19
|
+
draft: ComposerDraft;
|
|
20
|
+
inputRef: RefObject<ComposerInputHandle | null>;
|
|
21
|
+
}
|
|
22
|
+
export declare function useComposerMenus({ chatId, info, capabilities, providerName, draft: { draft, setDraft, setText }, inputRef }: MenusInput): {
|
|
23
|
+
listId: string;
|
|
24
|
+
activeIndex: number;
|
|
25
|
+
inputAria: Record<string, string>;
|
|
26
|
+
hasSkills: boolean;
|
|
27
|
+
commandQuery: string | null;
|
|
28
|
+
commands: ComposerCommand[];
|
|
29
|
+
commandMenuOpen: boolean;
|
|
30
|
+
skillMatches: {
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
source: "plugin" | "project" | "user";
|
|
34
|
+
}[];
|
|
35
|
+
skillMenuOpen: boolean;
|
|
36
|
+
mention: MentionQuery | null;
|
|
37
|
+
mentionMenuOpen: boolean;
|
|
38
|
+
mentionCount: number;
|
|
39
|
+
chatMatches: import("../../index.js").ProjectChat[];
|
|
40
|
+
files: string[];
|
|
41
|
+
setMenuIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
42
|
+
trackTriggers: (value: string, selection: TextRange, state: EditorState) => void;
|
|
43
|
+
close: () => void;
|
|
44
|
+
chooseMention: (path: string) => void;
|
|
45
|
+
chooseChat: (id: string) => void;
|
|
46
|
+
chooseMentionAt: (index: number) => void;
|
|
47
|
+
chooseSkill: (name: string) => void;
|
|
48
|
+
pickCommand: (command: ComposerCommand, index: number) => void;
|
|
49
|
+
moveInMenu: (key: string, count: number) => boolean;
|
|
50
|
+
};
|
|
51
|
+
export type ComposerMenus = ReturnType<typeof useComposerMenus>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { useEffect, useId, useMemo, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
|
|
4
|
+
import { useChatHost } from '../../../host.js';
|
|
5
|
+
import { useChatScope } from '../../../scope.js';
|
|
6
|
+
import { chatSuggestions } from '../../chat-references.js';
|
|
7
|
+
import { usableSlashCommands } from '../../guards.js';
|
|
8
|
+
import { dropQuery, findMentionQuery, findSkillQuery, insertMention, insertSkill, mentionPick, NO_MENTION_SEARCH } from '../../mentions.js';
|
|
9
|
+
import { inCode } from './keys.js';
|
|
10
|
+
import { clampMenuIndex, composerMenuAria } from './menu-aria.js';
|
|
11
|
+
const SEARCH_DEBOUNCE_MS = 80;
|
|
12
|
+
// What fits above the composer without turning the picker into a file tree.
|
|
13
|
+
const MENTION_RESULTS = 8;
|
|
14
|
+
// The chats go above the files, so a few leave the files in view.
|
|
15
|
+
const CHAT_RESULTS = 4;
|
|
16
|
+
// Commands the composer handles itself; the CLI's own ones are sent through as text.
|
|
17
|
+
export const LOCAL_COMMANDS = ['model', 'compact', 'clear'];
|
|
18
|
+
function sameQuery(left, right) {
|
|
19
|
+
return left?.start === right?.start && left?.query === right?.query;
|
|
20
|
+
}
|
|
21
|
+
/*
|
|
22
|
+
* The pickers over the box: `/` for a command, `$` for a skill and `@` for a file or another chat.
|
|
23
|
+
* One is open at a time, and the arrow keys and the editor's ARIA attributes follow that one.
|
|
24
|
+
*/
|
|
25
|
+
export function useComposerMenus({ chatId, info, capabilities, providerName, draft: { draft, setDraft, setText }, inputRef }) {
|
|
26
|
+
const { t } = useTranslation('agent-chat');
|
|
27
|
+
const scope = useChatScope();
|
|
28
|
+
const host = useChatHost();
|
|
29
|
+
const { searchFiles } = host;
|
|
30
|
+
const chats = host.useReferableChats();
|
|
31
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
32
|
+
const [mention, setMention] = useState(null);
|
|
33
|
+
const [skillQuery, setSkillQuery] = useState(null);
|
|
34
|
+
const [skills, setSkills] = useState([]);
|
|
35
|
+
const [searched, setSearched] = useState(NO_MENTION_SEARCH);
|
|
36
|
+
const text = draft.text;
|
|
37
|
+
const goals = capabilities?.goals;
|
|
38
|
+
// Every keystroke inside an `@word` searches again; a slow answer must not overwrite a newer one.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (mention === null) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let stale = false;
|
|
44
|
+
const search = () => {
|
|
45
|
+
if (searchFiles === null) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
searchFiles(scope.id, info.cwd, mention.query, MENTION_RESULTS)
|
|
49
|
+
.then((files) => {
|
|
50
|
+
if (!stale) {
|
|
51
|
+
setSearched({ query: mention.query, files });
|
|
52
|
+
setMenuIndex(0);
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
.catch(() => undefined);
|
|
56
|
+
};
|
|
57
|
+
// A bare `@` is not a keystroke in a query. It opens the list of what is there, at once.
|
|
58
|
+
if (mention.query === '') {
|
|
59
|
+
search();
|
|
60
|
+
return () => {
|
|
61
|
+
stale = true;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const timer = setTimeout(search, SEARCH_DEBOUNCE_MS);
|
|
65
|
+
return () => {
|
|
66
|
+
stale = true;
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
};
|
|
69
|
+
}, [scope, info.cwd, mention, searchFiles]);
|
|
70
|
+
/*
|
|
71
|
+
* The host's skill list, once per chat, again once the CLI announced its own after the first message,
|
|
72
|
+
* and again when its process comes or goes, which is when a restart reads new skills.
|
|
73
|
+
*/
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
let stale = false;
|
|
76
|
+
scope.chats
|
|
77
|
+
.listSkills(chatId)
|
|
78
|
+
.then((found) => {
|
|
79
|
+
if (!stale) {
|
|
80
|
+
setSkills(found);
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.catch(() => undefined);
|
|
84
|
+
return () => {
|
|
85
|
+
stale = true;
|
|
86
|
+
};
|
|
87
|
+
}, [scope, chatId, info.skills, info.running]);
|
|
88
|
+
const commandQuery = text.startsWith('/') && !text.includes('\n') ? text.slice(1).trim().toLowerCase() : null;
|
|
89
|
+
const commands = useMemo(() => {
|
|
90
|
+
if (commandQuery === null) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const known = new Set(skills.map((skill) => skill.name));
|
|
94
|
+
const own = LOCAL_COMMANDS.map((name) => ({ name, hint: t(`composer.commands.${name}`), local: true, skill: false }));
|
|
95
|
+
// Every CLI with goals reads `/goal`, also one that lists no commands of its own.
|
|
96
|
+
const goal = goals === undefined ? [] : [{ name: 'goal', hint: t('composer.commands.goal'), local: false, skill: false }];
|
|
97
|
+
const cli = usableSlashCommands(info.slashCommands)
|
|
98
|
+
.filter((name) => !LOCAL_COMMANDS.includes(name) && (goals === undefined || name !== 'goal'))
|
|
99
|
+
.map((name) => ({
|
|
100
|
+
name,
|
|
101
|
+
hint: known.has(name) ? t('composer.commands.skill') : t('composer.commands.cli', { cli: providerName ?? t('composer.commands.anyCli') }),
|
|
102
|
+
local: false,
|
|
103
|
+
skill: known.has(name)
|
|
104
|
+
}));
|
|
105
|
+
return [...own, ...goal, ...cli].filter((command) => command.name.startsWith(commandQuery)).slice(0, 8);
|
|
106
|
+
}, [commandQuery, goals, info.slashCommands, providerName, skills, t]);
|
|
107
|
+
const skillMatches = useMemo(() => {
|
|
108
|
+
if (skillQuery === null) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const query = skillQuery.query.toLowerCase();
|
|
112
|
+
return skills.filter((skill) => skill.name.toLowerCase().includes(query)).slice(0, 8);
|
|
113
|
+
}, [skillQuery, skills]);
|
|
114
|
+
const commandMenuOpen = commandQuery !== null && commands.length > 0;
|
|
115
|
+
const skillMenuOpen = !commandMenuOpen && skillQuery !== null && skillMatches.length > 0;
|
|
116
|
+
const mentionMenuOpen = !commandMenuOpen && !skillMenuOpen && mention !== null;
|
|
117
|
+
// Results belong to the query that asked for them; a closed picker shows none while the next answer is on its way.
|
|
118
|
+
const files = mention === null ? [] : searched.files;
|
|
119
|
+
const chatMatches = useMemo(() => (mention === null ? [] : chatSuggestions(chats, mention.query, [chatId, ...draft.chats], CHAT_RESULTS)), [chatId, chats, draft.chats, mention]);
|
|
120
|
+
const mentionCount = chatMatches.length + files.length;
|
|
121
|
+
// One menu is open at a time, so they share an id.
|
|
122
|
+
const listId = useId();
|
|
123
|
+
const menuOpen = commandMenuOpen || skillMenuOpen || mentionMenuOpen;
|
|
124
|
+
const menuCount = commandMenuOpen ? commands.length : skillMenuOpen ? skillMatches.length : mentionCount;
|
|
125
|
+
// The list can shrink under a stale index; every reader takes the one that still points at an option.
|
|
126
|
+
const activeIndex = clampMenuIndex(menuIndex, menuCount);
|
|
127
|
+
const inputAria = useMemo(() => composerMenuAria({ label: t('composer.inputLabel'), listId, open: menuOpen, activeIndex, count: menuCount }), [t, listId, menuOpen, activeIndex, menuCount]);
|
|
128
|
+
/* Which picker the caret opens: `@` for a file, `$` for a skill, or neither. In code a sigil is just text. */
|
|
129
|
+
const trackTriggers = (value, selection, state) => {
|
|
130
|
+
const empty = selection.from === selection.to;
|
|
131
|
+
const tree = empty ? (ensureSyntaxTree(state, selection.from, 50) ?? syntaxTree(state)) : null;
|
|
132
|
+
const caret = tree !== null && !inCode(tree, value, selection.from) ? selection.from : null;
|
|
133
|
+
const skill = caret === null ? null : findSkillQuery(value, caret);
|
|
134
|
+
setSkillQuery((current) => (sameQuery(current, skill) ? current : skill));
|
|
135
|
+
// A CLI that does not expand `@path` gets the text as it is, so the picker stays out of the way.
|
|
136
|
+
const next = caret === null || capabilities?.mentions === false ? null : findMentionQuery(value, caret);
|
|
137
|
+
setMention((current) => (sameQuery(current, next) ? current : next));
|
|
138
|
+
};
|
|
139
|
+
// The inserted text reaches the editor with the render it causes, so the caret waits a frame for it.
|
|
140
|
+
const moveCaret = (caret) => {
|
|
141
|
+
requestAnimationFrame(() => inputRef.current?.setCaret(caret));
|
|
142
|
+
};
|
|
143
|
+
const close = () => {
|
|
144
|
+
setMention(null);
|
|
145
|
+
setSkillQuery(null);
|
|
146
|
+
};
|
|
147
|
+
const chooseMention = (path) => {
|
|
148
|
+
if (!mention) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const result = insertMention(text, mention, path);
|
|
152
|
+
setText(result.text, draft.mentions.includes(path) ? draft.mentions : [...draft.mentions, path]);
|
|
153
|
+
setMention(null);
|
|
154
|
+
moveCaret(result.caret);
|
|
155
|
+
};
|
|
156
|
+
/* A picked chat sits beside the text as a chip, so the `@query` that found it goes. */
|
|
157
|
+
const chooseChat = (id) => {
|
|
158
|
+
if (!mention) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const result = dropQuery(text, mention);
|
|
162
|
+
setDraft((current) => ({ ...current, text: result.text, chats: current.chats.includes(id) ? current.chats : [...current.chats, id] }));
|
|
163
|
+
setMention(null);
|
|
164
|
+
moveCaret(result.caret);
|
|
165
|
+
};
|
|
166
|
+
const chooseMentionAt = (index) => {
|
|
167
|
+
const pick = mention === null ? null : mentionPick(index, chatMatches, searched, mention.query);
|
|
168
|
+
if (pick?.kind === 'chat') {
|
|
169
|
+
chooseChat(pick.chat.id);
|
|
170
|
+
}
|
|
171
|
+
else if (pick?.kind === 'file') {
|
|
172
|
+
chooseMention(pick.path);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/*
|
|
176
|
+
* A picked skill becomes a `$name` chip in the text and a name on the send, which is what
|
|
177
|
+
* dispatches it. The `/` menu lands here too, so both spellings end up on the same path.
|
|
178
|
+
*/
|
|
179
|
+
const chooseSkill = (name) => {
|
|
180
|
+
const result = skillQuery ? insertSkill(text, skillQuery, name) : { text: `$${name} `, caret: name.length + 2 };
|
|
181
|
+
setText(result.text, draft.mentions, draft.skills.includes(name) ? draft.skills : [...draft.skills, name]);
|
|
182
|
+
setSkillQuery(null);
|
|
183
|
+
moveCaret(result.caret);
|
|
184
|
+
};
|
|
185
|
+
/* A clicked command; Tab completes one with a space after it instead. */
|
|
186
|
+
const pickCommand = (command, index) => {
|
|
187
|
+
setMenuIndex(index);
|
|
188
|
+
if (command.skill) {
|
|
189
|
+
chooseSkill(command.name);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
setText(`/${command.name}`);
|
|
193
|
+
inputRef.current?.focus();
|
|
194
|
+
};
|
|
195
|
+
/* The arrows walk an open picker round; true when the key was one of them. */
|
|
196
|
+
const moveInMenu = (key, count) => {
|
|
197
|
+
if (key === 'ArrowDown') {
|
|
198
|
+
setMenuIndex((i) => (i + 1) % count);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (key === 'ArrowUp') {
|
|
202
|
+
setMenuIndex((i) => (i - 1 + count) % count);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
listId,
|
|
209
|
+
activeIndex,
|
|
210
|
+
inputAria,
|
|
211
|
+
hasSkills: skills.length > 0,
|
|
212
|
+
commandQuery,
|
|
213
|
+
commands,
|
|
214
|
+
commandMenuOpen,
|
|
215
|
+
skillMatches,
|
|
216
|
+
skillMenuOpen,
|
|
217
|
+
mention,
|
|
218
|
+
mentionMenuOpen,
|
|
219
|
+
mentionCount,
|
|
220
|
+
chatMatches,
|
|
221
|
+
files,
|
|
222
|
+
setMenuIndex,
|
|
223
|
+
trackTriggers,
|
|
224
|
+
close,
|
|
225
|
+
chooseMention,
|
|
226
|
+
chooseChat,
|
|
227
|
+
chooseMentionAt,
|
|
228
|
+
chooseSkill,
|
|
229
|
+
pickCommand,
|
|
230
|
+
moveInMenu
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { ChatSendExtras } from '../../chat-client.js';
|
|
3
|
+
import { type ComposerSend } from '../../composer-send.js';
|
|
4
|
+
import type { PromptGuard } from '../../guards.js';
|
|
5
|
+
import { type StashedPrompt } from '../../stash.js';
|
|
6
|
+
import type { ComposerInputHandle } from '../ComposerInput.js';
|
|
7
|
+
import type { ChatControls } from './use-chat-controls.js';
|
|
8
|
+
import type { ComposerDraft } from './use-composer-draft.js';
|
|
9
|
+
import type { ComposerMenus } from './use-composer-menus.js';
|
|
10
|
+
import type { PromptHistory } from './use-prompt-history.js';
|
|
11
|
+
import type { ComposerPromptHold } from './use-prompt-hold.js';
|
|
12
|
+
interface SubmitInput {
|
|
13
|
+
chatId: string;
|
|
14
|
+
writable: boolean;
|
|
15
|
+
guard: PromptGuard;
|
|
16
|
+
steerable: boolean;
|
|
17
|
+
sendDelivery: 'queue' | 'steer';
|
|
18
|
+
draft: ComposerDraft;
|
|
19
|
+
menus: ComposerMenus;
|
|
20
|
+
controls: ChatControls;
|
|
21
|
+
history: PromptHistory;
|
|
22
|
+
hold: ComposerPromptHold;
|
|
23
|
+
inputRef: RefObject<ComposerInputHandle | null>;
|
|
24
|
+
setNotice(notice: string): void;
|
|
25
|
+
onSend(text: string, extras: ChatSendExtras): void | Promise<boolean>;
|
|
26
|
+
}
|
|
27
|
+
export declare function useComposerSubmit({ chatId, writable, guard, steerable, sendDelivery, draft: box, menus, controls, history, hold, inputRef, setNotice, onSend }: SubmitInput): {
|
|
28
|
+
submit: (how?: ComposerSend) => void;
|
|
29
|
+
restoreStashed: (prompt: StashedPrompt) => void;
|
|
30
|
+
toggleStash: () => void;
|
|
31
|
+
};
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useChatHost } from '../../../host.js';
|
|
3
|
+
import { useChatScope } from '../../../scope.js';
|
|
4
|
+
import { checkAttachmentLimits, uploadBytes } from '../../attachments.js';
|
|
5
|
+
import { registerComposerSender } from '../../composer-send.js';
|
|
6
|
+
import { isEmptyDraft } from '../../drafts.js';
|
|
7
|
+
import { presentMentions, presentSkills } from '../../mentions.js';
|
|
8
|
+
import { withQuote } from '../../quote.js';
|
|
9
|
+
import { stashDraft, useStash } from '../../stash.js';
|
|
10
|
+
import { scrollTimelineToEnd } from '../../timeline-scroll.js';
|
|
11
|
+
/* Sends the draft, or runs the command it names, and puts it away in the stash and back. */
|
|
12
|
+
export function useComposerSubmit({ chatId, writable, guard, steerable, sendDelivery, draft: box, menus, controls, history, hold, inputRef, setNotice, onSend }) {
|
|
13
|
+
const host = useChatHost();
|
|
14
|
+
const scope = useChatScope();
|
|
15
|
+
const { draft, setDraft } = box;
|
|
16
|
+
/* Opens the host's new chat once the host took the message; a host that answers nothing took it. */
|
|
17
|
+
const afterSend = (sent, andNew) => {
|
|
18
|
+
const openNewChat = host.openNewChat;
|
|
19
|
+
if (!andNew || openNewChat === null) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
void Promise.resolve(sent)
|
|
23
|
+
.then((taken) => {
|
|
24
|
+
if (taken !== false) {
|
|
25
|
+
openNewChat(scope.id, chatId);
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
.catch(() => undefined);
|
|
29
|
+
};
|
|
30
|
+
const submit = (how = 'default') => {
|
|
31
|
+
const alternate = how === 'alternate';
|
|
32
|
+
const delivery = steerable && (alternate ? sendDelivery === 'queue' : sendDelivery === 'steer') ? 'steer' : 'queue';
|
|
33
|
+
const { text, quote, quoteSource } = draft;
|
|
34
|
+
const trimmed = text.trim();
|
|
35
|
+
if (isEmptyDraft(draft) || !writable || guard.tooLong) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Recheck restored drafts and files that finished reading concurrently.
|
|
39
|
+
const rejected = checkAttachmentLimits(0, draft.attachments.map((attachment) => ({ ...attachment, bytes: uploadBytes(attachment) }))).rejected[0];
|
|
40
|
+
if (rejected) {
|
|
41
|
+
setNotice(`${rejected.name}: ${rejected.reason}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
hold.release('send');
|
|
45
|
+
const { commandQuery } = menus;
|
|
46
|
+
const chosen = menus.commands[menus.activeIndex];
|
|
47
|
+
if (commandQuery !== null && chosen?.local && controls.runCommand(chosen.name)) {
|
|
48
|
+
box.clear();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (commandQuery !== null && chosen?.skill) {
|
|
52
|
+
menus.chooseSkill(chosen.name);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Whoever sends wants to see what comes back, wherever they were reading.
|
|
56
|
+
scrollTimelineToEnd(chatId, 'instant');
|
|
57
|
+
if (commandQuery !== null && chosen && !chosen.local) {
|
|
58
|
+
afterSend(onSend(`/${chosen.name}`, {}), how === 'and-new');
|
|
59
|
+
box.clear();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const sent = onSend(withQuote(quote, trimmed), {
|
|
63
|
+
delivery,
|
|
64
|
+
mentions: presentMentions(trimmed, draft.mentions),
|
|
65
|
+
skills: presentSkills(trimmed, draft.skills),
|
|
66
|
+
chats: draft.chats,
|
|
67
|
+
quoteSource: quote !== '' && quoteSource !== null ? { itemId: quoteSource } : undefined,
|
|
68
|
+
attachments: draft.attachments
|
|
69
|
+
});
|
|
70
|
+
afterSend(sent, how === 'and-new');
|
|
71
|
+
box.clear();
|
|
72
|
+
menus.close();
|
|
73
|
+
history.forget();
|
|
74
|
+
};
|
|
75
|
+
// A menu row means the draft as it stands when it is picked, so the registration reads the latest submit.
|
|
76
|
+
const submitRef = useRef(submit);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
submitRef.current = submit;
|
|
79
|
+
});
|
|
80
|
+
useEffect(() => registerComposerSender(scope.keyOf(chatId), (how) => submitRef.current(how)), [scope, chatId]);
|
|
81
|
+
/*
|
|
82
|
+
* A stashed prompt comes back as text, mentions and skills; its files were never kept. Its quote
|
|
83
|
+
* keeps leading to its reply in the chat it came from, and is plain text in any other.
|
|
84
|
+
*/
|
|
85
|
+
const restoreStashed = (prompt) => {
|
|
86
|
+
const stashedQuote = prompt.quote;
|
|
87
|
+
const here = stashedQuote !== undefined && stashedQuote.chatId === chatId;
|
|
88
|
+
setDraft((current) => ({
|
|
89
|
+
...current,
|
|
90
|
+
text: stashedQuote === undefined || here ? prompt.text : withQuote(stashedQuote.text, prompt.text),
|
|
91
|
+
mentions: [...prompt.mentions],
|
|
92
|
+
skills: [...prompt.skills],
|
|
93
|
+
...(here ? { quote: stashedQuote.text, quoteSource: stashedQuote.itemId } : {})
|
|
94
|
+
}));
|
|
95
|
+
history.forget();
|
|
96
|
+
inputRef.current?.focus();
|
|
97
|
+
};
|
|
98
|
+
/* Cmd+S puts the draft away and clears the box; on an empty box the same key takes the last one back. */
|
|
99
|
+
const toggleStash = () => {
|
|
100
|
+
if (isEmptyDraft(draft)) {
|
|
101
|
+
const newest = useStash.getState().prompts[0];
|
|
102
|
+
if (newest) {
|
|
103
|
+
restoreStashed(newest);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (stashDraft(draft, chatId)) {
|
|
108
|
+
box.clear();
|
|
109
|
+
menus.close();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
return { submit, restoreStashed, toggleStash };
|
|
113
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type DragEvent, type RefObject } from 'react';
|
|
2
|
+
import { EditorView } from '@codemirror/view';
|
|
3
|
+
import type { ProviderCapabilities } from '@adecore/agent-contracts';
|
|
4
|
+
import type { ComposerInputHandle } from '../ComposerInput.js';
|
|
5
|
+
import type { ComposerDraft } from './use-composer-draft.js';
|
|
6
|
+
export declare const LEAVE_DROPS_TO_THE_CARD: import("@codemirror/state").Extension;
|
|
7
|
+
interface DraftFilesInput {
|
|
8
|
+
capabilities: ProviderCapabilities | undefined;
|
|
9
|
+
providerName: string | undefined;
|
|
10
|
+
writable: boolean;
|
|
11
|
+
draft: ComposerDraft;
|
|
12
|
+
inputRef: RefObject<ComposerInputHandle | null>;
|
|
13
|
+
setNotice(notice: string): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function useDraftFiles({ capabilities, providerName, writable, draft: { draft, setDraft, setText }, inputRef, setNotice }: DraftFilesInput): {
|
|
16
|
+
dragging: boolean;
|
|
17
|
+
dropTarget: {
|
|
18
|
+
'data-takes-drop': string;
|
|
19
|
+
onDragOver: (e: DragEvent<HTMLDivElement>) => void;
|
|
20
|
+
onDragLeave: (e: DragEvent<HTMLDivElement>) => void;
|
|
21
|
+
onDrop: (e: DragEvent<HTMLDivElement>) => void;
|
|
22
|
+
};
|
|
23
|
+
addFiles: (incoming: File[]) => void;
|
|
24
|
+
noteKey: (e: KeyboardEvent) => void;
|
|
25
|
+
onPaste: (e: ClipboardEvent, view: EditorView) => boolean;
|
|
26
|
+
};
|
|
27
|
+
export {};
|