@adecore/agents-react 0.23.1 → 0.24.1
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 +130 -907
- 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/surface.d.ts +1 -0
- package/dist/chat/ui/composer/surface.js +44 -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 +212 -1358
- 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/surface.ts +45 -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,289 @@
|
|
|
1
|
+
import { useEffect, useId, useMemo, useState, type RefObject } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
|
|
4
|
+
import type { EditorState } from '@codemirror/state';
|
|
5
|
+
import type { ChatInfo, ChatSkill, ProviderCapabilities } from '@adecore/agent-contracts';
|
|
6
|
+
import { useChatHost } from '../../../host';
|
|
7
|
+
import { useChatScope } from '../../../scope';
|
|
8
|
+
import { chatSuggestions } from '../../chat-references';
|
|
9
|
+
import { usableSlashCommands } from '../../guards';
|
|
10
|
+
import {
|
|
11
|
+
dropQuery,
|
|
12
|
+
findMentionQuery,
|
|
13
|
+
findSkillQuery,
|
|
14
|
+
insertMention,
|
|
15
|
+
insertSkill,
|
|
16
|
+
mentionPick,
|
|
17
|
+
NO_MENTION_SEARCH,
|
|
18
|
+
type MentionQuery,
|
|
19
|
+
type MentionSearch,
|
|
20
|
+
type TextRange
|
|
21
|
+
} from '../../mentions';
|
|
22
|
+
import type { ComposerInputHandle } from '../ComposerInput';
|
|
23
|
+
import type { ComposerDraft } from './use-composer-draft';
|
|
24
|
+
import { inCode } from './keys';
|
|
25
|
+
import { clampMenuIndex, composerMenuAria } from './menu-aria';
|
|
26
|
+
|
|
27
|
+
const SEARCH_DEBOUNCE_MS = 80;
|
|
28
|
+
// What fits above the composer without turning the picker into a file tree.
|
|
29
|
+
const MENTION_RESULTS = 8;
|
|
30
|
+
// The chats go above the files, so a few leave the files in view.
|
|
31
|
+
const CHAT_RESULTS = 4;
|
|
32
|
+
|
|
33
|
+
// Commands the composer handles itself; the CLI's own ones are sent through as text.
|
|
34
|
+
export const LOCAL_COMMANDS = ['model', 'compact', 'clear'];
|
|
35
|
+
|
|
36
|
+
export interface ComposerCommand {
|
|
37
|
+
name: string;
|
|
38
|
+
hint: string;
|
|
39
|
+
local: boolean;
|
|
40
|
+
skill: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface MenusInput {
|
|
44
|
+
chatId: string;
|
|
45
|
+
info: ChatInfo;
|
|
46
|
+
capabilities: ProviderCapabilities | undefined;
|
|
47
|
+
providerName: string | undefined;
|
|
48
|
+
draft: ComposerDraft;
|
|
49
|
+
inputRef: RefObject<ComposerInputHandle | null>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sameQuery(left: MentionQuery | null, right: MentionQuery | null): boolean {
|
|
53
|
+
return left?.start === right?.start && left?.query === right?.query;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* The pickers over the box: `/` for a command, `$` for a skill and `@` for a file or another chat.
|
|
58
|
+
* One is open at a time, and the arrow keys and the editor's ARIA attributes follow that one.
|
|
59
|
+
*/
|
|
60
|
+
export function useComposerMenus({ chatId, info, capabilities, providerName, draft: { draft, setDraft, setText }, inputRef }: MenusInput) {
|
|
61
|
+
const { t } = useTranslation('agent-chat');
|
|
62
|
+
const scope = useChatScope();
|
|
63
|
+
const host = useChatHost();
|
|
64
|
+
const { searchFiles } = host;
|
|
65
|
+
const chats = host.useReferableChats();
|
|
66
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
67
|
+
const [mention, setMention] = useState<MentionQuery | null>(null);
|
|
68
|
+
const [skillQuery, setSkillQuery] = useState<MentionQuery | null>(null);
|
|
69
|
+
const [skills, setSkills] = useState<ChatSkill[]>([]);
|
|
70
|
+
const [searched, setSearched] = useState<MentionSearch>(NO_MENTION_SEARCH);
|
|
71
|
+
const text = draft.text;
|
|
72
|
+
const goals = capabilities?.goals;
|
|
73
|
+
|
|
74
|
+
// Every keystroke inside an `@word` searches again; a slow answer must not overwrite a newer one.
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (mention === null) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
let stale = false;
|
|
80
|
+
const search = (): void => {
|
|
81
|
+
if (searchFiles === null) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
searchFiles(scope.id, info.cwd, mention.query, MENTION_RESULTS)
|
|
85
|
+
.then((files) => {
|
|
86
|
+
if (!stale) {
|
|
87
|
+
setSearched({ query: mention.query, files });
|
|
88
|
+
setMenuIndex(0);
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
.catch(() => undefined);
|
|
92
|
+
};
|
|
93
|
+
// A bare `@` is not a keystroke in a query. It opens the list of what is there, at once.
|
|
94
|
+
if (mention.query === '') {
|
|
95
|
+
search();
|
|
96
|
+
return () => {
|
|
97
|
+
stale = true;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const timer = setTimeout(search, SEARCH_DEBOUNCE_MS);
|
|
101
|
+
return () => {
|
|
102
|
+
stale = true;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
};
|
|
105
|
+
}, [scope, info.cwd, mention, searchFiles]);
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* The host's skill list, once per chat, again once the CLI announced its own after the first message,
|
|
109
|
+
* and again when its process comes or goes, which is when a restart reads new skills.
|
|
110
|
+
*/
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
let stale = false;
|
|
113
|
+
scope.chats
|
|
114
|
+
.listSkills(chatId)
|
|
115
|
+
.then((found) => {
|
|
116
|
+
if (!stale) {
|
|
117
|
+
setSkills(found);
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.catch(() => undefined);
|
|
121
|
+
return () => {
|
|
122
|
+
stale = true;
|
|
123
|
+
};
|
|
124
|
+
}, [scope, chatId, info.skills, info.running]);
|
|
125
|
+
|
|
126
|
+
const commandQuery = text.startsWith('/') && !text.includes('\n') ? text.slice(1).trim().toLowerCase() : null;
|
|
127
|
+
const commands = useMemo((): ComposerCommand[] => {
|
|
128
|
+
if (commandQuery === null) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
const known = new Set(skills.map((skill) => skill.name));
|
|
132
|
+
const own = LOCAL_COMMANDS.map((name) => ({ name, hint: t(`composer.commands.${name}`), local: true, skill: false }));
|
|
133
|
+
// Every CLI with goals reads `/goal`, also one that lists no commands of its own.
|
|
134
|
+
const goal = goals === undefined ? [] : [{ name: 'goal', hint: t('composer.commands.goal'), local: false, skill: false }];
|
|
135
|
+
const cli = usableSlashCommands(info.slashCommands)
|
|
136
|
+
.filter((name) => !LOCAL_COMMANDS.includes(name) && (goals === undefined || name !== 'goal'))
|
|
137
|
+
.map((name) => ({
|
|
138
|
+
name,
|
|
139
|
+
hint: known.has(name) ? t('composer.commands.skill') : t('composer.commands.cli', { cli: providerName ?? t('composer.commands.anyCli') }),
|
|
140
|
+
local: false,
|
|
141
|
+
skill: known.has(name)
|
|
142
|
+
}));
|
|
143
|
+
return [...own, ...goal, ...cli].filter((command) => command.name.startsWith(commandQuery)).slice(0, 8);
|
|
144
|
+
}, [commandQuery, goals, info.slashCommands, providerName, skills, t]);
|
|
145
|
+
|
|
146
|
+
const skillMatches = useMemo(() => {
|
|
147
|
+
if (skillQuery === null) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
const query = skillQuery.query.toLowerCase();
|
|
151
|
+
return skills.filter((skill) => skill.name.toLowerCase().includes(query)).slice(0, 8);
|
|
152
|
+
}, [skillQuery, skills]);
|
|
153
|
+
|
|
154
|
+
const commandMenuOpen = commandQuery !== null && commands.length > 0;
|
|
155
|
+
const skillMenuOpen = !commandMenuOpen && skillQuery !== null && skillMatches.length > 0;
|
|
156
|
+
const mentionMenuOpen = !commandMenuOpen && !skillMenuOpen && mention !== null;
|
|
157
|
+
// Results belong to the query that asked for them; a closed picker shows none while the next answer is on its way.
|
|
158
|
+
const files = mention === null ? [] : searched.files;
|
|
159
|
+
const chatMatches = useMemo(
|
|
160
|
+
() => (mention === null ? [] : chatSuggestions(chats, mention.query, [chatId, ...draft.chats], CHAT_RESULTS)),
|
|
161
|
+
[chatId, chats, draft.chats, mention]
|
|
162
|
+
);
|
|
163
|
+
const mentionCount = chatMatches.length + files.length;
|
|
164
|
+
// One menu is open at a time, so they share an id.
|
|
165
|
+
const listId = useId();
|
|
166
|
+
const menuOpen = commandMenuOpen || skillMenuOpen || mentionMenuOpen;
|
|
167
|
+
const menuCount = commandMenuOpen ? commands.length : skillMenuOpen ? skillMatches.length : mentionCount;
|
|
168
|
+
// The list can shrink under a stale index; every reader takes the one that still points at an option.
|
|
169
|
+
const activeIndex = clampMenuIndex(menuIndex, menuCount);
|
|
170
|
+
const inputAria = useMemo(
|
|
171
|
+
() => composerMenuAria({ label: t('composer.inputLabel'), listId, open: menuOpen, activeIndex, count: menuCount }),
|
|
172
|
+
[t, listId, menuOpen, activeIndex, menuCount]
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
/* Which picker the caret opens: `@` for a file, `$` for a skill, or neither. In code a sigil is just text. */
|
|
176
|
+
const trackTriggers = (value: string, selection: TextRange, state: EditorState): void => {
|
|
177
|
+
const empty = selection.from === selection.to;
|
|
178
|
+
const tree = empty ? (ensureSyntaxTree(state, selection.from, 50) ?? syntaxTree(state)) : null;
|
|
179
|
+
const caret = tree !== null && !inCode(tree, value, selection.from) ? selection.from : null;
|
|
180
|
+
const skill = caret === null ? null : findSkillQuery(value, caret);
|
|
181
|
+
setSkillQuery((current) => (sameQuery(current, skill) ? current : skill));
|
|
182
|
+
// A CLI that does not expand `@path` gets the text as it is, so the picker stays out of the way.
|
|
183
|
+
const next = caret === null || capabilities?.mentions === false ? null : findMentionQuery(value, caret);
|
|
184
|
+
setMention((current) => (sameQuery(current, next) ? current : next));
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// The inserted text reaches the editor with the render it causes, so the caret waits a frame for it.
|
|
188
|
+
const moveCaret = (caret: number): void => {
|
|
189
|
+
requestAnimationFrame(() => inputRef.current?.setCaret(caret));
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const close = (): void => {
|
|
193
|
+
setMention(null);
|
|
194
|
+
setSkillQuery(null);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const chooseMention = (path: string): void => {
|
|
198
|
+
if (!mention) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const result = insertMention(text, mention, path);
|
|
202
|
+
setText(result.text, draft.mentions.includes(path) ? draft.mentions : [...draft.mentions, path]);
|
|
203
|
+
setMention(null);
|
|
204
|
+
moveCaret(result.caret);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/* A picked chat sits beside the text as a chip, so the `@query` that found it goes. */
|
|
208
|
+
const chooseChat = (id: string): void => {
|
|
209
|
+
if (!mention) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const result = dropQuery(text, mention);
|
|
213
|
+
setDraft((current) => ({ ...current, text: result.text, chats: current.chats.includes(id) ? current.chats : [...current.chats, id] }));
|
|
214
|
+
setMention(null);
|
|
215
|
+
moveCaret(result.caret);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const chooseMentionAt = (index: number): void => {
|
|
219
|
+
const pick = mention === null ? null : mentionPick(index, chatMatches, searched, mention.query);
|
|
220
|
+
if (pick?.kind === 'chat') {
|
|
221
|
+
chooseChat(pick.chat.id);
|
|
222
|
+
} else if (pick?.kind === 'file') {
|
|
223
|
+
chooseMention(pick.path);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* A picked skill becomes a `$name` chip in the text and a name on the send, which is what
|
|
229
|
+
* dispatches it. The `/` menu lands here too, so both spellings end up on the same path.
|
|
230
|
+
*/
|
|
231
|
+
const chooseSkill = (name: string): void => {
|
|
232
|
+
const result = skillQuery ? insertSkill(text, skillQuery, name) : { text: `$${name} `, caret: name.length + 2 };
|
|
233
|
+
setText(result.text, draft.mentions, draft.skills.includes(name) ? draft.skills : [...draft.skills, name]);
|
|
234
|
+
setSkillQuery(null);
|
|
235
|
+
moveCaret(result.caret);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/* A clicked command; Tab completes one with a space after it instead. */
|
|
239
|
+
const pickCommand = (command: ComposerCommand, index: number): void => {
|
|
240
|
+
setMenuIndex(index);
|
|
241
|
+
if (command.skill) {
|
|
242
|
+
chooseSkill(command.name);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
setText(`/${command.name}`);
|
|
246
|
+
inputRef.current?.focus();
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/* The arrows walk an open picker round; true when the key was one of them. */
|
|
250
|
+
const moveInMenu = (key: string, count: number): boolean => {
|
|
251
|
+
if (key === 'ArrowDown') {
|
|
252
|
+
setMenuIndex((i) => (i + 1) % count);
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
if (key === 'ArrowUp') {
|
|
256
|
+
setMenuIndex((i) => (i - 1 + count) % count);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
listId,
|
|
264
|
+
activeIndex,
|
|
265
|
+
inputAria,
|
|
266
|
+
hasSkills: skills.length > 0,
|
|
267
|
+
commandQuery,
|
|
268
|
+
commands,
|
|
269
|
+
commandMenuOpen,
|
|
270
|
+
skillMatches,
|
|
271
|
+
skillMenuOpen,
|
|
272
|
+
mention,
|
|
273
|
+
mentionMenuOpen,
|
|
274
|
+
mentionCount,
|
|
275
|
+
chatMatches,
|
|
276
|
+
files,
|
|
277
|
+
setMenuIndex,
|
|
278
|
+
trackTriggers,
|
|
279
|
+
close,
|
|
280
|
+
chooseMention,
|
|
281
|
+
chooseChat,
|
|
282
|
+
chooseMentionAt,
|
|
283
|
+
chooseSkill,
|
|
284
|
+
pickCommand,
|
|
285
|
+
moveInMenu
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export type ComposerMenus = ReturnType<typeof useComposerMenus>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useEffect, useRef, type RefObject } from 'react';
|
|
2
|
+
import { useChatHost } from '../../../host';
|
|
3
|
+
import { useChatScope } from '../../../scope';
|
|
4
|
+
import { checkAttachmentLimits, uploadBytes } from '../../attachments';
|
|
5
|
+
import type { ChatSendExtras } from '../../chat-client';
|
|
6
|
+
import { registerComposerSender, type ComposerSend } from '../../composer-send';
|
|
7
|
+
import { isEmptyDraft } from '../../drafts';
|
|
8
|
+
import type { PromptGuard } from '../../guards';
|
|
9
|
+
import { presentMentions, presentSkills } from '../../mentions';
|
|
10
|
+
import { withQuote } from '../../quote';
|
|
11
|
+
import { stashDraft, useStash, type StashedPrompt } from '../../stash';
|
|
12
|
+
import { scrollTimelineToEnd } from '../../timeline-scroll';
|
|
13
|
+
import type { ComposerInputHandle } from '../ComposerInput';
|
|
14
|
+
import type { ChatControls } from './use-chat-controls';
|
|
15
|
+
import type { ComposerDraft } from './use-composer-draft';
|
|
16
|
+
import type { ComposerMenus } from './use-composer-menus';
|
|
17
|
+
import type { PromptHistory } from './use-prompt-history';
|
|
18
|
+
import type { ComposerPromptHold } from './use-prompt-hold';
|
|
19
|
+
|
|
20
|
+
interface SubmitInput {
|
|
21
|
+
chatId: string;
|
|
22
|
+
writable: boolean;
|
|
23
|
+
guard: PromptGuard;
|
|
24
|
+
/* Whether the provider lets a send join a running turn; without it every send queues. */
|
|
25
|
+
steerable: boolean;
|
|
26
|
+
/* Where a running turn takes a send by default; Alt sends the other way. */
|
|
27
|
+
sendDelivery: 'queue' | 'steer';
|
|
28
|
+
draft: ComposerDraft;
|
|
29
|
+
menus: ComposerMenus;
|
|
30
|
+
controls: ChatControls;
|
|
31
|
+
history: PromptHistory;
|
|
32
|
+
hold: ComposerPromptHold;
|
|
33
|
+
inputRef: RefObject<ComposerInputHandle | null>;
|
|
34
|
+
setNotice(notice: string): void;
|
|
35
|
+
onSend(text: string, extras: ChatSendExtras): void | Promise<boolean>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Sends the draft, or runs the command it names, and puts it away in the stash and back. */
|
|
39
|
+
export function useComposerSubmit({
|
|
40
|
+
chatId,
|
|
41
|
+
writable,
|
|
42
|
+
guard,
|
|
43
|
+
steerable,
|
|
44
|
+
sendDelivery,
|
|
45
|
+
draft: box,
|
|
46
|
+
menus,
|
|
47
|
+
controls,
|
|
48
|
+
history,
|
|
49
|
+
hold,
|
|
50
|
+
inputRef,
|
|
51
|
+
setNotice,
|
|
52
|
+
onSend
|
|
53
|
+
}: SubmitInput) {
|
|
54
|
+
const host = useChatHost();
|
|
55
|
+
const scope = useChatScope();
|
|
56
|
+
const { draft, setDraft } = box;
|
|
57
|
+
|
|
58
|
+
/* Opens the host's new chat once the host took the message; a host that answers nothing took it. */
|
|
59
|
+
const afterSend = (sent: void | Promise<boolean>, andNew: boolean): void => {
|
|
60
|
+
const openNewChat = host.openNewChat;
|
|
61
|
+
if (!andNew || openNewChat === null) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
void Promise.resolve(sent)
|
|
65
|
+
.then((taken) => {
|
|
66
|
+
if (taken !== false) {
|
|
67
|
+
openNewChat(scope.id, chatId);
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.catch(() => undefined);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const submit = (how: ComposerSend = 'default'): void => {
|
|
74
|
+
const alternate = how === 'alternate';
|
|
75
|
+
const delivery = steerable && (alternate ? sendDelivery === 'queue' : sendDelivery === 'steer') ? 'steer' : 'queue';
|
|
76
|
+
const { text, quote, quoteSource } = draft;
|
|
77
|
+
const trimmed = text.trim();
|
|
78
|
+
if (isEmptyDraft(draft) || !writable || guard.tooLong) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Recheck restored drafts and files that finished reading concurrently.
|
|
82
|
+
const rejected = checkAttachmentLimits(
|
|
83
|
+
0,
|
|
84
|
+
draft.attachments.map((attachment) => ({ ...attachment, bytes: uploadBytes(attachment) }))
|
|
85
|
+
).rejected[0];
|
|
86
|
+
if (rejected) {
|
|
87
|
+
setNotice(`${rejected.name}: ${rejected.reason}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
hold.release('send');
|
|
91
|
+
const { commandQuery } = menus;
|
|
92
|
+
const chosen = menus.commands[menus.activeIndex];
|
|
93
|
+
if (commandQuery !== null && chosen?.local && controls.runCommand(chosen.name)) {
|
|
94
|
+
box.clear();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (commandQuery !== null && chosen?.skill) {
|
|
98
|
+
menus.chooseSkill(chosen.name);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Whoever sends wants to see what comes back, wherever they were reading.
|
|
102
|
+
scrollTimelineToEnd(chatId, 'instant');
|
|
103
|
+
if (commandQuery !== null && chosen && !chosen.local) {
|
|
104
|
+
afterSend(onSend(`/${chosen.name}`, {}), how === 'and-new');
|
|
105
|
+
box.clear();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const sent = onSend(withQuote(quote, trimmed), {
|
|
109
|
+
delivery,
|
|
110
|
+
mentions: presentMentions(trimmed, draft.mentions),
|
|
111
|
+
skills: presentSkills(trimmed, draft.skills),
|
|
112
|
+
chats: draft.chats,
|
|
113
|
+
quoteSource: quote !== '' && quoteSource !== null ? { itemId: quoteSource } : undefined,
|
|
114
|
+
attachments: draft.attachments
|
|
115
|
+
});
|
|
116
|
+
afterSend(sent, how === 'and-new');
|
|
117
|
+
box.clear();
|
|
118
|
+
menus.close();
|
|
119
|
+
history.forget();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// A menu row means the draft as it stands when it is picked, so the registration reads the latest submit.
|
|
123
|
+
const submitRef = useRef(submit);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
submitRef.current = submit;
|
|
126
|
+
});
|
|
127
|
+
useEffect(() => registerComposerSender(scope.keyOf(chatId), (how) => submitRef.current(how)), [scope, chatId]);
|
|
128
|
+
|
|
129
|
+
/*
|
|
130
|
+
* A stashed prompt comes back as text, mentions and skills; its files were never kept. Its quote
|
|
131
|
+
* keeps leading to its reply in the chat it came from, and is plain text in any other.
|
|
132
|
+
*/
|
|
133
|
+
const restoreStashed = (prompt: StashedPrompt): void => {
|
|
134
|
+
const stashedQuote = prompt.quote;
|
|
135
|
+
const here = stashedQuote !== undefined && stashedQuote.chatId === chatId;
|
|
136
|
+
setDraft((current) => ({
|
|
137
|
+
...current,
|
|
138
|
+
text: stashedQuote === undefined || here ? prompt.text : withQuote(stashedQuote.text, prompt.text),
|
|
139
|
+
mentions: [...prompt.mentions],
|
|
140
|
+
skills: [...prompt.skills],
|
|
141
|
+
...(here ? { quote: stashedQuote.text, quoteSource: stashedQuote.itemId } : {})
|
|
142
|
+
}));
|
|
143
|
+
history.forget();
|
|
144
|
+
inputRef.current?.focus();
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/* Cmd+S puts the draft away and clears the box; on an empty box the same key takes the last one back. */
|
|
148
|
+
const toggleStash = (): void => {
|
|
149
|
+
if (isEmptyDraft(draft)) {
|
|
150
|
+
const newest = useStash.getState().prompts[0];
|
|
151
|
+
if (newest) {
|
|
152
|
+
restoreStashed(newest);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (stashDraft(draft, chatId)) {
|
|
157
|
+
box.clear();
|
|
158
|
+
menus.close();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return { submit, restoreStashed, toggleStash };
|
|
163
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useRef, useState, type DragEvent, type RefObject } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { EditorView } from '@codemirror/view';
|
|
4
|
+
import type { ProviderCapabilities } from '@adecore/agent-contracts';
|
|
5
|
+
import { useChatHost } from '../../../host';
|
|
6
|
+
import { checkAttachmentLimits, filesOf, readAttachments, uploadBytes } from '../../attachments';
|
|
7
|
+
import { pasteBecomesAttachment, pastedTextName } from '../../guards';
|
|
8
|
+
import { carriesMentions, droppedMentions, pastedMentions } from '../../mentions';
|
|
9
|
+
import type { ComposerInputHandle } from '../ComposerInput';
|
|
10
|
+
import type { ComposerDraft } from './use-composer-draft';
|
|
11
|
+
|
|
12
|
+
/* The card turns dropped files and paths into attachments and chips; CodeMirror would also paste their text at the drop point. */
|
|
13
|
+
export const LEAVE_DROPS_TO_THE_CARD = EditorView.domEventHandlers({
|
|
14
|
+
drop: (event) => event.dataTransfer !== null && (event.dataTransfer.types.includes('Files') || carriesMentions(event.dataTransfer.types))
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
interface DraftFilesInput {
|
|
18
|
+
capabilities: ProviderCapabilities | undefined;
|
|
19
|
+
providerName: string | undefined;
|
|
20
|
+
writable: boolean;
|
|
21
|
+
draft: ComposerDraft;
|
|
22
|
+
inputRef: RefObject<ComposerInputHandle | null>;
|
|
23
|
+
setNotice(notice: string): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* What reaches the draft besides typing: files as attachments, dragged paths as mentions, and a long paste as a file of its own. */
|
|
27
|
+
export function useDraftFiles({ capabilities, providerName, writable, draft: { draft, setDraft, setText }, inputRef, setNotice }: DraftFilesInput) {
|
|
28
|
+
const { t } = useTranslation('agent-chat');
|
|
29
|
+
const { isApplePlatform } = useChatHost();
|
|
30
|
+
const [dragging, setDragging] = useState(false);
|
|
31
|
+
// A paste event says nothing about the keys behind it, so the key that asked for text inline is remembered here.
|
|
32
|
+
const pasteInline = useRef(false);
|
|
33
|
+
|
|
34
|
+
/* Paths dragged in from a list of files. With no mention query to insert into, they land at the end of the prompt. */
|
|
35
|
+
const addMentions = (paths: string[]): void => {
|
|
36
|
+
const fresh = paths.filter((path) => path !== '');
|
|
37
|
+
if (fresh.length === 0 || capabilities?.mentions === false) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const appended = fresh.map((path) => `@${path}`).join(' ');
|
|
41
|
+
const before = draft.text.replace(/\s+$/, '');
|
|
42
|
+
setText(before === '' ? `${appended} ` : `${before} ${appended} `, [...draft.mentions, ...fresh.filter((path) => !draft.mentions.includes(path))]);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const addFiles = (incoming: File[]): void => {
|
|
46
|
+
if (incoming.length === 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (capabilities?.attachments === false) {
|
|
50
|
+
setNotice(t('composer.notice.noAttachments', { agent: providerName ?? t('composer.notice.thisAgent') }));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const checked = checkAttachmentLimits(
|
|
54
|
+
draft.attachments.length,
|
|
55
|
+
incoming.map((file) => ({ name: file.name, mime: file.type, bytes: file.size, file })),
|
|
56
|
+
draft.attachments.reduce((bytes, attachment) => bytes + uploadBytes(attachment), 0)
|
|
57
|
+
);
|
|
58
|
+
if (checked.rejected[0]) {
|
|
59
|
+
setNotice(t('composer.notice.rejected', { name: checked.rejected[0].name || t('composer.notice.thatFile'), reason: checked.rejected[0].reason }));
|
|
60
|
+
}
|
|
61
|
+
if (checked.accepted.length === 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
readAttachments(checked.accepted.map((entry) => entry.file))
|
|
65
|
+
.then((attachments) => setDraft((current) => ({ ...current, attachments: [...current.attachments, ...attachments] })))
|
|
66
|
+
.catch(() => setNotice(t('composer.notice.unreadable')));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/* Shift with the paste shortcut pastes a large text inline after all; any other key forgets it. */
|
|
70
|
+
const noteKey = (e: KeyboardEvent): void => {
|
|
71
|
+
pasteInline.current = e.shiftKey && e.key.toLowerCase() === 'v' && (isApplePlatform() ? e.metaKey : e.ctrlKey);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/* Files become attachments and a long text a file of its own; anything else the editor pastes as plain text. */
|
|
75
|
+
const onPaste = (e: ClipboardEvent, view: EditorView): boolean => {
|
|
76
|
+
const data = e.clipboardData;
|
|
77
|
+
if (!data) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const pasted = filesOf(data);
|
|
81
|
+
if (pasted.length > 0) {
|
|
82
|
+
addFiles(pasted);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
const inline = pasteInline.current;
|
|
86
|
+
pasteInline.current = false;
|
|
87
|
+
const clip = data.getData('text/plain');
|
|
88
|
+
const mentions = capabilities?.mentions === false ? [] : pastedMentions(clip);
|
|
89
|
+
if (mentions.length > 0) {
|
|
90
|
+
// The editor's own change reads the draft as it was rendered, so the mentions join it after that change.
|
|
91
|
+
view.dispatch(view.state.replaceSelection(clip), { scrollIntoView: true, userEvent: 'input.paste' });
|
|
92
|
+
setDraft((current) => ({ ...current, mentions: [...current.mentions, ...mentions.filter((path) => !current.mentions.includes(path))] }));
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
// Without attachments there is nowhere else for the text to go, so it pastes as it always did.
|
|
96
|
+
if (inline || capabilities?.attachments === false || !pasteBecomesAttachment(clip)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
// The paste still replaces what was selected; it just puts nothing in its place.
|
|
100
|
+
const { from, to } = view.state.selection.main;
|
|
101
|
+
if (from !== to) {
|
|
102
|
+
view.dispatch({ changes: { from, to }, userEvent: 'delete' });
|
|
103
|
+
}
|
|
104
|
+
const name = pastedTextName(draft.attachments.map((attachment) => attachment.name));
|
|
105
|
+
addFiles([new File([clip], name, { type: 'text/plain' })]);
|
|
106
|
+
return true;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/* The whole card takes a dropped file, so a layout around it that takes drops of its own leaves it alone. */
|
|
110
|
+
const dropTarget = {
|
|
111
|
+
'data-takes-drop': 'all',
|
|
112
|
+
onDragOver: (e: DragEvent<HTMLDivElement>): void => {
|
|
113
|
+
if (writable && (e.dataTransfer.types.includes('Files') || carriesMentions(e.dataTransfer.types))) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
e.stopPropagation();
|
|
116
|
+
setDragging(true);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
onDragLeave: (e: DragEvent<HTMLDivElement>): void => {
|
|
120
|
+
if (!e.currentTarget.contains(e.relatedTarget as Node | null)) {
|
|
121
|
+
setDragging(false);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
onDrop: (e: DragEvent<HTMLDivElement>): void => {
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
e.stopPropagation();
|
|
127
|
+
setDragging(false);
|
|
128
|
+
addMentions(droppedMentions(e.dataTransfer));
|
|
129
|
+
addFiles(filesOf(e.dataTransfer));
|
|
130
|
+
inputRef.current?.focus();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return { dragging, dropTarget, addFiles, noteKey, onPaste };
|
|
135
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const NOTICE_MS = 4000;
|
|
4
|
+
|
|
5
|
+
export interface ComposerNotice {
|
|
6
|
+
notice: string | null;
|
|
7
|
+
setNotice(notice: string | null): void;
|
|
8
|
+
showError(error: unknown): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* A line under the box that says what went wrong, gone again after a few seconds. */
|
|
12
|
+
export function useNotice(): ComposerNotice {
|
|
13
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!notice) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const timer = setTimeout(() => setNotice(null), NOTICE_MS);
|
|
20
|
+
return () => clearTimeout(timer);
|
|
21
|
+
}, [notice]);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
notice,
|
|
25
|
+
setNotice,
|
|
26
|
+
showError: (error) => setNotice(error instanceof Error ? error.message : String(error))
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import type { ChatApprovalItem, ChatQuestionItem } from '@adecore/agent-contracts';
|
|
3
|
+
import { useChatRow, waitingRequestsOf } from '../../../state/chats';
|
|
4
|
+
|
|
5
|
+
/* The approvals and then the questions the chat waits on, which take the composer's place until answered. */
|
|
6
|
+
export function usePendingRequests(chatId: string): Array<ChatApprovalItem | ChatQuestionItem> {
|
|
7
|
+
// The structure, which a delta leaves alone. The prompts and the requests are items of their own.
|
|
8
|
+
const structure = useChatRow(chatId, (row) => row?.structure);
|
|
9
|
+
const order = useChatRow(chatId, (row) => row?.order);
|
|
10
|
+
const waitingBefore = useChatRow(chatId, (row) => row?.waitingBefore);
|
|
11
|
+
return useMemo(() => {
|
|
12
|
+
const approvals: ChatApprovalItem[] = [];
|
|
13
|
+
const questions: ChatQuestionItem[] = [];
|
|
14
|
+
for (const item of waitingRequestsOf(structure && order ? { structure, order, waitingBefore } : undefined)) {
|
|
15
|
+
if (item.kind === 'approval') {
|
|
16
|
+
approvals.push(item);
|
|
17
|
+
} else {
|
|
18
|
+
questions.push(item);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return [...approvals, ...questions];
|
|
22
|
+
}, [structure, order, waitingBefore]);
|
|
23
|
+
}
|