@ruimte/agents-react 0.0.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/README.md +142 -1
- package/dist/agents/AccountDot.d.ts +4 -0
- package/dist/agents/AccountDot.js +8 -0
- package/dist/agents/AgentIcon.d.ts +6 -0
- package/dist/agents/AgentIcon.js +20 -0
- package/dist/agents/ProviderLogo.d.ts +8 -0
- package/dist/agents/ProviderLogo.js +8 -0
- package/dist/agents/account-limits.d.ts +7 -0
- package/dist/agents/account-limits.js +26 -0
- package/dist/agents/accounts.d.ts +22 -0
- package/dist/agents/accounts.js +103 -0
- package/dist/agents/model-name.d.ts +6 -0
- package/dist/agents/model-name.js +44 -0
- package/dist/agents/provider-paths.d.ts +2 -0
- package/dist/agents/provider-paths.js +8 -0
- package/dist/agents/status-look.d.ts +13 -0
- package/dist/agents/status-look.js +22 -0
- package/dist/chat/account-choice.d.ts +11 -0
- package/dist/chat/account-choice.js +30 -0
- package/dist/chat/actions.d.ts +6 -0
- package/dist/chat/actions.js +64 -0
- package/dist/chat/activity.d.ts +5 -0
- package/dist/chat/activity.js +4 -0
- package/dist/chat/attachments.d.ts +23 -0
- package/dist/chat/attachments.js +70 -0
- package/dist/chat/bookmarks.d.ts +14 -0
- package/dist/chat/bookmarks.js +73 -0
- package/dist/chat/chat-client.d.ts +53 -0
- package/dist/chat/chat-client.js +232 -0
- package/dist/chat/chat-references.d.ts +5 -0
- package/dist/chat/chat-references.js +5 -0
- package/dist/chat/drafts.d.ts +29 -0
- package/dist/chat/drafts.js +104 -0
- package/dist/chat/forks.d.ts +2 -0
- package/dist/chat/forks.js +26 -0
- package/dist/chat/guards.d.ts +12 -0
- package/dist/chat/guards.js +63 -0
- package/dist/chat/logic/answers.d.ts +3 -0
- package/dist/chat/logic/answers.js +14 -0
- package/dist/chat/logic/bookmarks.d.ts +5 -0
- package/dist/chat/logic/bookmarks.js +31 -0
- package/dist/chat/logic/context-usage.d.ts +12 -0
- package/dist/chat/logic/context-usage.js +20 -0
- package/dist/chat/logic/fork.d.ts +44 -0
- package/dist/chat/logic/fork.js +138 -0
- package/dist/chat/logic/handback.d.ts +2 -0
- package/dist/chat/logic/handback.js +8 -0
- package/dist/chat/logic/json.d.ts +1 -0
- package/dist/chat/logic/json.js +2 -0
- package/dist/chat/logic/limit.d.ts +7 -0
- package/dist/chat/logic/limit.js +34 -0
- package/dist/chat/logic/resume-compaction.d.ts +14 -0
- package/dist/chat/logic/resume-compaction.js +47 -0
- package/dist/chat/logic/scrubber.d.ts +50 -0
- package/dist/chat/logic/scrubber.js +216 -0
- package/dist/chat/logic/timeline-copy.d.ts +4 -0
- package/dist/chat/logic/timeline-copy.js +51 -0
- package/dist/chat/logic/timeline-target.d.ts +12 -0
- package/dist/chat/logic/timeline-target.js +34 -0
- package/dist/chat/logic/timeline.d.ts +110 -0
- package/dist/chat/logic/timeline.js +339 -0
- package/dist/chat/logic/tool-catalog.d.ts +10 -0
- package/dist/chat/logic/tool-catalog.js +40 -0
- package/dist/chat/logic/tools.d.ts +15 -0
- package/dist/chat/logic/tools.js +79 -0
- package/dist/chat/logic/workflow.d.ts +14 -0
- package/dist/chat/logic/workflow.js +47 -0
- package/dist/chat/mentions.d.ts +46 -0
- package/dist/chat/mentions.js +128 -0
- package/dist/chat/persisted-json.d.ts +5 -0
- package/dist/chat/persisted-json.js +24 -0
- package/dist/chat/preferences.d.ts +23 -0
- package/dist/chat/preferences.js +110 -0
- package/dist/chat/quote.d.ts +2 -0
- package/dist/chat/quote.js +25 -0
- package/dist/chat/recent-messages.d.ts +17 -0
- package/dist/chat/recent-messages.js +28 -0
- package/dist/chat/resume-compaction-dismissals.d.ts +3 -0
- package/dist/chat/resume-compaction-dismissals.js +14 -0
- package/dist/chat/runtime-modes.d.ts +4 -0
- package/dist/chat/runtime-modes.js +11 -0
- package/dist/chat/stash.d.ts +24 -0
- package/dist/chat/stash.js +88 -0
- package/dist/chat/subagent-conversation.d.ts +38 -0
- package/dist/chat/subagent-conversation.js +150 -0
- package/dist/chat/subagent-list.d.ts +16 -0
- package/dist/chat/subagent-list.js +76 -0
- package/dist/chat/subagent-support.d.ts +6 -0
- package/dist/chat/subagent-support.js +14 -0
- package/dist/chat/subagent-view.d.ts +33 -0
- package/dist/chat/subagent-view.js +65 -0
- package/dist/chat/thumbnails.d.ts +10 -0
- package/dist/chat/thumbnails.js +87 -0
- package/dist/chat/timeline-scroll.d.ts +16 -0
- package/dist/chat/timeline-scroll.js +100 -0
- package/dist/chat/ui/AccountPill.d.ts +3 -0
- package/dist/chat/ui/AccountPill.js +24 -0
- package/dist/chat/ui/BookmarkMarker.d.ts +8 -0
- package/dist/chat/ui/BookmarkMarker.js +59 -0
- package/dist/chat/ui/BookmarkSubmenu.d.ts +3 -0
- package/dist/chat/ui/BookmarkSubmenu.js +44 -0
- package/dist/chat/ui/ChatActivity.d.ts +3 -0
- package/dist/chat/ui/ChatActivity.js +87 -0
- package/dist/chat/ui/ChatReferenceChip.d.ts +4 -0
- package/dist/chat/ui/ChatReferenceChip.js +17 -0
- package/dist/chat/ui/CodeBlock.d.ts +4 -0
- package/dist/chat/ui/CodeBlock.js +130 -0
- package/dist/chat/ui/Composer.d.ts +14 -0
- package/dist/chat/ui/Composer.js +821 -0
- package/dist/chat/ui/ComposerInput.d.ts +28 -0
- package/dist/chat/ui/ComposerInput.js +118 -0
- package/dist/chat/ui/DiffPool.d.ts +5 -0
- package/dist/chat/ui/DiffPool.js +26 -0
- package/dist/chat/ui/EditDiff.d.ts +4 -0
- package/dist/chat/ui/EditDiff.js +21 -0
- package/dist/chat/ui/FadingWords.d.ts +3 -0
- package/dist/chat/ui/FadingWords.js +5 -0
- package/dist/chat/ui/ImageView.d.ts +6 -0
- package/dist/chat/ui/ImageView.js +85 -0
- package/dist/chat/ui/LimitState.d.ts +8 -0
- package/dist/chat/ui/LimitState.js +117 -0
- package/dist/chat/ui/Markdown.d.ts +18 -0
- package/dist/chat/ui/Markdown.js +158 -0
- package/dist/chat/ui/MessageActions.d.ts +9 -0
- package/dist/chat/ui/MessageActions.js +56 -0
- package/dist/chat/ui/Pickers.d.ts +15 -0
- package/dist/chat/ui/Pickers.js +110 -0
- package/dist/chat/ui/PromptComposer.d.ts +13 -0
- package/dist/chat/ui/PromptComposer.js +48 -0
- package/dist/chat/ui/QuoteButton.d.ts +7 -0
- package/dist/chat/ui/QuoteButton.js +76 -0
- package/dist/chat/ui/ResumeCompactionDock.d.ts +5 -0
- package/dist/chat/ui/ResumeCompactionDock.js +15 -0
- package/dist/chat/ui/RunSettings.d.ts +25 -0
- package/dist/chat/ui/RunSettings.js +148 -0
- package/dist/chat/ui/Scrubber.d.ts +17 -0
- package/dist/chat/ui/Scrubber.js +121 -0
- package/dist/chat/ui/SubagentControls.d.ts +11 -0
- package/dist/chat/ui/SubagentControls.js +37 -0
- package/dist/chat/ui/SubagentStopButton.d.ts +7 -0
- package/dist/chat/ui/SubagentStopButton.js +36 -0
- package/dist/chat/ui/SubagentTimeline.d.ts +4 -0
- package/dist/chat/ui/SubagentTimeline.js +92 -0
- package/dist/chat/ui/Timeline.d.ts +5 -0
- package/dist/chat/ui/Timeline.js +318 -0
- package/dist/chat/ui/TimelineMenu.d.ts +12 -0
- package/dist/chat/ui/TimelineMenu.js +61 -0
- package/dist/chat/ui/UnifiedDiff.d.ts +8 -0
- package/dist/chat/ui/UnifiedDiff.js +51 -0
- package/dist/chat/ui/UploadThumb.d.ts +4 -0
- package/dist/chat/ui/UploadThumb.js +11 -0
- package/dist/chat/ui/chips.d.ts +4 -0
- package/dist/chat/ui/chips.js +11 -0
- package/dist/chat/ui/code-lines.d.ts +18 -0
- package/dist/chat/ui/code-lines.js +46 -0
- package/dist/chat/ui/code-streaming.d.ts +1 -0
- package/dist/chat/ui/code-streaming.js +3 -0
- package/dist/chat/ui/code-theme.d.ts +3 -0
- package/dist/chat/ui/code-theme.js +10 -0
- package/dist/chat/ui/composer/chips.d.ts +6 -0
- package/dist/chat/ui/composer/chips.js +29 -0
- package/dist/chat/ui/composer/editor.d.ts +11 -0
- package/dist/chat/ui/composer/editor.js +94 -0
- package/dist/chat/ui/composer/keys.d.ts +30 -0
- package/dist/chat/ui/composer/keys.js +98 -0
- package/dist/chat/ui/diff-theme.d.ts +2 -0
- package/dist/chat/ui/diff-theme.js +20 -0
- package/dist/chat/ui/file-links.d.ts +5 -0
- package/dist/chat/ui/file-links.js +18 -0
- package/dist/chat/ui/find-reveal.d.ts +7 -0
- package/dist/chat/ui/find-reveal.js +13 -0
- package/dist/chat/ui/icons.d.ts +4 -0
- package/dist/chat/ui/icons.js +20 -0
- package/dist/chat/ui/markdown-blocks.d.ts +6 -0
- package/dist/chat/ui/markdown-blocks.js +101 -0
- package/dist/chat/ui/quote-selection.d.ts +3 -0
- package/dist/chat/ui/quote-selection.js +44 -0
- package/dist/chat/ui/rehype-chips.d.ts +23 -0
- package/dist/chat/ui/rehype-chips.js +28 -0
- package/dist/chat/ui/rehype-fade.d.ts +23 -0
- package/dist/chat/ui/rehype-fade.js +30 -0
- package/dist/chat/ui/remark-html-as-text.d.ts +8 -0
- package/dist/chat/ui/remark-html-as-text.js +24 -0
- package/dist/chat/ui/reveal.d.ts +13 -0
- package/dist/chat/ui/reveal.js +112 -0
- package/dist/chat/ui/rows/ForksRow.d.ts +4 -0
- package/dist/chat/ui/rows/ForksRow.js +40 -0
- package/dist/chat/ui/rows/MessageRows.d.ts +36 -0
- package/dist/chat/ui/rows/MessageRows.js +190 -0
- package/dist/chat/ui/rows/Rows.d.ts +12 -0
- package/dist/chat/ui/rows/Rows.js +49 -0
- package/dist/chat/ui/rows/SubagentRow.d.ts +15 -0
- package/dist/chat/ui/rows/SubagentRow.js +98 -0
- package/dist/chat/ui/rows/WorkRows.d.ts +46 -0
- package/dist/chat/ui/rows/WorkRows.js +143 -0
- package/dist/chat/ui/rows/WorkflowRow.d.ts +7 -0
- package/dist/chat/ui/rows/WorkflowRow.js +37 -0
- package/dist/chat/ui/rows/row-rhythm.d.ts +3 -0
- package/dist/chat/ui/rows/row-rhythm.js +14 -0
- package/dist/chat/ui/useToggleSet.d.ts +6 -0
- package/dist/chat/ui/useToggleSet.js +25 -0
- package/dist/host.d.ts +167 -0
- package/dist/host.js +41 -0
- package/dist/lazy.d.ts +5 -0
- package/dist/lazy.js +35 -0
- package/dist/locales/en/agent-chat.json +417 -0
- package/dist/locales/en/agent-prompts.json +42 -0
- package/dist/locales/en/agent-providers.json +131 -0
- package/dist/locales/en/agent-usage.json +131 -0
- package/dist/locales/nl/agent-chat.json +417 -0
- package/dist/locales/nl/agent-prompts.json +42 -0
- package/dist/locales/nl/agent-providers.json +131 -0
- package/dist/locales/nl/agent-usage.json +131 -0
- package/dist/locales.d.ts +5 -0
- package/dist/locales.js +18 -0
- package/dist/mounted-registry.d.ts +7 -0
- package/dist/mounted-registry.js +39 -0
- package/dist/port-transport.d.ts +6 -0
- package/dist/port-transport.js +120 -0
- package/dist/prompts/logic/focus.d.ts +1 -0
- package/dist/prompts/logic/focus.js +17 -0
- package/dist/prompts/logic/keys.d.ts +42 -0
- package/dist/prompts/logic/keys.js +84 -0
- package/dist/prompts/logic/prompts.d.ts +34 -0
- package/dist/prompts/logic/prompts.fixtures.d.ts +5 -0
- package/dist/prompts/logic/prompts.fixtures.js +73 -0
- package/dist/prompts/logic/prompts.js +22 -0
- package/dist/prompts/logic/subjects.d.ts +49 -0
- package/dist/prompts/logic/subjects.js +56 -0
- package/dist/prompts/logic/useFocusAfterAnswer.d.ts +6 -0
- package/dist/prompts/logic/useFocusAfterAnswer.js +28 -0
- package/dist/prompts/logic/usePromptSession.d.ts +24 -0
- package/dist/prompts/logic/usePromptSession.js +87 -0
- package/dist/prompts/ui/ApprovalBody.d.ts +13 -0
- package/dist/prompts/ui/ApprovalBody.js +35 -0
- package/dist/prompts/ui/PromptActions.d.ts +26 -0
- package/dist/prompts/ui/PromptActions.js +19 -0
- package/dist/prompts/ui/PromptCard.d.ts +16 -0
- package/dist/prompts/ui/PromptCard.js +50 -0
- package/dist/prompts/ui/PromptView.d.ts +2 -0
- package/dist/prompts/ui/PromptView.js +47 -0
- package/dist/prompts/ui/QuestionBody.d.ts +9 -0
- package/dist/prompts/ui/QuestionBody.js +87 -0
- package/dist/providers/AccountColors.d.ts +5 -0
- package/dist/providers/AccountColors.js +8 -0
- package/dist/providers/AccountDetail.d.ts +12 -0
- package/dist/providers/AccountDetail.js +76 -0
- package/dist/providers/AccountVariables.d.ts +8 -0
- package/dist/providers/AccountVariables.js +48 -0
- package/dist/providers/AddAccountForm.d.ts +10 -0
- package/dist/providers/AddAccountForm.js +58 -0
- package/dist/providers/CliDetail.d.ts +11 -0
- package/dist/providers/CliDetail.js +62 -0
- package/dist/providers/ProvidersPane.d.ts +8 -0
- package/dist/providers/ProvidersPane.js +90 -0
- package/dist/providers/account-actions.d.ts +7 -0
- package/dist/providers/account-actions.js +53 -0
- package/dist/providers/parts.d.ts +9 -0
- package/dist/providers/parts.js +18 -0
- package/dist/providers/provider-abilities.d.ts +2 -0
- package/dist/providers/provider-abilities.js +7 -0
- package/dist/providers/variables.d.ts +13 -0
- package/dist/providers/variables.js +53 -0
- package/dist/scope.d.ts +11 -0
- package/dist/scope.js +10 -0
- package/dist/settings/sections.d.ts +13 -0
- package/dist/settings/sections.js +30 -0
- package/dist/state/chats.d.ts +32 -0
- package/dist/state/chats.js +124 -0
- package/dist/state/provider-accounts.d.ts +17 -0
- package/dist/state/provider-accounts.js +63 -0
- package/dist/state/providers.d.ts +17 -0
- package/dist/state/providers.js +29 -0
- package/dist/state/usage.d.ts +41 -0
- package/dist/state/usage.js +100 -0
- package/dist/theme.css +186 -0
- package/dist/transport.d.ts +24 -0
- package/dist/transport.js +16 -0
- package/dist/ui/PlainTextarea.d.ts +2 -0
- package/dist/ui/PlainTextarea.js +5 -0
- package/dist/usage/LimitsList.d.ts +18 -0
- package/dist/usage/LimitsList.js +69 -0
- package/dist/usage/UsageBreakdown.d.ts +9 -0
- package/dist/usage/UsageBreakdown.js +69 -0
- package/dist/usage/UsageChart.d.ts +10 -0
- package/dist/usage/UsageChart.js +49 -0
- package/dist/usage/UsageDialog.d.ts +6 -0
- package/dist/usage/UsageDialog.js +15 -0
- package/dist/usage/UsageLimits.d.ts +1 -0
- package/dist/usage/UsageLimits.js +62 -0
- package/dist/usage/UsageLimitsCard.d.ts +4 -0
- package/dist/usage/UsageLimitsCard.js +27 -0
- package/dist/usage/UsagePage.d.ts +7 -0
- package/dist/usage/UsagePage.js +161 -0
- package/dist/usage/UsagePane.d.ts +3 -0
- package/dist/usage/UsagePane.js +24 -0
- package/dist/usage/UsageSummary.d.ts +12 -0
- package/dist/usage/UsageSummary.js +15 -0
- package/dist/usage/UsageTiles.d.ts +7 -0
- package/dist/usage/UsageTiles.js +19 -0
- package/dist/usage/format.d.ts +14 -0
- package/dist/usage/format.js +50 -0
- package/dist/usage/limit-groups.d.ts +22 -0
- package/dist/usage/limit-groups.js +78 -0
- package/dist/usage/limits.d.ts +4 -0
- package/dist/usage/limits.js +58 -0
- package/dist/usage/money.d.ts +1 -0
- package/dist/usage/money.js +13 -0
- package/dist/usage/summary.d.ts +34 -0
- package/dist/usage/summary.js +90 -0
- package/package.json +639 -2
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { ContextMenu } from '@base-ui-components/react/context-menu';
|
|
5
|
+
import { indentLess, indentMore, insertNewline } from '@codemirror/commands';
|
|
6
|
+
import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
|
|
7
|
+
import { EditorView } from '@codemirror/view';
|
|
8
|
+
import clsx from 'clsx';
|
|
9
|
+
import { ArrowUp, ChevronDown, Clock, Copy, FastForward, MessageSquare, Paperclip, Pencil, Plus, Square, SquareSlash, X, Zap } from 'lucide-react';
|
|
10
|
+
import { chatHost } from '../../host';
|
|
11
|
+
import { useChatScope } from '../../scope';
|
|
12
|
+
import { errorCode } from '../../transport';
|
|
13
|
+
import { useAccountChoice } from '../account-choice';
|
|
14
|
+
import { useChatActions } from '../actions';
|
|
15
|
+
import { checkAttachmentLimits, filesOf, formatBytes, isImageAttachment, readAttachments, readStoredAttachments, uploadBytes } from '../attachments';
|
|
16
|
+
import { chatSuggestions } from '../chat-references';
|
|
17
|
+
import { EMPTY_DRAFT, isEmptyDraft, joinDraftText, readDraft, takeBackIntoDraft, takeDraftOffers, writeDraft } from '../drafts';
|
|
18
|
+
import { MENTION_DRAG_TYPE, dropQuery, findMentionQuery, findSkillQuery, insertMention, insertSkill, presentMentions, presentSkills } from '../mentions';
|
|
19
|
+
import { RESUME_COMPACTION_TOKENS, resumeCompactionOffer } from '../logic/resume-compaction';
|
|
20
|
+
import { dismissResumeCompaction, useResumeCompactionDismissal } from '../resume-compaction-dismissals';
|
|
21
|
+
import { composerStopLabel, composerStopOf } from '../subagent-list';
|
|
22
|
+
import { PROMPT_MAX_CHARS, pasteBecomesAttachment, pastedTextName, promptGuard, usableSlashCommands } from '../guards';
|
|
23
|
+
import { withQuote } from '../quote';
|
|
24
|
+
import { rememberChatAccount, rememberChatPreferences, rememberChatSelection } from '../preferences';
|
|
25
|
+
import { STASH_SHORTCUT, stashDraft, useStash } from '../stash';
|
|
26
|
+
import { pageTimeline, scrollTimelineToEnd, subscribeTimelineEnd, timelineAtEnd } from '../timeline-scroll';
|
|
27
|
+
import { ChatActivity } from './ChatActivity';
|
|
28
|
+
import { ChatReferenceChip } from './ChatReferenceChip';
|
|
29
|
+
import { chipDecorations } from './composer/chips';
|
|
30
|
+
import { enterAction, inCode, inFenceBody, inOpenFence, listItemAt, recallDirection, tabSpaces } from './composer/keys';
|
|
31
|
+
import { ComposerInput } from './ComposerInput';
|
|
32
|
+
import { PromptComposer } from './PromptComposer';
|
|
33
|
+
import { QuoteTakerContext } from './quote-selection';
|
|
34
|
+
import { LimitDock } from './LimitState';
|
|
35
|
+
import { ResumeCompactionDock } from './ResumeCompactionDock';
|
|
36
|
+
import { StashPicker } from './Pickers';
|
|
37
|
+
import { RunSettings } from './RunSettings';
|
|
38
|
+
import { UploadThumb } from './UploadThumb';
|
|
39
|
+
import { formatNumber } from '@ruimte/ui/format/number';
|
|
40
|
+
import { useChatRow } from '../../state/chats';
|
|
41
|
+
import { useProviders } from '../../state/providers';
|
|
42
|
+
import { BTN_GROUP, FLOAT, MENU_LABEL, MENU_SEPARATOR } from '@ruimte/ui/classes';
|
|
43
|
+
import { copyText } from '@ruimte/ui/clipboard';
|
|
44
|
+
import { Tooltip } from '@ruimte/ui/Tooltip';
|
|
45
|
+
import { FileIcon } from '@ruimte/ui/FileIcon';
|
|
46
|
+
import { Icon } from '@ruimte/ui/Icon';
|
|
47
|
+
import { PromptDialog } from '@ruimte/ui/PromptDialog';
|
|
48
|
+
import { KEY_SHORTCUTS, isModHeld, matchesShortcut } from '@ruimte/ui/shortcut';
|
|
49
|
+
import { useNow } from '@ruimte/ui/useNow';
|
|
50
|
+
const SEARCH_DEBOUNCE_MS = 80;
|
|
51
|
+
// What fits above the composer without turning the picker into a file tree.
|
|
52
|
+
const MENTION_RESULTS = 8;
|
|
53
|
+
// The chats go above the files, so a few leave the files in view.
|
|
54
|
+
const CHAT_RESULTS = 4;
|
|
55
|
+
const NOTICE_MS = 4000;
|
|
56
|
+
// How many sent prompts the arrow keys walk back through, per chat.
|
|
57
|
+
const PROMPT_HISTORY = 50;
|
|
58
|
+
// Commands the composer handles itself; the CLI's own ones are sent through as text.
|
|
59
|
+
const LOCAL_COMMANDS = ['model', 'compact', 'clear'];
|
|
60
|
+
/* Files and paths dropped on the card become attachments and chips there. CodeMirror would paste a
|
|
61
|
+
file's text, or the drag's own text, at the drop point on top of that. */
|
|
62
|
+
const LEAVE_DROPS_TO_THE_CARD = EditorView.domEventHandlers({
|
|
63
|
+
drop: (event) => event.dataTransfer !== null && (event.dataTransfer.types.includes('Files') || event.dataTransfer.types.includes(MENTION_DRAG_TYPE))
|
|
64
|
+
});
|
|
65
|
+
const usePendingRequests = (chatId) => {
|
|
66
|
+
// The structure, which a delta leaves alone. The prompts and the requests are items of their own.
|
|
67
|
+
const items = useChatRow(chatId, (row) => row?.structure);
|
|
68
|
+
const order = useChatRow(chatId, (row) => row?.order);
|
|
69
|
+
return useMemo(() => {
|
|
70
|
+
const approvals = [];
|
|
71
|
+
const questions = [];
|
|
72
|
+
for (const id of order ?? []) {
|
|
73
|
+
const item = items?.[id];
|
|
74
|
+
if (item?.kind === 'approval' && item.decision === 'pending') {
|
|
75
|
+
approvals.push(item);
|
|
76
|
+
}
|
|
77
|
+
else if (item?.kind === 'question' && item.state === 'pending') {
|
|
78
|
+
questions.push(item);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { approvals, questions };
|
|
82
|
+
}, [items, order]);
|
|
83
|
+
};
|
|
84
|
+
/* The empty box names its sigils as key caps, so they read as keys to press rather than as punctuation. */
|
|
85
|
+
const hintedPlaceholder = (lead, joiner, hints) => {
|
|
86
|
+
const root = document.createElement('span');
|
|
87
|
+
root.className = 'composer-hints';
|
|
88
|
+
const extra = document.createElement('span');
|
|
89
|
+
extra.className = 'composer-hints-extra';
|
|
90
|
+
extra.append(joiner);
|
|
91
|
+
for (const hint of hints) {
|
|
92
|
+
const cap = document.createElement('kbd');
|
|
93
|
+
cap.textContent = hint.key;
|
|
94
|
+
extra.append(cap, hint.label);
|
|
95
|
+
}
|
|
96
|
+
root.append(lead, extra);
|
|
97
|
+
return root;
|
|
98
|
+
};
|
|
99
|
+
const splitPath = (path) => {
|
|
100
|
+
const slash = path.lastIndexOf('/');
|
|
101
|
+
return slash < 0 ? { name: path, dir: '' } : { name: path.slice(slash + 1), dir: path.slice(0, slash) };
|
|
102
|
+
};
|
|
103
|
+
// Keep the editor mounted while a pending request takes over, so its selection and draft survive.
|
|
104
|
+
export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabled, providerFixed, onSend, onRetarget }) {
|
|
105
|
+
const { t } = useTranslation('agent-chat');
|
|
106
|
+
const [draft, setDraft] = useState(() => readDraft(chatId));
|
|
107
|
+
const [historyIndex, setHistoryIndex] = useState(null);
|
|
108
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
109
|
+
const [mention, setMention] = useState(null);
|
|
110
|
+
const [skillQuery, setSkillQuery] = useState(null);
|
|
111
|
+
const [skills, setSkills] = useState([]);
|
|
112
|
+
const [searched, setSearched] = useState([]);
|
|
113
|
+
const [notice, setNotice] = useState(null);
|
|
114
|
+
const [dragging, setDragging] = useState(false);
|
|
115
|
+
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
|
116
|
+
const [confirmClear, setConfirmClear] = useState(false);
|
|
117
|
+
const [takingBack, setTakingBack] = useState(null);
|
|
118
|
+
const scope = useChatScope();
|
|
119
|
+
const actions = useChatActions();
|
|
120
|
+
const { isApplePlatform, notify } = chatHost();
|
|
121
|
+
const registerQuoteTaker = useContext(QuoteTakerContext);
|
|
122
|
+
const inputRef = useRef(null);
|
|
123
|
+
// Taking a queued message back waits on the machine, and what was typed meanwhile is what it merges with.
|
|
124
|
+
const draftRef = useRef(draft);
|
|
125
|
+
const fileInputRef = useRef(null);
|
|
126
|
+
const [dictationToolbar, setDictationToolbar] = useState(null);
|
|
127
|
+
// A paste event says nothing about the keys behind it, so the key that asked for text inline is remembered here.
|
|
128
|
+
const pasteInlineRef = useRef(false);
|
|
129
|
+
const providers = useProviders((s) => s.providers);
|
|
130
|
+
const { approvals, questions } = usePendingRequests(chatId);
|
|
131
|
+
const order = useChatRow(chatId, (row) => row?.order);
|
|
132
|
+
// The structure, which a delta leaves alone. The prompts and the requests are items of their own.
|
|
133
|
+
const items = useChatRow(chatId, (row) => row?.structure);
|
|
134
|
+
const chats = chatHost().useReferableChats();
|
|
135
|
+
const pending = useMemo(() => [...approvals, ...questions], [approvals, questions]);
|
|
136
|
+
const provider = providers.find((entry) => entry.kind === info.provider);
|
|
137
|
+
// Absent until the host answered `provider.list`, so only an explicit false hides anything.
|
|
138
|
+
const capabilities = provider?.capabilities;
|
|
139
|
+
const compaction = capabilities?.compaction;
|
|
140
|
+
const dismissedTurnId = useResumeCompactionDismissal(chatId);
|
|
141
|
+
/* The cheap half of the offer, so a chat that can never make one keeps no timer. */
|
|
142
|
+
const couldOfferCompaction = compaction !== undefined && compaction !== 'none' && info.usage.contextTokens >= RESUME_COMPACTION_TOKENS;
|
|
143
|
+
const now = useNow(60_000, couldOfferCompaction);
|
|
144
|
+
const compactionOffer = !couldOfferCompaction
|
|
145
|
+
? null
|
|
146
|
+
: resumeCompactionOffer({
|
|
147
|
+
info,
|
|
148
|
+
compaction,
|
|
149
|
+
items: (order ?? []).map((id) => items?.[id]).filter((item) => item !== undefined),
|
|
150
|
+
dismissedTurnId,
|
|
151
|
+
now
|
|
152
|
+
});
|
|
153
|
+
const models = provider?.models ?? [];
|
|
154
|
+
const model = models.find((entry) => entry.slug === info.selection.model);
|
|
155
|
+
const busy = info.activeTurnId !== null;
|
|
156
|
+
const queue = info.queue ?? [];
|
|
157
|
+
const text = draft.text;
|
|
158
|
+
const quote = draft.quote;
|
|
159
|
+
// The CLI announces its session on the first message, so anything before that is still a blank chat.
|
|
160
|
+
const started = info.agentSessionId !== null || info.usage.turns > 0;
|
|
161
|
+
/*
|
|
162
|
+
* A chat that has started keeps its provider (the CLI holds the thread), and one opened for a
|
|
163
|
+
* named CLI was never going to change it. Either way the models stay switchable. The CLI takes
|
|
164
|
+
* the new one on the restart the next send does anyway. With one provider in the list the
|
|
165
|
+
* picker drops its group headers on its own, so it reads as that CLI's own catalog.
|
|
166
|
+
*/
|
|
167
|
+
const accountChoice = useAccountChoice(info.provider, info.account);
|
|
168
|
+
const pickable = started || providerFixed
|
|
169
|
+
? providers.filter((entry) => entry.kind === info.provider)
|
|
170
|
+
: providers.filter((entry) => entry.installed && entry.capabilities.chat);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (focused) {
|
|
173
|
+
inputRef.current?.focus();
|
|
174
|
+
}
|
|
175
|
+
}, [focused]);
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
draftRef.current = draft;
|
|
178
|
+
writeDraft(chatId, draft);
|
|
179
|
+
}, [chatId, draft]);
|
|
180
|
+
useEffect(() => takeDraftOffers(chatId, (offered) => {
|
|
181
|
+
setDraft((current) => ({ ...current, text: joinDraftText(current.text, offered) }));
|
|
182
|
+
inputRef.current?.focus();
|
|
183
|
+
}), [chatId]);
|
|
184
|
+
useEffect(() => registerQuoteTaker?.((selected) => {
|
|
185
|
+
setDraft((current) => ({ ...current, quote: selected }));
|
|
186
|
+
inputRef.current?.focus();
|
|
187
|
+
}), [registerQuoteTaker]);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (!notice) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const timer = setTimeout(() => setNotice(null), NOTICE_MS);
|
|
193
|
+
return () => clearTimeout(timer);
|
|
194
|
+
}, [notice]);
|
|
195
|
+
// Every keystroke inside an `@word` searches again; a slow answer must not overwrite a newer one.
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (mention === null) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let stale = false;
|
|
201
|
+
const search = () => {
|
|
202
|
+
const search = chatHost().searchFiles;
|
|
203
|
+
if (search === null) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
search(scope.id, info.cwd, mention.query, MENTION_RESULTS)
|
|
207
|
+
.then((files) => {
|
|
208
|
+
if (!stale) {
|
|
209
|
+
setSearched(files);
|
|
210
|
+
setMenuIndex(0);
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
.catch(() => undefined);
|
|
214
|
+
};
|
|
215
|
+
// A bare `@` is not a keystroke in a query. It opens the list of what is there, at once.
|
|
216
|
+
if (mention.query === '') {
|
|
217
|
+
search();
|
|
218
|
+
return () => {
|
|
219
|
+
stale = true;
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const timer = setTimeout(search, SEARCH_DEBOUNCE_MS);
|
|
223
|
+
return () => {
|
|
224
|
+
stale = true;
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
};
|
|
227
|
+
}, [scope, info.cwd, mention]);
|
|
228
|
+
/* The host's skill list, once per chat and again once the CLI announced its own after the first message. */
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
let stale = false;
|
|
231
|
+
scope.chats
|
|
232
|
+
.listSkills(chatId)
|
|
233
|
+
.then((found) => {
|
|
234
|
+
if (!stale) {
|
|
235
|
+
setSkills(found);
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
.catch(() => undefined);
|
|
239
|
+
return () => {
|
|
240
|
+
stale = true;
|
|
241
|
+
};
|
|
242
|
+
}, [scope, chatId, info.skills]);
|
|
243
|
+
const history = useMemo(() => {
|
|
244
|
+
const prompts = [];
|
|
245
|
+
for (const id of order ?? []) {
|
|
246
|
+
const item = items?.[id];
|
|
247
|
+
if (item?.kind === 'user') {
|
|
248
|
+
prompts.push({ text: item.text, mentions: item.mentions ?? [] });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Walking back further than this is faster with the timeline than with the arrow key.
|
|
252
|
+
return prompts.slice(-PROMPT_HISTORY);
|
|
253
|
+
}, [order, items]);
|
|
254
|
+
const commandQuery = text.startsWith('/') && !text.includes('\n') ? text.slice(1).trim().toLowerCase() : null;
|
|
255
|
+
const commands = useMemo(() => {
|
|
256
|
+
if (commandQuery === null) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
const known = new Set(skills.map((skill) => skill.name));
|
|
260
|
+
const own = LOCAL_COMMANDS.map((name) => ({ name, hint: t(`composer.commands.${name}`), local: true, skill: false }));
|
|
261
|
+
const cli = usableSlashCommands(info.slashCommands)
|
|
262
|
+
.filter((name) => !LOCAL_COMMANDS.includes(name))
|
|
263
|
+
.map((name) => ({
|
|
264
|
+
name,
|
|
265
|
+
hint: known.has(name) ? t('composer.commands.skill') : t('composer.commands.cli', { cli: provider?.name ?? t('composer.commands.anyCli') }),
|
|
266
|
+
local: false,
|
|
267
|
+
skill: known.has(name)
|
|
268
|
+
}));
|
|
269
|
+
return [...own, ...cli].filter((command) => command.name.startsWith(commandQuery)).slice(0, 8);
|
|
270
|
+
}, [commandQuery, info.slashCommands, provider?.name, skills, t]);
|
|
271
|
+
const skillMatches = useMemo(() => {
|
|
272
|
+
if (skillQuery === null) {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
const query = skillQuery.query.toLowerCase();
|
|
276
|
+
return skills.filter((skill) => skill.name.toLowerCase().includes(query)).slice(0, 8);
|
|
277
|
+
}, [skillQuery, skills]);
|
|
278
|
+
const commandMenuOpen = commandQuery !== null && commands.length > 0;
|
|
279
|
+
const skillMenuOpen = !commandMenuOpen && skillQuery !== null && skillMatches.length > 0;
|
|
280
|
+
const mentionMenuOpen = !commandMenuOpen && !skillMenuOpen && mention !== null;
|
|
281
|
+
// Results belong to the query that asked for them; a closed picker shows none while the next answer is on its way.
|
|
282
|
+
const files = mention === null ? [] : searched;
|
|
283
|
+
const chatMatches = useMemo(() => (mention === null ? [] : chatSuggestions(chats, mention.query, [chatId, ...draft.chats], CHAT_RESULTS)), [chatId, chats, draft.chats, mention]);
|
|
284
|
+
const mentionCount = chatMatches.length + files.length;
|
|
285
|
+
const editorExtensions = useMemo(() => [chipDecorations({ mentions: draft.mentions, skills: draft.skills }), LEAVE_DROPS_TO_THE_CARD], [draft.mentions, draft.skills]);
|
|
286
|
+
const guard = promptGuard(withQuote(quote, text));
|
|
287
|
+
const setText = (next, mentions = draft.mentions, chosen = draft.skills) => {
|
|
288
|
+
setDraft((current) => ({ ...current, text: next, mentions, skills: chosen }));
|
|
289
|
+
};
|
|
290
|
+
const clearDraft = () => {
|
|
291
|
+
setDraft(EMPTY_DRAFT);
|
|
292
|
+
};
|
|
293
|
+
const removeQuote = () => {
|
|
294
|
+
setDraft((current) => ({ ...current, quote: '' }));
|
|
295
|
+
inputRef.current?.focus();
|
|
296
|
+
};
|
|
297
|
+
const configure = (patch) => {
|
|
298
|
+
if (patch.selection) {
|
|
299
|
+
rememberChatSelection(info.provider, patch.selection);
|
|
300
|
+
}
|
|
301
|
+
if (patch.runtimeMode) {
|
|
302
|
+
rememberChatPreferences({ runtimeMode: patch.runtimeMode });
|
|
303
|
+
}
|
|
304
|
+
void actions.configure(chatId, patch).catch(() => undefined);
|
|
305
|
+
};
|
|
306
|
+
/* From the next turn on, and the default for new agents of this CLI on this machine, as a model pick is. */
|
|
307
|
+
const chooseAccount = (account) => {
|
|
308
|
+
void actions
|
|
309
|
+
.configure(chatId, { account })
|
|
310
|
+
.then(() => rememberChatAccount(scope.id, info.provider, account))
|
|
311
|
+
.catch((e) => setNotice(e instanceof Error ? e.message : String(e)));
|
|
312
|
+
};
|
|
313
|
+
/* A model of another CLI re-points the whole chat; one of this CLI's own is a configure. */
|
|
314
|
+
const chooseModel = (provider, slug) => {
|
|
315
|
+
const selection = { model: slug, options: {} };
|
|
316
|
+
if (provider !== info.provider) {
|
|
317
|
+
rememberChatSelection(provider, selection);
|
|
318
|
+
onRetarget(provider, selection);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
configure({ selection });
|
|
322
|
+
};
|
|
323
|
+
const stop = (shiftKey) => {
|
|
324
|
+
if (composerStopOf(shiftKey) === 'turn') {
|
|
325
|
+
void actions.stopTurn(chatId, false).catch(() => undefined);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
chatHost().confirm.stopSubagents(scope.id, chatId, () => void actions.stopTurn(chatId, true).catch(() => undefined));
|
|
329
|
+
};
|
|
330
|
+
/* The host decides whether a turn is in the way; only its refusal asks the person first. */
|
|
331
|
+
const clearThread = async () => {
|
|
332
|
+
try {
|
|
333
|
+
await actions.clear(chatId, false);
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
if (errorCode(e) === 'chat-busy') {
|
|
337
|
+
setConfirmClear(true);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
setNotice(t('composer.notice.clearFailed'));
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
const forceClearThread = async () => {
|
|
344
|
+
await actions.clear(chatId, true);
|
|
345
|
+
setConfirmClear(false);
|
|
346
|
+
};
|
|
347
|
+
const compact = () => {
|
|
348
|
+
void actions.compact(chatId).catch(() => setNotice(t('composer.notice.compactFailed')));
|
|
349
|
+
};
|
|
350
|
+
const runCommand = (name) => {
|
|
351
|
+
switch (name) {
|
|
352
|
+
case 'model':
|
|
353
|
+
setModelPickerOpen(true);
|
|
354
|
+
return true;
|
|
355
|
+
case 'compact':
|
|
356
|
+
void actions.compact(chatId).catch(() => undefined);
|
|
357
|
+
return true;
|
|
358
|
+
case 'clear':
|
|
359
|
+
void clearThread();
|
|
360
|
+
return true;
|
|
361
|
+
default:
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
const same = (a, b) => a?.start === b?.start && a?.query === b?.query;
|
|
366
|
+
/* Which picker the caret opens: `@` for a file, `$` for a skill, or neither. In code a sigil is just text. */
|
|
367
|
+
const trackTriggers = (value, selection, state) => {
|
|
368
|
+
const empty = selection.from === selection.to;
|
|
369
|
+
const tree = empty ? (ensureSyntaxTree(state, selection.from, 50) ?? syntaxTree(state)) : null;
|
|
370
|
+
const caret = tree !== null && !inCode(tree, value, selection.from) ? selection.from : null;
|
|
371
|
+
const skill = caret === null ? null : findSkillQuery(value, caret);
|
|
372
|
+
setSkillQuery((current) => (same(current, skill) ? current : skill));
|
|
373
|
+
// A CLI that does not expand `@path` gets the text as it is, so the picker stays out of the way.
|
|
374
|
+
const next = caret === null || capabilities?.mentions === false ? null : findMentionQuery(value, caret);
|
|
375
|
+
setMention((current) => (same(current, next) ? current : next));
|
|
376
|
+
};
|
|
377
|
+
// The inserted text reaches the editor with the render it causes, so the caret waits a frame for it.
|
|
378
|
+
const moveCaret = (caret) => {
|
|
379
|
+
requestAnimationFrame(() => inputRef.current?.setCaret(caret));
|
|
380
|
+
};
|
|
381
|
+
const chooseMention = (path) => {
|
|
382
|
+
if (!mention) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const result = insertMention(text, mention, path);
|
|
386
|
+
setText(result.text, draft.mentions.includes(path) ? draft.mentions : [...draft.mentions, path]);
|
|
387
|
+
setMention(null);
|
|
388
|
+
moveCaret(result.caret);
|
|
389
|
+
};
|
|
390
|
+
/* A picked chat sits beside the text as a chip, so the `@query` that found it goes. */
|
|
391
|
+
const chooseChat = (id) => {
|
|
392
|
+
if (!mention) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const result = dropQuery(text, mention);
|
|
396
|
+
setDraft((current) => ({ ...current, text: result.text, chats: current.chats.includes(id) ? current.chats : [...current.chats, id] }));
|
|
397
|
+
setMention(null);
|
|
398
|
+
moveCaret(result.caret);
|
|
399
|
+
};
|
|
400
|
+
const chooseMentionAt = (index) => {
|
|
401
|
+
const chat = chatMatches[index];
|
|
402
|
+
if (chat) {
|
|
403
|
+
chooseChat(chat.id);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const path = files[index - chatMatches.length] ?? files[0];
|
|
407
|
+
if (path !== undefined) {
|
|
408
|
+
chooseMention(path);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
const removeChat = (id) => {
|
|
412
|
+
setDraft((current) => ({ ...current, chats: current.chats.filter((entry) => entry !== id) }));
|
|
413
|
+
inputRef.current?.focus();
|
|
414
|
+
};
|
|
415
|
+
/*
|
|
416
|
+
* A picked skill becomes a `$name` chip in the text and a name on the send, which is what
|
|
417
|
+
* dispatches it. The `/` menu lands here too, so both spellings end up on the same path.
|
|
418
|
+
*/
|
|
419
|
+
const chooseSkill = (name) => {
|
|
420
|
+
const result = skillQuery ? insertSkill(text, skillQuery, name) : { text: `$${name} `, caret: name.length + 2 };
|
|
421
|
+
setText(result.text, draft.mentions, draft.skills.includes(name) ? draft.skills : [...draft.skills, name]);
|
|
422
|
+
setSkillQuery(null);
|
|
423
|
+
moveCaret(result.caret);
|
|
424
|
+
};
|
|
425
|
+
/* Paths dragged in from a list of files. There is no mention query to insert into, so they land
|
|
426
|
+
at the end of the prompt and join the draft's mentions, which is what `chat.send` carries. */
|
|
427
|
+
const addMentions = (paths) => {
|
|
428
|
+
const fresh = paths.filter((path) => path !== '');
|
|
429
|
+
if (fresh.length === 0 || capabilities?.mentions === false) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const appended = fresh.map((path) => `@${path}`).join(' ');
|
|
433
|
+
const before = text.replace(/\s+$/, '');
|
|
434
|
+
setText(before === '' ? `${appended} ` : `${before} ${appended} `, [...draft.mentions, ...fresh.filter((path) => !draft.mentions.includes(path))]);
|
|
435
|
+
};
|
|
436
|
+
const addFiles = (incoming) => {
|
|
437
|
+
if (incoming.length === 0) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (capabilities?.attachments === false) {
|
|
441
|
+
setNotice(t('composer.notice.noAttachments', { agent: provider?.name ?? t('composer.notice.thisAgent') }));
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const checked = checkAttachmentLimits(draft.attachments.length, incoming.map((file) => ({ name: file.name, mime: file.type, bytes: file.size, file })), draft.attachments.reduce((bytes, attachment) => bytes + uploadBytes(attachment), 0));
|
|
445
|
+
if (checked.rejected[0]) {
|
|
446
|
+
setNotice(t('composer.notice.rejected', { name: checked.rejected[0].name || t('composer.notice.thatFile'), reason: checked.rejected[0].reason }));
|
|
447
|
+
}
|
|
448
|
+
if (checked.accepted.length === 0) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
readAttachments(checked.accepted.map((entry) => entry.file))
|
|
452
|
+
.then((attachments) => setDraft((current) => ({ ...current, attachments: [...current.attachments, ...attachments] })))
|
|
453
|
+
.catch(() => setNotice(t('composer.notice.unreadable')));
|
|
454
|
+
};
|
|
455
|
+
const removeAttachment = (index) => {
|
|
456
|
+
setDraft((current) => ({ ...current, attachments: current.attachments.filter((_, i) => i !== index) }));
|
|
457
|
+
};
|
|
458
|
+
const submit = () => {
|
|
459
|
+
const trimmed = text.trim();
|
|
460
|
+
if (isEmptyDraft(draft) || disabled || guard.tooLong) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
// Recheck restored drafts and files that finished reading concurrently.
|
|
464
|
+
const rejected = checkAttachmentLimits(0, draft.attachments.map((attachment) => ({ ...attachment, bytes: uploadBytes(attachment) }))).rejected[0];
|
|
465
|
+
if (rejected) {
|
|
466
|
+
setNotice(`${rejected.name}: ${rejected.reason}`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const chosen = commands[menuIndex];
|
|
470
|
+
if (commandQuery !== null && chosen?.local && runCommand(chosen.name)) {
|
|
471
|
+
clearDraft();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (commandQuery !== null && chosen?.skill) {
|
|
475
|
+
chooseSkill(chosen.name);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (commandQuery !== null && chosen && !chosen.local) {
|
|
479
|
+
onSend(`/${chosen.name}`, {});
|
|
480
|
+
clearDraft();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
onSend(withQuote(quote, trimmed), {
|
|
484
|
+
mentions: presentMentions(trimmed, draft.mentions),
|
|
485
|
+
skills: presentSkills(trimmed, draft.skills),
|
|
486
|
+
chats: draft.chats,
|
|
487
|
+
attachments: draft.attachments
|
|
488
|
+
});
|
|
489
|
+
clearDraft();
|
|
490
|
+
setMention(null);
|
|
491
|
+
setSkillQuery(null);
|
|
492
|
+
setHistoryIndex(null);
|
|
493
|
+
};
|
|
494
|
+
/* A stashed prompt comes back as text, mentions and skills; its files were never kept. */
|
|
495
|
+
const restoreStashed = (prompt) => {
|
|
496
|
+
setDraft((current) => ({ ...current, text: prompt.text, mentions: [...prompt.mentions], skills: [...prompt.skills] }));
|
|
497
|
+
setHistoryIndex(null);
|
|
498
|
+
inputRef.current?.focus();
|
|
499
|
+
};
|
|
500
|
+
/*
|
|
501
|
+
* The files come back from the machine before the message leaves the queue, since the machine only
|
|
502
|
+
* serves a file some message still holds. The machine's refusal is what says it already went out.
|
|
503
|
+
*/
|
|
504
|
+
const takeBack = async (message) => {
|
|
505
|
+
if (takingBack !== null) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const failed = (e) => {
|
|
509
|
+
notify({ kind: 'error', title: t('composer.queue.editFailed'), description: e instanceof Error ? e.message : String(e) });
|
|
510
|
+
};
|
|
511
|
+
const readFiles = async () => {
|
|
512
|
+
const stored = message.attachments ?? [];
|
|
513
|
+
if (stored.length === 0) {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
const files = chatHost().attachments;
|
|
517
|
+
return readStoredAttachments((attachment) => files.read(scope.id, chatId, attachment.id), stored);
|
|
518
|
+
};
|
|
519
|
+
setTakingBack(message.id);
|
|
520
|
+
try {
|
|
521
|
+
let uploads;
|
|
522
|
+
try {
|
|
523
|
+
uploads = await readFiles();
|
|
524
|
+
}
|
|
525
|
+
catch (e) {
|
|
526
|
+
failed(e);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const taken = {
|
|
530
|
+
text: message.text,
|
|
531
|
+
mentions: message.mentions ?? [],
|
|
532
|
+
skills: message.skills ?? [],
|
|
533
|
+
chats: message.chats ?? [],
|
|
534
|
+
attachments: uploads
|
|
535
|
+
};
|
|
536
|
+
// A file that would not fit is refused here, while the message still holds it on the machine.
|
|
537
|
+
const wouldReject = takeBackIntoDraft(draftRef.current, taken).rejected[0];
|
|
538
|
+
if (wouldReject) {
|
|
539
|
+
setNotice(t('composer.notice.queuedDoesNotFit', { name: wouldReject.name || t('composer.notice.thatFile'), reason: wouldReject.reason }));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
await actions.unqueue(chatId, message.id);
|
|
544
|
+
}
|
|
545
|
+
catch (e) {
|
|
546
|
+
if (errorCode(e) === 'request-not-found') {
|
|
547
|
+
notify({ kind: 'error', title: t('composer.queue.alreadySent') });
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
failed(e);
|
|
551
|
+
}
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const merged = takeBackIntoDraft(draftRef.current, taken);
|
|
555
|
+
setDraft(merged.draft);
|
|
556
|
+
if (merged.rejected[0]) {
|
|
557
|
+
setNotice(t('composer.notice.rejected', { name: merged.rejected[0].name, reason: merged.rejected[0].reason }));
|
|
558
|
+
}
|
|
559
|
+
setHistoryIndex(null);
|
|
560
|
+
inputRef.current?.focus();
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
setTakingBack(null);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
/* Cmd+S puts the draft away and clears the box; on an empty box the same key takes the last one back. */
|
|
567
|
+
const toggleStash = () => {
|
|
568
|
+
if (isEmptyDraft(draft)) {
|
|
569
|
+
const newest = useStash.getState().prompts[0];
|
|
570
|
+
if (newest) {
|
|
571
|
+
restoreStashed(newest);
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (stashDraft(draft)) {
|
|
576
|
+
clearDraft();
|
|
577
|
+
setMention(null);
|
|
578
|
+
setSkillQuery(null);
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
const recall = (direction) => {
|
|
582
|
+
if (history.length === 0) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
const next = historyIndex === null ? (direction === -1 ? history.length - 1 : null) : historyIndex + direction;
|
|
586
|
+
if (next === null || next >= history.length) {
|
|
587
|
+
setHistoryIndex(null);
|
|
588
|
+
setText('', [], []);
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
if (next < 0) {
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
setHistoryIndex(next);
|
|
595
|
+
setText(history[next].text, history[next].mentions);
|
|
596
|
+
return true;
|
|
597
|
+
};
|
|
598
|
+
/* Runs before the editor's own keymap; true means the key is handled and its default prevented. */
|
|
599
|
+
const onKeyDown = (e, view) => {
|
|
600
|
+
// Shift with the paste shortcut pastes a large text inline after all; any other key forgets it.
|
|
601
|
+
pasteInlineRef.current = e.shiftKey && e.key.toLowerCase() === 'v' && (isApplePlatform() ? e.metaKey : e.ctrlKey);
|
|
602
|
+
// Escape leaves the node, unless it first has a recalled prompt to put back.
|
|
603
|
+
if (e.key === 'Escape' && !e.isComposing) {
|
|
604
|
+
if (historyIndex === null) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
e.stopPropagation();
|
|
608
|
+
setHistoryIndex(null);
|
|
609
|
+
setText('', [], []);
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
// The composer keeps the keyboard while you type, but the shortcuts that move between views,
|
|
613
|
+
// panels and the palette stay the app's, untouched: Mod+Shift+Enter would otherwise send as well.
|
|
614
|
+
if (chatHost().isAppShortcut(e)) {
|
|
615
|
+
return false;
|
|
616
|
+
}
|
|
617
|
+
e.stopPropagation();
|
|
618
|
+
// A key inside an IME composition belongs to the composition, Enter and the arrows included.
|
|
619
|
+
if (e.isComposing) {
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
// Reading back through a long answer should not mean leaving the box you are typing in.
|
|
623
|
+
if (e.key === 'PageUp' || e.key === 'PageDown') {
|
|
624
|
+
return pageTimeline(chatId, e.key === 'PageUp' ? -1 : 1);
|
|
625
|
+
}
|
|
626
|
+
if (matchesShortcut(STASH_SHORTCUT, e, isApplePlatform())) {
|
|
627
|
+
toggleStash();
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
if (commandMenuOpen) {
|
|
631
|
+
if (e.key === 'ArrowDown') {
|
|
632
|
+
setMenuIndex((i) => (i + 1) % commands.length);
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
if (e.key === 'ArrowUp') {
|
|
636
|
+
setMenuIndex((i) => (i - 1 + commands.length) % commands.length);
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
if (e.key === 'Tab') {
|
|
640
|
+
const chosen = commands[menuIndex];
|
|
641
|
+
if (chosen.skill) {
|
|
642
|
+
chooseSkill(chosen.name);
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
setText(`/${chosen.name} `);
|
|
646
|
+
}
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (skillMenuOpen) {
|
|
651
|
+
if (e.key === 'ArrowDown') {
|
|
652
|
+
setMenuIndex((i) => (i + 1) % skillMatches.length);
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
if (e.key === 'ArrowUp') {
|
|
656
|
+
setMenuIndex((i) => (i - 1 + skillMatches.length) % skillMatches.length);
|
|
657
|
+
return true;
|
|
658
|
+
}
|
|
659
|
+
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
660
|
+
chooseSkill((skillMatches[menuIndex] ?? skillMatches[0]).name);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (mentionMenuOpen && mentionCount > 0) {
|
|
665
|
+
if (e.key === 'ArrowDown') {
|
|
666
|
+
setMenuIndex((i) => (i + 1) % mentionCount);
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
if (e.key === 'ArrowUp') {
|
|
670
|
+
setMenuIndex((i) => (i - 1 + mentionCount) % mentionCount);
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
if (e.key === 'Tab' || e.key === 'Enter') {
|
|
674
|
+
chooseMentionAt(menuIndex);
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
// In a fence Tab indents like a code editor; anywhere else it keeps moving the focus.
|
|
679
|
+
if (e.key === 'Tab' && !e.altKey && !e.metaKey && !e.ctrlKey) {
|
|
680
|
+
const { state } = view;
|
|
681
|
+
const { from, to, head } = state.selection.main;
|
|
682
|
+
const tree = ensureSyntaxTree(state, state.doc.length, 50) ?? syntaxTree(state);
|
|
683
|
+
if (inFenceBody(tree, state.doc.toString(), head)) {
|
|
684
|
+
// Handled even when there is nothing to outdent, so Shift+Tab never throws the focus out of a block.
|
|
685
|
+
if (e.shiftKey) {
|
|
686
|
+
indentLess(view);
|
|
687
|
+
}
|
|
688
|
+
else if (from !== to) {
|
|
689
|
+
indentMore(view);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
view.dispatch(state.replaceSelection(tabSpaces(head - state.doc.lineAt(head).from)), { scrollIntoView: true, userEvent: 'input' });
|
|
693
|
+
}
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (e.key === 'Enter') {
|
|
698
|
+
const { state } = view;
|
|
699
|
+
const { from, to, head } = state.selection.main;
|
|
700
|
+
// Parsed to the end, since the fence that closes the block can sit after the caret.
|
|
701
|
+
const tree = ensureSyntaxTree(state, state.doc.length, 50) ?? syntaxTree(state);
|
|
702
|
+
const line = state.doc.lineAt(head);
|
|
703
|
+
const list = from === to && !inCode(tree, state.doc.toString(), head) ? listItemAt(line.text) : null;
|
|
704
|
+
const action = enterAction({ shift: e.shiftKey, mod: isModHeld(e, isApplePlatform()) }, { inOpenFence: inOpenFence(tree, head), list, column: head - line.from });
|
|
705
|
+
if (action === 'newline') {
|
|
706
|
+
return insertNewline(view);
|
|
707
|
+
}
|
|
708
|
+
if (action === 'continue-list' && list) {
|
|
709
|
+
view.dispatch({
|
|
710
|
+
changes: { from: head, insert: `\n${list.next}` },
|
|
711
|
+
selection: { anchor: head + 1 + list.next.length },
|
|
712
|
+
scrollIntoView: true,
|
|
713
|
+
userEvent: 'input'
|
|
714
|
+
});
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
if (action === 'leave-list') {
|
|
718
|
+
view.dispatch({ changes: { from: line.from, to: line.to }, selection: { anchor: line.from }, userEvent: 'delete' });
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
submit();
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
const { from, to } = view.state.selection.main;
|
|
725
|
+
const recalled = historyIndex !== null && text === history[historyIndex]?.text;
|
|
726
|
+
const direction = recallDirection({ key: e.key, text, from, to, recalled });
|
|
727
|
+
return direction !== null && recall(direction);
|
|
728
|
+
};
|
|
729
|
+
/* Files become attachments and a long text a file of its own; anything else the editor pastes as plain text. */
|
|
730
|
+
const onPaste = (e, view) => {
|
|
731
|
+
const data = e.clipboardData;
|
|
732
|
+
if (!data) {
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
const pasted = filesOf(data);
|
|
736
|
+
if (pasted.length > 0) {
|
|
737
|
+
addFiles(pasted);
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
const inline = pasteInlineRef.current;
|
|
741
|
+
pasteInlineRef.current = false;
|
|
742
|
+
const clip = data.getData('text/plain');
|
|
743
|
+
// Without attachments there is nowhere else for the text to go, so it pastes as it always did.
|
|
744
|
+
if (inline || capabilities?.attachments === false || !pasteBecomesAttachment(clip)) {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
// The paste still replaces what was selected; it just puts nothing in its place.
|
|
748
|
+
const { from, to } = view.state.selection.main;
|
|
749
|
+
if (from !== to) {
|
|
750
|
+
view.dispatch({ changes: { from, to }, userEvent: 'delete' });
|
|
751
|
+
}
|
|
752
|
+
const name = pastedTextName(draft.attachments.map((attachment) => attachment.name));
|
|
753
|
+
addFiles([new File([clip], name, { type: 'text/plain' })]);
|
|
754
|
+
return true;
|
|
755
|
+
};
|
|
756
|
+
/* The timeline owns the scroller; this is the one bit of it the composer needs to know. */
|
|
757
|
+
const atEnd = useSyncExternalStore(subscribeTimelineEnd, () => timelineAtEnd(chatId), () => true);
|
|
758
|
+
const mentionable = capabilities?.mentions !== false;
|
|
759
|
+
const hasSkills = skills.length > 0;
|
|
760
|
+
const placeholder = useMemo(() => {
|
|
761
|
+
if (disabled) {
|
|
762
|
+
return t('composer.placeholder.disconnected');
|
|
763
|
+
}
|
|
764
|
+
return hintedPlaceholder(t('composer.placeholder.lead'), t('composer.placeholder.joiner'), [
|
|
765
|
+
{ key: '/', label: t('composer.placeholder.commands') },
|
|
766
|
+
...(mentionable ? [{ key: '@', label: t('composer.placeholder.files') }] : []),
|
|
767
|
+
...(hasSkills ? [{ key: '$', label: t('composer.placeholder.skills') }] : [])
|
|
768
|
+
]);
|
|
769
|
+
}, [disabled, mentionable, hasSkills, t]);
|
|
770
|
+
const attachable = capabilities?.attachments !== false;
|
|
771
|
+
return (_jsxs("div", { className: "chat-composer-content pointer-events-none relative z-10 w-full", children: [!atEnd && (_jsx("div", { className: "absolute inset-x-0 bottom-full mb-2 flex justify-center", children: _jsx(Tooltip, { label: t('composer.jumpToEnd'), name: true, children: _jsx("button", { className: `${FLOAT} pointer-events-auto grid h-8 w-8 place-items-center rounded-full text-text-muted hover:text-text`, onClick: () => scrollTimelineToEnd(chatId), children: _jsx(Icon, { icon: ChevronDown, size: 16 }) }) }) })), _jsx(ChatActivity, { chatId: chatId }), _jsx("div", { className: clsx('@container/composer pointer-events-auto flex flex-col overflow-hidden rounded-2xl border shadow-float backdrop-blur-[14px] focus-ring-within', dragging
|
|
772
|
+
? 'border-accent bg-[color-mix(in_srgb,var(--accent-soft)_60%,var(--surface-raised))]'
|
|
773
|
+
: 'border-border bg-[color-mix(in_srgb,var(--surface-raised)_92%,transparent)]'), "data-takes-drop": "all", onDragOver: (e) => {
|
|
774
|
+
if (e.dataTransfer.types.includes('Files') || e.dataTransfer.types.includes(MENTION_DRAG_TYPE)) {
|
|
775
|
+
e.preventDefault();
|
|
776
|
+
e.stopPropagation();
|
|
777
|
+
setDragging(true);
|
|
778
|
+
}
|
|
779
|
+
}, onDragLeave: (e) => {
|
|
780
|
+
if (!e.currentTarget.contains(e.relatedTarget)) {
|
|
781
|
+
setDragging(false);
|
|
782
|
+
}
|
|
783
|
+
}, onDrop: (e) => {
|
|
784
|
+
e.preventDefault();
|
|
785
|
+
e.stopPropagation();
|
|
786
|
+
setDragging(false);
|
|
787
|
+
addMentions(e.dataTransfer.getData(MENTION_DRAG_TYPE).split(' '));
|
|
788
|
+
addFiles(filesOf(e.dataTransfer));
|
|
789
|
+
inputRef.current?.focus();
|
|
790
|
+
}, children: _jsxs(PromptComposer, { chatId: chatId, pending: pending, focused: focused, onShowElsewhere: answerPromptsElsewhere, disabled: disabled, hasDraft: !isEmptyDraft(draft), denyReason: capabilities?.denyReason === true, onAllAnswered: () => inputRef.current?.focus(), children: [_jsx(LimitDock, { chatId: chatId, info: info }), compactionOffer && (_jsx(ResumeCompactionDock, { tokens: compactionOffer.tokens, onCompact: compact, onDismiss: () => dismissResumeCompaction(scope, chatId, compactionOffer.turnId) })), commandMenuOpen && (_jsx("div", { className: "border-b border-border px-1.5 py-1.5", children: commands.map((command, index) => (_jsxs("button", { "data-active": index === menuIndex, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted", onMouseEnter: () => setMenuIndex(index), onMouseDown: (e) => e.preventDefault(), onClick: () => {
|
|
791
|
+
setMenuIndex(index);
|
|
792
|
+
if (command.skill) {
|
|
793
|
+
chooseSkill(command.name);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
setText(`/${command.name}`);
|
|
797
|
+
inputRef.current?.focus();
|
|
798
|
+
}, children: [_jsx(Icon, { icon: command.skill ? Zap : SquareSlash, size: 12, className: clsx('shrink-0', command.skill ? 'text-skill' : 'text-text-faint') }), _jsxs("span", { className: "font-mono text-text", children: ["/", command.name] }), _jsx("span", { className: "text-text-faint", children: command.hint })] }, command.name))) })), skillMenuOpen && (_jsx("div", { className: "border-b border-border px-1.5 py-1.5", children: skillMatches.map((skill, index) => (_jsxs("button", { "data-active": index === menuIndex, className: "cursor-row flex w-full items-start gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted", onMouseEnter: () => setMenuIndex(index), onMouseDown: (e) => e.preventDefault(), onClick: () => chooseSkill(skill.name), children: [_jsx(Icon, { icon: Zap, size: 12, className: "mt-0.5 shrink-0 text-skill" }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsxs("span", { className: "truncate font-mono text-text", children: ["$", skill.name] }), skill.description !== '' && _jsx("span", { className: "line-clamp-1 text-text-faint", children: skill.description })] })] }, skill.name))) })), mentionMenuOpen && (_jsxs("div", { className: "border-b border-border px-1.5 py-1.5", children: [chatMatches.length > 0 && _jsx("div", { className: MENU_LABEL, children: t('composer.mentions.chats') }), chatMatches.map((chat, index) => (_jsxs("button", { "data-active": index === menuIndex, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted", onMouseEnter: () => setMenuIndex(index), onMouseDown: (e) => e.preventDefault(), onClick: () => chooseChat(chat.id), children: [_jsx(Icon, { icon: MessageSquare, size: 14, className: "shrink-0 text-text-faint" }), _jsx("span", { className: "truncate text-text", children: chat.title || t('chatReference.untitled') })] }, chat.id))), files.length > 0 && (mention.query === '' || chatMatches.length > 0) && (_jsx("div", { className: MENU_LABEL, children: mention.query === '' ? t('composer.mentions.here') : t('composer.mentions.files') })), mentionCount === 0 && (_jsx("div", { className: "px-2 py-1 text-xs text-text-faint", children: mention.query ? t('composer.mentions.noMatch') : t('composer.mentions.empty') })), files.map((path, index) => {
|
|
799
|
+
const { name, dir } = splitPath(path);
|
|
800
|
+
return (_jsxs("button", { "data-active": chatMatches.length + index === menuIndex, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs text-text-muted", onMouseEnter: () => setMenuIndex(chatMatches.length + index), onMouseDown: (e) => e.preventDefault(), onClick: () => chooseMention(path), children: [_jsx(FileIcon, { path: path, size: 14 }), _jsx("span", { className: "truncate font-mono text-text", children: name }), dir && _jsx("span", { className: "min-w-0 truncate text-text-faint", children: dir })] }, path));
|
|
801
|
+
})] })), queue.length > 0 && (_jsx("div", { className: "flex flex-col gap-1 border-b border-border px-2 py-1.5", children: queue.map((message) => {
|
|
802
|
+
const sendNow = () => void actions.sendNow(chatId, message.id).catch(() => undefined);
|
|
803
|
+
const unqueue = () => void actions.unqueue(chatId, message.id).catch(() => undefined);
|
|
804
|
+
const edit = () => void takeBack(message);
|
|
805
|
+
return (_jsxs(ContextMenu.Root, { children: [_jsxs(ContextMenu.Trigger, { className: "group/queued flex items-center gap-2 rounded-md px-1.5 py-1 text-xs text-text-muted", children: [_jsx(Icon, { icon: Clock, size: 12, className: "shrink-0 text-text-faint" }), _jsx("span", { className: "min-w-0 grow truncate", children: message.text || t('composer.queue.attachments', { count: message.attachments?.length ?? 0 }) }), _jsxs("span", { className: `${BTN_GROUP} opacity-0 transition-opacity group-hover/queued:opacity-100 focus-within:opacity-100`, children: [_jsx(Tooltip, { label: t('composer.queue.sendNow'), name: true, children: _jsx("button", { className: "icon-btn icon-btn-2xs", onClick: sendNow, children: _jsx(Icon, { icon: FastForward, size: 12 }) }) }), _jsx(Tooltip, { label: t('common.action.edit'), name: true, children: _jsx("button", { className: "icon-btn icon-btn-2xs", disabled: takingBack !== null, onClick: edit, children: _jsx(Icon, { icon: Pencil, size: 12 }) }) }), _jsx(Tooltip, { label: t('common.action.remove'), name: true, children: _jsx("button", { className: "icon-btn icon-btn-2xs", onClick: unqueue, children: _jsx(Icon, { icon: X, size: 12 }) }) })] })] }), _jsx(ContextMenu.Portal, { children: _jsx(ContextMenu.Positioner, { className: "z-(--z-popup)", children: _jsxs(ContextMenu.Popup, { className: "menu-popup", children: [_jsxs(ContextMenu.Item, { className: "menu-item", onClick: sendNow, children: [_jsx(Icon, { icon: FastForward, size: 14 }), " ", t('composer.queue.sendNow')] }), _jsxs(ContextMenu.Item, { className: "menu-item", disabled: takingBack !== null, onClick: edit, children: [_jsx(Icon, { icon: Pencil, size: 14 }), " ", t('common.action.edit')] }), _jsxs(ContextMenu.Item, { className: "menu-item", disabled: message.text === '', onClick: () => copyText(message.text), children: [_jsx(Icon, { icon: Copy, size: 14 }), " ", t('common.action.copy')] }), _jsx(ContextMenu.Separator, { className: MENU_SEPARATOR }), _jsxs(ContextMenu.Item, { className: "menu-item text-status-error", onClick: unqueue, children: [_jsx(Icon, { icon: X, size: 14 }), " ", t('common.action.remove')] })] }) }) })] }, message.id));
|
|
806
|
+
}) })), quote !== '' && (_jsxs("div", { className: "flex items-center gap-2 px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3", children: [_jsx("span", { className: "min-w-0 grow truncate border-l-2 border-border-strong pl-2.5 text-xs text-text-muted", children: quote }), _jsx(Tooltip, { label: t('composer.quote.remove'), name: true, children: _jsx("button", { className: "icon-btn icon-btn-2xs", onClick: removeQuote, children: _jsx(Icon, { icon: X, size: 12 }) }) })] })), draft.chats.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1.5 px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3", children: draft.chats.map((id) => (_jsx(ChatReferenceChip, { chatId: id, onRemove: () => removeChat(id) }, id))) })), draft.attachments.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 px-5 pt-4 @max-md/composer:px-3.5 @max-md/composer:pt-3", children: draft.attachments.map((attachment, index) => (_jsxs("div", { className: "group/thumb relative", children: [isImageAttachment(attachment.mime) ? (_jsx(UploadThumb, { upload: attachment })) : (_jsxs("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", children: [_jsxs("span", { className: "flex items-center gap-1.5 text-xs text-text", children: [_jsx(Icon, { icon: Paperclip, size: 12, className: "shrink-0 text-text-faint" }), _jsx("span", { className: "truncate", children: attachment.name })] }), _jsx("span", { className: "pl-5 text-xs text-text-faint", children: formatBytes(uploadBytes(attachment)) })] })), _jsx(Tooltip, { label: t('composer.removeAttachment', { name: attachment.name }), children: _jsx("button", { className: "icon-btn icon-btn-2xs 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", onClick: () => removeAttachment(index), children: _jsx(Icon, { icon: X, size: 12 }) }) })] }, `${attachment.name}-${index}`))) })), _jsx(ComposerInput, { ref: inputRef, dictationToolbar: dictationToolbar, className: "composer-input select-text text-sm leading-normal text-text", value: text, placeholder: placeholder, disabled: disabled, tabbable: focused, extensions: editorExtensions, onChange: (value, selection, state) => {
|
|
807
|
+
setText(value);
|
|
808
|
+
setMenuIndex(0);
|
|
809
|
+
trackTriggers(value, selection, state);
|
|
810
|
+
if (historyIndex !== null && value !== history[historyIndex]?.text) {
|
|
811
|
+
setHistoryIndex(null);
|
|
812
|
+
}
|
|
813
|
+
}, onSelectionChange: trackTriggers, onBlur: () => {
|
|
814
|
+
setMention(null);
|
|
815
|
+
setSkillQuery(null);
|
|
816
|
+
}, onKeyDown: onKeyDown, onPaste: onPaste }), notice && (_jsx("div", { className: "px-5 pb-1 text-xs text-status-error @max-md/composer:px-3.5", role: "alert", children: notice })), guard.visible && (_jsxs("div", { className: clsx('px-5 pb-1 text-right text-xs tabular-nums @max-md/composer:px-3.5', guard.tooLong ? 'text-status-error' : 'text-text-faint'), children: [formatNumber(guard.count), " / ", formatNumber(PROMPT_MAX_CHARS), guard.tooLong && ` ${t('composer.tooLong')}`] })), _jsxs("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", children: [attachable && (_jsx(Tooltip, { label: t('composer.attach'), name: true, children: _jsx("button", { type: "button", 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", disabled: disabled, onClick: () => fileInputRef.current?.click(), children: _jsx(Icon, { icon: Plus, size: 16 }) }) })), _jsx("input", { ref: fileInputRef, type: "file", multiple: true, hidden: true, onChange: (e) => {
|
|
817
|
+
addFiles(Array.from(e.target.files ?? []));
|
|
818
|
+
e.target.value = '';
|
|
819
|
+
inputRef.current?.focus();
|
|
820
|
+
} }), _jsx(RunSettings, { providers: pickable, provider: info.provider, selection: info.selection, model: model, runtimeMode: info.runtimeMode, open: modelPickerOpen, onOpenChange: setModelPickerOpen, onModel: chooseModel, onOption: (id, value) => configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } }), onMode: (runtimeMode) => configure({ runtimeMode }), usage: info.usage, compactDisabled: busy || disabled, onCompact: compact, account: accountChoice === null ? null : { choice: accountChoice, started, onPick: chooseAccount } }), _jsx(StashPicker, { onRestore: restoreStashed }), _jsx("span", { className: "grow" }), _jsxs("div", { className: "flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover", children: [_jsx("div", { ref: setDictationToolbar, className: "flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" }), busy && (_jsx(Tooltip, { label: composerStopLabel(), name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text", onClick: (event) => stop(event.shiftKey), children: _jsx(Icon, { icon: Square, size: 16 }) }) })), (!busy || !isEmptyDraft(draft)) && (_jsx(Tooltip, { label: busy ? t('composer.queueButton') : t('composer.send'), kbd: KEY_SHORTCUTS.modEnter, name: true, children: _jsx("button", { 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", disabled: isEmptyDraft(draft) || disabled || guard.tooLong, onClick: submit, children: _jsx(Icon, { icon: ArrowUp, size: 16 }) }) }))] })] })] }) }), _jsx(PromptDialog, { open: confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: forceClearThread, onClose: () => setConfirmClear(false) })] }));
|
|
821
|
+
}
|