@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,147 @@
|
|
|
1
|
+
import { useRef, type Ref } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ArrowUp, Plus, Square } from 'lucide-react';
|
|
4
|
+
import type { ChatInfo } from '@adecore/agent-contracts';
|
|
5
|
+
import { ErrorBoundary, Icon, Tooltip, type Shortcut } from '@adecore/ui';
|
|
6
|
+
import { useChatHost } from '../../../host';
|
|
7
|
+
import { useChatScope } from '../../../scope';
|
|
8
|
+
import { useProviders } from '../../../state/providers';
|
|
9
|
+
import { useAccountChoice } from '../../account-choice';
|
|
10
|
+
import type { StashedPrompt } from '../../stash';
|
|
11
|
+
import { composerStopLabel } from '../../subagent-list';
|
|
12
|
+
import { StashPicker } from '../Pickers';
|
|
13
|
+
import { RunSettings } from '../RunSettings';
|
|
14
|
+
import type { ChatControls } from './use-chat-controls';
|
|
15
|
+
|
|
16
|
+
interface SendProps {
|
|
17
|
+
/* Absent while a turn runs and the box is empty, when the stop button stands alone. */
|
|
18
|
+
visible: boolean;
|
|
19
|
+
disabled: boolean;
|
|
20
|
+
label: string;
|
|
21
|
+
kbd: Shortcut;
|
|
22
|
+
onSend(alternate: boolean): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ComposerControlsProps {
|
|
26
|
+
chatId: string;
|
|
27
|
+
info: ChatInfo;
|
|
28
|
+
providerFixed: boolean;
|
|
29
|
+
writable: boolean;
|
|
30
|
+
attachable: boolean;
|
|
31
|
+
controls: ChatControls;
|
|
32
|
+
send: SendProps;
|
|
33
|
+
dictationToolbar: Ref<HTMLDivElement>;
|
|
34
|
+
onFiles(files: File[]): void;
|
|
35
|
+
onRestore(prompt: StashedPrompt): void;
|
|
36
|
+
insert(text: string): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* The row under the box: attach, the chat's settings, the stash, the app's own control, and stop and send. */
|
|
40
|
+
export function ComposerControls({
|
|
41
|
+
chatId,
|
|
42
|
+
info,
|
|
43
|
+
providerFixed,
|
|
44
|
+
writable,
|
|
45
|
+
attachable,
|
|
46
|
+
controls,
|
|
47
|
+
send,
|
|
48
|
+
dictationToolbar,
|
|
49
|
+
onFiles,
|
|
50
|
+
onRestore,
|
|
51
|
+
insert
|
|
52
|
+
}: ComposerControlsProps) {
|
|
53
|
+
const { t } = useTranslation('agent-chat');
|
|
54
|
+
const scope = useChatScope();
|
|
55
|
+
const Slot = useChatHost().ComposerSlot;
|
|
56
|
+
const providers = useProviders((row) => row.providers);
|
|
57
|
+
const accountChoice = useAccountChoice(info.provider, info.account);
|
|
58
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
59
|
+
const busy = info.activeTurnId !== null;
|
|
60
|
+
const model = providers.find((entry) => entry.kind === info.provider)?.models.find((entry) => entry.slug === info.selection.model);
|
|
61
|
+
// The CLI announces its session on the first message, so anything before that is still a blank chat.
|
|
62
|
+
const started = info.agentSessionId !== null || info.usage.turns > 0;
|
|
63
|
+
// A chat that has started keeps its provider (the CLI holds the thread), and so does one opened for a
|
|
64
|
+
// named CLI; its models stay switchable, since the CLI takes a new one on the restart the next send does.
|
|
65
|
+
const pickable =
|
|
66
|
+
started || providerFixed
|
|
67
|
+
? providers.filter((entry) => entry.kind === info.provider)
|
|
68
|
+
: providers.filter((entry) => entry.installed && entry.capabilities.chat);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
// The composer's controls are round, from attach to send, so they keep their own shape instead of `.icon-btn`'s.
|
|
72
|
+
<div className="flex items-center gap-2 pt-3 pr-3.5 pb-3.5 pl-5 @max-md/composer:gap-1.5 @max-md/composer:p-3 @max-md/composer:pt-2.5">
|
|
73
|
+
{attachable && (
|
|
74
|
+
<Tooltip label={t('composer.attach')} name>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
className="grid h-8 w-8 shrink-0 place-items-center rounded-full border border-border text-text-muted hover:bg-surface-hover hover:text-text disabled:opacity-50"
|
|
78
|
+
disabled={!writable}
|
|
79
|
+
onClick={() => fileInputRef.current?.click()}
|
|
80
|
+
>
|
|
81
|
+
<Icon icon={Plus} size={16} />
|
|
82
|
+
</button>
|
|
83
|
+
</Tooltip>
|
|
84
|
+
)}
|
|
85
|
+
<input
|
|
86
|
+
ref={fileInputRef}
|
|
87
|
+
type="file"
|
|
88
|
+
multiple
|
|
89
|
+
hidden
|
|
90
|
+
onChange={(e) => {
|
|
91
|
+
onFiles(Array.from(e.target.files ?? []));
|
|
92
|
+
e.target.value = '';
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
<RunSettings
|
|
96
|
+
disabled={controls.configurationPending || !writable}
|
|
97
|
+
providers={pickable}
|
|
98
|
+
provider={info.provider}
|
|
99
|
+
selection={info.selection}
|
|
100
|
+
model={model}
|
|
101
|
+
runtimeMode={info.runtimeMode}
|
|
102
|
+
effectiveRuntimeMode={info.effectiveRuntimeMode}
|
|
103
|
+
permissionMode={info.permissionMode}
|
|
104
|
+
open={controls.modelPickerOpen}
|
|
105
|
+
onOpenChange={controls.setModelPickerOpen}
|
|
106
|
+
onModel={controls.chooseModel}
|
|
107
|
+
onOption={(id, value) => controls.configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } })}
|
|
108
|
+
onMode={(runtimeMode) => controls.configure({ runtimeMode })}
|
|
109
|
+
usage={info.usage}
|
|
110
|
+
compactDisabled={busy || !writable}
|
|
111
|
+
onCompact={controls.compact}
|
|
112
|
+
account={accountChoice === null ? null : { choice: accountChoice, started, onPick: controls.chooseAccount }}
|
|
113
|
+
/>
|
|
114
|
+
<StashPicker onRestore={onRestore} />
|
|
115
|
+
{Slot !== null && (
|
|
116
|
+
<ErrorBoundary label={t('composer.slotFailed')} resetKeys={[chatId]} compact className="relative rounded-lg">
|
|
117
|
+
<Slot scopeId={scope.id} chatId={chatId} disabled={!writable} insert={insert} />
|
|
118
|
+
</ErrorBoundary>
|
|
119
|
+
)}
|
|
120
|
+
<span className="grow" />
|
|
121
|
+
<div className="flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover">
|
|
122
|
+
<div ref={dictationToolbar} className="flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" />
|
|
123
|
+
{busy && (
|
|
124
|
+
<Tooltip label={composerStopLabel()} name>
|
|
125
|
+
<button
|
|
126
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text"
|
|
127
|
+
onClick={(event) => controls.stop(event.shiftKey)}
|
|
128
|
+
>
|
|
129
|
+
<Icon icon={Square} size={16} />
|
|
130
|
+
</button>
|
|
131
|
+
</Tooltip>
|
|
132
|
+
)}
|
|
133
|
+
{send.visible && (
|
|
134
|
+
<Tooltip label={send.label} kbd={send.kbd} name>
|
|
135
|
+
<button
|
|
136
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text hover:brightness-90 disabled:opacity-50 disabled:hover:brightness-100"
|
|
137
|
+
disabled={send.disabled}
|
|
138
|
+
onClick={(event) => send.onSend(event.altKey)}
|
|
139
|
+
>
|
|
140
|
+
<Icon icon={ArrowUp} size={16} />
|
|
141
|
+
</button>
|
|
142
|
+
</Tooltip>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { MessageSquare, SquareSlash, Zap } from 'lucide-react';
|
|
5
|
+
import { FileIcon, Icon, Menu, MiddleTruncate } from '@adecore/ui';
|
|
6
|
+
import type { ComposerMenus as Menus } from './use-composer-menus';
|
|
7
|
+
import { optionIdOf } from './menu-aria';
|
|
8
|
+
|
|
9
|
+
function splitPath(path: string): { name: string; dir: string } {
|
|
10
|
+
const slash = path.lastIndexOf('/');
|
|
11
|
+
return slash < 0 ? { name: path, dir: '' } : { name: path.slice(slash + 1), dir: path.slice(0, slash) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface OptionProps {
|
|
15
|
+
menus: Menus;
|
|
16
|
+
index: number;
|
|
17
|
+
className: string;
|
|
18
|
+
onPick(): void;
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* A row of a picker. The focus stays in the editor, which names the active row through `aria-activedescendant`. */
|
|
23
|
+
function MenuOption({ menus, index, className, onPick, children }: OptionProps) {
|
|
24
|
+
return (
|
|
25
|
+
<button
|
|
26
|
+
id={optionIdOf(menus.listId, index)}
|
|
27
|
+
role="option"
|
|
28
|
+
aria-selected={index === menus.activeIndex}
|
|
29
|
+
tabIndex={-1}
|
|
30
|
+
data-active={index === menus.activeIndex}
|
|
31
|
+
className={clsx('cursor-row flex w-full gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted', className)}
|
|
32
|
+
onMouseEnter={() => menus.setMenuIndex(index)}
|
|
33
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
34
|
+
onClick={onPick}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</button>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CommandMenu({ menus }: { menus: Menus }) {
|
|
42
|
+
const { t } = useTranslation('agent-chat');
|
|
43
|
+
return (
|
|
44
|
+
<div id={menus.listId} role="listbox" aria-label={t('composer.menu.commands')} className="border-b border-border px-1.5 py-1.5">
|
|
45
|
+
{menus.commands.map((command, index) => (
|
|
46
|
+
<MenuOption key={command.name} menus={menus} index={index} className="items-center" onPick={() => menus.pickCommand(command, index)}>
|
|
47
|
+
<Icon icon={command.skill ? Zap : SquareSlash} size={12} className={clsx('shrink-0', command.skill ? 'text-skill' : 'text-text-faint')} />
|
|
48
|
+
<span className="font-mono text-text">/{command.name}</span>
|
|
49
|
+
<span className="text-text-faint">{command.hint}</span>
|
|
50
|
+
</MenuOption>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function SkillMenu({ menus }: { menus: Menus }) {
|
|
57
|
+
const { t } = useTranslation('agent-chat');
|
|
58
|
+
return (
|
|
59
|
+
<div id={menus.listId} role="listbox" aria-label={t('composer.menu.skills')} className="border-b border-border px-1.5 py-1.5">
|
|
60
|
+
{menus.skillMatches.map((skill, index) => (
|
|
61
|
+
<MenuOption key={skill.name} menus={menus} index={index} className="items-start" onPick={() => menus.chooseSkill(skill.name)}>
|
|
62
|
+
<Icon icon={Zap} size={12} className="mt-0.5 shrink-0 text-skill" />
|
|
63
|
+
<span className="flex min-w-0 flex-col">
|
|
64
|
+
<span className="truncate font-mono text-text">${skill.name}</span>
|
|
65
|
+
{skill.description !== '' && <span className="line-clamp-1 text-text-faint">{skill.description}</span>}
|
|
66
|
+
</span>
|
|
67
|
+
</MenuOption>
|
|
68
|
+
))}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function MentionMenu({ menus, query }: { menus: Menus; query: string }) {
|
|
74
|
+
const { t } = useTranslation('agent-chat');
|
|
75
|
+
const { listId, chatMatches, files, mentionCount } = menus;
|
|
76
|
+
const mentionsLabelId = `${listId}-chats`;
|
|
77
|
+
const filesLabelId = `${listId}-files`;
|
|
78
|
+
const filesLabelShown = query === '' || chatMatches.length > 0;
|
|
79
|
+
return (
|
|
80
|
+
<div className="border-b border-border px-1.5 py-1.5">
|
|
81
|
+
{mentionCount > 0 && (
|
|
82
|
+
<div id={listId} role="listbox" aria-label={t('composer.menu.mentions')}>
|
|
83
|
+
{chatMatches.length > 0 && (
|
|
84
|
+
<div role="group" aria-labelledby={mentionsLabelId}>
|
|
85
|
+
<Menu.Label id={mentionsLabelId}>{t('composer.mentions.chats')}</Menu.Label>
|
|
86
|
+
{chatMatches.map((chat, index) => (
|
|
87
|
+
<MenuOption key={chat.id} menus={menus} index={index} className="items-center" onPick={() => menus.chooseChat(chat.id)}>
|
|
88
|
+
<Icon icon={MessageSquare} size={14} className="shrink-0 text-text-faint" />
|
|
89
|
+
<span className="truncate text-text">{chat.title || t('chatReference.untitled')}</span>
|
|
90
|
+
</MenuOption>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
{files.length > 0 && (
|
|
95
|
+
<div role="group" {...(filesLabelShown ? { 'aria-labelledby': filesLabelId } : { 'aria-label': t('composer.mentions.files') })}>
|
|
96
|
+
{filesLabelShown && (
|
|
97
|
+
<Menu.Label id={filesLabelId}>{query === '' ? t('composer.mentions.here') : t('composer.mentions.files')}</Menu.Label>
|
|
98
|
+
)}
|
|
99
|
+
{files.map((path, index) => {
|
|
100
|
+
const { name, dir } = splitPath(path);
|
|
101
|
+
return (
|
|
102
|
+
<MenuOption
|
|
103
|
+
key={path}
|
|
104
|
+
menus={menus}
|
|
105
|
+
index={chatMatches.length + index}
|
|
106
|
+
className="items-center"
|
|
107
|
+
onPick={() => menus.chooseMention(path)}
|
|
108
|
+
>
|
|
109
|
+
<FileIcon path={path} size={14} />
|
|
110
|
+
<span className="truncate font-mono text-text">{name}</span>
|
|
111
|
+
{dir && <MiddleTruncate text={dir} tooltip={false} className="text-text-faint" />}
|
|
112
|
+
</MenuOption>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
)}
|
|
119
|
+
{mentionCount === 0 && (
|
|
120
|
+
<div className="px-2 py-1 text-xs text-text-faint">{query ? t('composer.mentions.noMatch') : t('composer.mentions.empty')}</div>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Whichever picker the caret opened, over the box. */
|
|
127
|
+
export function ComposerMenu({ menus }: { menus: Menus }) {
|
|
128
|
+
if (menus.commandMenuOpen) {
|
|
129
|
+
return <CommandMenu menus={menus} />;
|
|
130
|
+
}
|
|
131
|
+
if (menus.skillMenuOpen) {
|
|
132
|
+
return <SkillMenu menus={menus} />;
|
|
133
|
+
}
|
|
134
|
+
if (menus.mentionMenuOpen && menus.mention !== null) {
|
|
135
|
+
return <MentionMenu menus={menus} query={menus.mention.query} />;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { Paperclip, X } from 'lucide-react';
|
|
3
|
+
import type { ChatAttachmentUpload } from '@adecore/agent-contracts';
|
|
4
|
+
import { Icon, IconButton, Tooltip } from '@adecore/ui';
|
|
5
|
+
import { useChatScope } from '../../../scope';
|
|
6
|
+
import { formatBytes, isImageAttachment, uploadBytes } from '../../attachments';
|
|
7
|
+
import { jumpToTimelineItem } from '../../timeline-scroll';
|
|
8
|
+
import { ChatReferenceChip } from '../ChatReferenceChip';
|
|
9
|
+
import { UploadThumb } from '../UploadThumb';
|
|
10
|
+
|
|
11
|
+
const ROW = 'px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3';
|
|
12
|
+
|
|
13
|
+
interface DraftQuoteProps {
|
|
14
|
+
chatId: string;
|
|
15
|
+
quote: string;
|
|
16
|
+
/* The reply the quote came from, which a click scrolls to; null for a quote that names none. */
|
|
17
|
+
source: string | null;
|
|
18
|
+
onRemove(): void;
|
|
19
|
+
onSourceGone(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* The quote that goes above the message, as a blockquote. */
|
|
23
|
+
export function DraftQuote({ chatId, quote, source, onRemove, onSourceGone }: DraftQuoteProps) {
|
|
24
|
+
const { t } = useTranslation('agent-chat');
|
|
25
|
+
const scope = useChatScope();
|
|
26
|
+
return (
|
|
27
|
+
<div className={`flex items-center gap-2 ${ROW}`}>
|
|
28
|
+
{source === null ? (
|
|
29
|
+
<span className="min-w-0 grow truncate border-l-2 border-border-strong pl-2.5 text-xs text-text-muted">{quote}</span>
|
|
30
|
+
) : (
|
|
31
|
+
<Tooltip label={t('composer.quote.goToSource')}>
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
className="min-w-0 grow truncate border-l-2 border-border-strong pl-2.5 text-left text-xs text-text-muted hover:text-text"
|
|
35
|
+
onClick={() => jumpToTimelineItem(scope.keyOf(chatId), source, { flash: true, onMissing: onSourceGone })}
|
|
36
|
+
>
|
|
37
|
+
{quote}
|
|
38
|
+
</button>
|
|
39
|
+
</Tooltip>
|
|
40
|
+
)}
|
|
41
|
+
<IconButton icon={X} size="2xs" label={t('composer.quote.remove')} onClick={onRemove} />
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Other chats the message points at, beside the text rather than in it. */
|
|
47
|
+
export function DraftChats({ chats, onRemove }: { chats: readonly string[]; onRemove(id: string): void }) {
|
|
48
|
+
return (
|
|
49
|
+
<div className={`flex flex-wrap gap-1.5 ${ROW}`}>
|
|
50
|
+
{chats.map((id) => (
|
|
51
|
+
<ChatReferenceChip key={id} chatId={id} onRemove={() => onRemove(id)} />
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* The files the message carries: an image as a thumbnail, anything else by its name and size. */
|
|
58
|
+
export function DraftAttachments({ attachments, onRemove }: { attachments: readonly ChatAttachmentUpload[]; onRemove(index: number): void }) {
|
|
59
|
+
const { t } = useTranslation('agent-chat');
|
|
60
|
+
return (
|
|
61
|
+
<div className={`flex flex-wrap gap-2 ${ROW}`}>
|
|
62
|
+
{attachments.map((attachment, index) => (
|
|
63
|
+
<div key={`${attachment.name}-${index}`} className="group/thumb relative">
|
|
64
|
+
{isImageAttachment(attachment.mime) ? (
|
|
65
|
+
<UploadThumb upload={attachment} />
|
|
66
|
+
) : (
|
|
67
|
+
<span className="flex h-14 w-36 flex-col justify-center gap-0.5 rounded-lg border border-border bg-surface-sunken px-2.5">
|
|
68
|
+
<span className="flex items-center gap-1.5 text-xs text-text">
|
|
69
|
+
<Icon icon={Paperclip} size={12} className="shrink-0 text-text-faint" />
|
|
70
|
+
<span className="truncate">{attachment.name}</span>
|
|
71
|
+
</span>
|
|
72
|
+
<span className="pl-5 text-xs text-text-faint">{formatBytes(uploadBytes(attachment))}</span>
|
|
73
|
+
</span>
|
|
74
|
+
)}
|
|
75
|
+
<IconButton
|
|
76
|
+
icon={X}
|
|
77
|
+
size="2xs"
|
|
78
|
+
label={t('composer.removeAttachment', { name: attachment.name })}
|
|
79
|
+
className="absolute -top-1.5 -right-1.5 border border-border bg-surface-raised opacity-0 transition-opacity group-hover/thumb:opacity-100 focus-visible:opacity-100"
|
|
80
|
+
onClick={() => onRemove(index)}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { Clock, Copy, FastForward, Pencil, X } from 'lucide-react';
|
|
3
|
+
import type { ChatQueuedMessage } from '@adecore/agent-contracts';
|
|
4
|
+
import { ButtonGroup, ContextMenu, copyText, Icon, IconButton } from '@adecore/ui';
|
|
5
|
+
import { useChatActions } from '../../actions';
|
|
6
|
+
import { scrollTimelineToEnd } from '../../timeline-scroll';
|
|
7
|
+
|
|
8
|
+
interface QueuedMessagesProps {
|
|
9
|
+
chatId: string;
|
|
10
|
+
queue: readonly ChatQueuedMessage[];
|
|
11
|
+
paused: boolean;
|
|
12
|
+
/* A message is on its way back into the draft, so no other can follow until it is there. */
|
|
13
|
+
takingBack: boolean;
|
|
14
|
+
onEdit(message: ChatQueuedMessage): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* The messages waiting for the running turn to end, each with a way to send it now, edit it or drop it. */
|
|
18
|
+
export function QueuedMessages({ chatId, queue, paused, takingBack, onEdit }: QueuedMessagesProps) {
|
|
19
|
+
const { t } = useTranslation('agent-chat');
|
|
20
|
+
const actions = useChatActions();
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex flex-col gap-1 border-b border-border px-2 py-1.5">
|
|
23
|
+
{paused && <p className="px-1.5 text-xs text-text-muted">{t('composer.queue.paused')}</p>}
|
|
24
|
+
{queue.map((message) => {
|
|
25
|
+
const sendNow = (): void => {
|
|
26
|
+
scrollTimelineToEnd(chatId, 'instant');
|
|
27
|
+
void actions.sendNow(chatId, message.id).catch(() => undefined);
|
|
28
|
+
};
|
|
29
|
+
const unqueue = (): void => void actions.unqueue(chatId, message.id).catch(() => undefined);
|
|
30
|
+
const edit = (): void => onEdit(message);
|
|
31
|
+
return (
|
|
32
|
+
<ContextMenu.Root key={message.id}>
|
|
33
|
+
<ContextMenu.Trigger className="group/queued flex items-center gap-2 rounded-md px-1.5 py-1 text-xs text-text-muted">
|
|
34
|
+
<Icon icon={Clock} size={12} className="shrink-0 text-text-faint" />
|
|
35
|
+
<span className="min-w-0 grow truncate">
|
|
36
|
+
{message.text || t('composer.queue.attachments', { count: message.attachments?.length ?? 0 })}
|
|
37
|
+
</span>
|
|
38
|
+
<ButtonGroup render={<span />} className="opacity-0 transition-opacity group-hover/queued:opacity-100 focus-within:opacity-100">
|
|
39
|
+
<IconButton icon={FastForward} size="2xs" label={t('composer.queue.sendNow')} onClick={sendNow} />
|
|
40
|
+
<IconButton icon={Pencil} size="2xs" label={t('common.action.edit')} disabled={takingBack} onClick={edit} />
|
|
41
|
+
<IconButton icon={X} size="2xs" label={t('common.action.remove')} onClick={unqueue} />
|
|
42
|
+
</ButtonGroup>
|
|
43
|
+
</ContextMenu.Trigger>
|
|
44
|
+
<ContextMenu.Popup>
|
|
45
|
+
<ContextMenu.Item onClick={sendNow}>
|
|
46
|
+
<Icon icon={FastForward} size={14} /> {t('composer.queue.sendNow')}
|
|
47
|
+
</ContextMenu.Item>
|
|
48
|
+
<ContextMenu.Item disabled={takingBack} onClick={edit}>
|
|
49
|
+
<Icon icon={Pencil} size={14} /> {t('common.action.edit')}
|
|
50
|
+
</ContextMenu.Item>
|
|
51
|
+
<ContextMenu.Item disabled={message.text === ''} onClick={() => copyText(message.text)}>
|
|
52
|
+
<Icon icon={Copy} size={14} /> {t('common.action.copy')}
|
|
53
|
+
</ContextMenu.Item>
|
|
54
|
+
<ContextMenu.Separator />
|
|
55
|
+
<ContextMenu.Item className="text-status-error" onClick={unqueue}>
|
|
56
|
+
<Icon icon={X} size={14} /> {t('common.action.remove')}
|
|
57
|
+
</ContextMenu.Item>
|
|
58
|
+
</ContextMenu.Popup>
|
|
59
|
+
</ContextMenu.Root>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { indentLess, indentMore, insertNewline } from '@codemirror/commands';
|
|
2
|
+
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
|
|
3
|
+
import type { EditorView } from '@codemirror/view';
|
|
4
|
+
import { isModHeld, matchesShortcut } from '@adecore/ui';
|
|
5
|
+
import type { ChatHost } from '../../../host';
|
|
6
|
+
import type { ComposerSend } from '../../composer-send';
|
|
7
|
+
import type { ChatSendKey } from '../../send-key';
|
|
8
|
+
import { STASH_SHORTCUT } from '../../stash';
|
|
9
|
+
import { pageTimeline } from '../../timeline-scroll';
|
|
10
|
+
import type { ComposerMenus } from './use-composer-menus';
|
|
11
|
+
import type { PromptHistory } from './use-prompt-history';
|
|
12
|
+
import { enterAction, inCode, inFenceBody, inOpenFence, listItemAt, recallDirection, tabSpaces } from './keys';
|
|
13
|
+
|
|
14
|
+
const SEND_HOW: Readonly<Record<'send' | 'send-alternate' | 'send-and-new', ComposerSend>> = {
|
|
15
|
+
send: 'default',
|
|
16
|
+
'send-alternate': 'alternate',
|
|
17
|
+
'send-and-new': 'and-new'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface KeyHandlerInput {
|
|
21
|
+
chatId: string;
|
|
22
|
+
host: Pick<ChatHost, 'isApplePlatform' | 'isAppShortcut' | 'openNewChat'>;
|
|
23
|
+
sendKey: ChatSendKey;
|
|
24
|
+
text: string;
|
|
25
|
+
menus: ComposerMenus;
|
|
26
|
+
history: PromptHistory;
|
|
27
|
+
noteKey(e: KeyboardEvent): void;
|
|
28
|
+
toggleStash(): void;
|
|
29
|
+
setText(next: string): void;
|
|
30
|
+
submit(how: ComposerSend): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* In a fence Tab indents like a code editor; anywhere else it keeps moving the focus. */
|
|
34
|
+
function indentInFence(e: KeyboardEvent, view: EditorView): boolean {
|
|
35
|
+
const { state } = view;
|
|
36
|
+
const { from, to, head } = state.selection.main;
|
|
37
|
+
const tree = ensureSyntaxTree(state, state.doc.length, 50) ?? syntaxTree(state);
|
|
38
|
+
if (!inFenceBody(tree, state.doc.toString(), head)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
// Handled even when there is nothing to outdent, so Shift+Tab never throws the focus out of a block.
|
|
42
|
+
if (e.shiftKey) {
|
|
43
|
+
indentLess(view);
|
|
44
|
+
} else if (from !== to) {
|
|
45
|
+
indentMore(view);
|
|
46
|
+
} else {
|
|
47
|
+
view.dispatch(state.replaceSelection(tabSpaces(head - state.doc.lineAt(head).from)), { scrollIntoView: true, userEvent: 'input' });
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* The keys an open picker takes: the arrows walk it, and Tab (or Enter, outside the commands) picks. */
|
|
53
|
+
function pickInMenu(e: KeyboardEvent, { menus, setText }: KeyHandlerInput): boolean {
|
|
54
|
+
const { activeIndex } = menus;
|
|
55
|
+
if (menus.commandMenuOpen) {
|
|
56
|
+
if (menus.moveInMenu(e.key, menus.commands.length)) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (e.key === 'Tab') {
|
|
60
|
+
const chosen = menus.commands[activeIndex]!;
|
|
61
|
+
if (chosen.skill) {
|
|
62
|
+
menus.chooseSkill(chosen.name);
|
|
63
|
+
} else {
|
|
64
|
+
setText(`/${chosen.name} `);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (menus.skillMenuOpen) {
|
|
70
|
+
if (menus.moveInMenu(e.key, menus.skillMatches.length)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
74
|
+
menus.chooseSkill((menus.skillMatches[activeIndex] ?? menus.skillMatches[0]!).name);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (menus.mentionMenuOpen && menus.mentionCount > 0) {
|
|
79
|
+
if (menus.moveInMenu(e.key, menus.mentionCount)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
83
|
+
menus.chooseMentionAt(activeIndex);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Enter sends, adds a line, or carries a list on, by the send key and where the caret is. */
|
|
91
|
+
function enter(e: KeyboardEvent, view: EditorView, { host, sendKey, submit }: KeyHandlerInput): boolean {
|
|
92
|
+
const { state } = view;
|
|
93
|
+
const { from, to, head } = state.selection.main;
|
|
94
|
+
// Parsed to the end, since the fence that closes the block can sit after the caret.
|
|
95
|
+
const tree = ensureSyntaxTree(state, state.doc.length, 50) ?? syntaxTree(state);
|
|
96
|
+
const line = state.doc.lineAt(head);
|
|
97
|
+
const list = from === to && !inCode(tree, state.doc.toString(), head) ? listItemAt(line.text) : null;
|
|
98
|
+
const action = enterAction(
|
|
99
|
+
{ shift: e.shiftKey, mod: isModHeld(e, host.isApplePlatform()), alt: e.altKey },
|
|
100
|
+
{
|
|
101
|
+
inOpenFence: inOpenFence(tree, head),
|
|
102
|
+
list,
|
|
103
|
+
column: head - line.from,
|
|
104
|
+
sendKey,
|
|
105
|
+
multiline: state.doc.lines > 1,
|
|
106
|
+
startsNewChat: host.openNewChat !== null
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
if (action === 'newline') {
|
|
110
|
+
return insertNewline(view);
|
|
111
|
+
}
|
|
112
|
+
if (action === 'continue-list') {
|
|
113
|
+
if (list) {
|
|
114
|
+
view.dispatch({
|
|
115
|
+
changes: { from: head, insert: `\n${list.next}` },
|
|
116
|
+
selection: { anchor: head + 1 + list.next.length },
|
|
117
|
+
scrollIntoView: true,
|
|
118
|
+
userEvent: 'input'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (action === 'leave-list') {
|
|
124
|
+
view.dispatch({ changes: { from: line.from, to: line.to }, selection: { anchor: line.from }, userEvent: 'delete' });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
submit(SEND_HOW[action]);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Runs before the editor's own keymap; true means the key is handled and its default prevented. */
|
|
132
|
+
export function composerKeyHandler(input: KeyHandlerInput): (e: KeyboardEvent, view: EditorView) => boolean {
|
|
133
|
+
const { chatId, host, text, history } = input;
|
|
134
|
+
return (e, view) => {
|
|
135
|
+
input.noteKey(e);
|
|
136
|
+
// Escape leaves the node, unless it first has a recalled prompt to put back.
|
|
137
|
+
if (e.key === 'Escape' && !e.isComposing) {
|
|
138
|
+
if (!history.leave()) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
e.stopPropagation();
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
// The app's own shortcuts stay the app's while you type: Mod+Shift+Enter would otherwise send as well.
|
|
145
|
+
if (host.isAppShortcut(e)) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
e.stopPropagation();
|
|
149
|
+
// A key inside an IME composition belongs to the composition, Enter and the arrows included.
|
|
150
|
+
if (e.isComposing) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
// Reading back through a long answer should not mean leaving the box you are typing in.
|
|
154
|
+
if (e.key === 'PageUp' || e.key === 'PageDown') {
|
|
155
|
+
return pageTimeline(chatId, e.key === 'PageUp' ? -1 : 1);
|
|
156
|
+
}
|
|
157
|
+
if (matchesShortcut(STASH_SHORTCUT, e, host.isApplePlatform())) {
|
|
158
|
+
input.toggleStash();
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
if (pickInMenu(e, input)) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (e.key === 'Tab' && !e.altKey && !e.metaKey && !e.ctrlKey && indentInFence(e, view)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (e.key === 'Enter') {
|
|
168
|
+
return enter(e, view, input);
|
|
169
|
+
}
|
|
170
|
+
const { from, to } = view.state.selection.main;
|
|
171
|
+
const direction = recallDirection({ key: e.key, text, from, to, recalled: history.recalled(text) });
|
|
172
|
+
return direction !== null && history.recall(direction);
|
|
173
|
+
};
|
|
174
|
+
}
|