@adecore/agents-react 0.38.0 → 0.40.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/dist/agents/AgentModelLabel.d.ts +10 -0
- package/dist/agents/AgentModelLabel.js +98 -0
- package/dist/agents/model-name.js +2 -1
- package/dist/agents/provider-paths.d.ts +2 -1
- package/dist/agents/provider-paths.js +7 -3
- package/dist/chat/account-choice.js +57 -28
- package/dist/chat/bookmarks.js +2 -1
- package/dist/chat/drafts.d.ts +0 -1
- package/dist/chat/drafts.js +9 -28
- package/dist/chat/logic/picker-entries.js +2 -8
- package/dist/chat/logic/resume-compaction.js +2 -5
- package/dist/chat/logic/timeline-copy.js +2 -8
- package/dist/chat/logic/timeline.js +9 -10
- package/dist/chat/logic/workflow.js +2 -4
- package/dist/chat/subagent-conversation.js +2 -1
- package/dist/chat/ui/Composer.js +385 -360
- package/dist/chat/ui/GoalDock.js +2 -1
- package/dist/chat/ui/LimitState.js +4 -6
- package/dist/chat/ui/Pickers.d.ts +2 -1
- package/dist/chat/ui/Pickers.js +126 -123
- package/dist/chat/ui/PromptEditor.d.ts +19 -0
- package/dist/chat/ui/PromptEditor.js +115 -0
- package/dist/chat/ui/Timeline.d.ts +2 -1
- package/dist/chat/ui/Timeline.js +13 -1
- package/dist/chat/ui/composer/ComposerControls.d.ts +3 -0
- package/dist/chat/ui/composer/ComposerControls.js +235 -38
- package/dist/chat/ui/composer/key-handler.d.ts +1 -1
- package/dist/chat/ui/composer/key-handler.js +3 -11
- package/dist/chat/ui/composer/prompt-picks.d.ts +4 -0
- package/dist/chat/ui/composer/prompt-picks.js +14 -0
- package/dist/chat/ui/composer/use-composer-menus.d.ts +4 -49
- package/dist/chat/ui/composer/use-composer-menus.js +65 -519
- package/dist/chat/ui/composer/use-continuable-turn.js +0 -1
- package/dist/chat/ui/composer/use-notice.js +2 -1
- package/dist/chat/ui/composer/use-prompt-history.js +1 -1
- package/dist/chat/ui/composer/use-prompt-menus.d.ts +73 -0
- package/dist/chat/ui/composer/use-prompt-menus.js +499 -0
- package/dist/chat/ui/composer/use-take-back.js +2 -2
- package/dist/chat/ui/diff/ApprovalReview.js +1 -1
- package/dist/chat/ui/highlighter.d.ts +0 -6
- package/dist/chat/ui/highlighter.js +0 -12
- package/dist/chat/ui/icons.d.ts +0 -1
- package/dist/chat/ui/icons.js +0 -3
- package/dist/chat/ui/index.d.ts +3 -0
- package/dist/chat/ui/index.js +2 -0
- package/dist/chat/ui/rows/MessageRows.js +2 -5
- package/dist/chat/ui/rows/row-rhythm.js +2 -5
- package/dist/chat/ui/theme-watch.js +3 -7
- package/dist/host-hooks.d.ts +2 -1
- package/dist/host-hooks.js +6 -0
- package/dist/host.d.ts +17 -1
- package/dist/index.d.ts +1 -1
- package/dist/locales/en/agent-chat.d.ts +1 -0
- package/dist/locales/en/agent-chat.js +1 -0
- package/dist/locales/nl/agent-chat.d.ts +1 -0
- package/dist/locales/nl/agent-chat.js +1 -0
- package/dist/prompts/logic/usePromptSession.js +2 -1
- package/dist/providers/AddAccountForm.js +2 -1
- package/dist/providers/CliDetail.js +104 -130
- package/dist/providers/account-actions.js +6 -9
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/state/chats.d.ts +1 -0
- package/dist/state/chats.js +6 -11
- package/dist/state/provider-accounts.d.ts +1 -1
- package/dist/state/provider-accounts.js +2 -8
- package/dist/theme.css +3 -0
- package/dist/transport.d.ts +2 -0
- package/dist/transport.js +14 -0
- package/dist/usage/ChartLogo.d.ts +5 -0
- package/dist/usage/ChartLogo.js +48 -0
- package/dist/usage/LimitsList.js +6 -16
- package/dist/usage/UsageBreakdown.js +17 -40
- package/dist/usage/UsageLimits.js +183 -211
- package/dist/usage/UsagePage.js +3 -17
- package/dist/usage/UsageSummary.js +4 -9
- package/dist/usage/limits.js +16 -31
- package/dist/usage/summary.d.ts +1 -1
- package/dist/usage/summary.js +3 -5
- package/package.json +26 -26
- package/src/agents/AccountDot.tsx +1 -0
- package/src/agents/AgentIcon.tsx +2 -0
- package/src/agents/AgentModelLabel.tsx +38 -0
- package/src/agents/ModelOptionControl.tsx +8 -0
- package/src/agents/account-limits.ts +3 -0
- package/src/agents/accounts.ts +12 -0
- package/src/agents/model-name.ts +13 -1
- package/src/agents/model-options.ts +2 -0
- package/src/agents/model-requirement.ts +4 -0
- package/src/agents/option-words.ts +2 -0
- package/src/agents/provider-paths.ts +8 -3
- package/src/agents/status-look.ts +1 -0
- package/src/chat/account-choice.ts +25 -19
- package/src/chat/actions.ts +5 -0
- package/src/chat/attachments.ts +8 -0
- package/src/chat/bookmarks.ts +13 -1
- package/src/chat/chat-client.ts +63 -0
- package/src/chat/chat-references.ts +3 -0
- package/src/chat/composer-effort.ts +5 -0
- package/src/chat/composer-send.ts +4 -0
- package/src/chat/drafts.ts +21 -17
- package/src/chat/forks.ts +4 -0
- package/src/chat/guards.ts +2 -0
- package/src/chat/index.ts +7 -0
- package/src/chat/logic/ansi.ts +47 -0
- package/src/chat/logic/bookmarks.ts +7 -0
- package/src/chat/logic/command-looks.ts +29 -0
- package/src/chat/logic/context-usage.ts +3 -0
- package/src/chat/logic/continue-after-stop.ts +5 -0
- package/src/chat/logic/effort.ts +4 -0
- package/src/chat/logic/embedded-code.ts +63 -0
- package/src/chat/logic/fork.ts +22 -0
- package/src/chat/logic/generated-image.ts +7 -0
- package/src/chat/logic/goal.ts +7 -0
- package/src/chat/logic/handback.ts +2 -0
- package/src/chat/logic/limit.ts +8 -0
- package/src/chat/logic/picker-entries.ts +16 -4
- package/src/chat/logic/resume-compaction.ts +22 -5
- package/src/chat/logic/scrubber.ts +38 -5
- package/src/chat/logic/thought.ts +5 -0
- package/src/chat/logic/thread-cards.ts +6 -0
- package/src/chat/logic/timeline-copy.ts +4 -9
- package/src/chat/logic/timeline-target.ts +15 -0
- package/src/chat/logic/timeline.ts +113 -10
- package/src/chat/logic/tools.ts +30 -0
- package/src/chat/logic/visual-height.ts +5 -0
- package/src/chat/logic/visual-viewport.ts +1 -0
- package/src/chat/logic/welcome.ts +3 -0
- package/src/chat/logic/workflow.ts +26 -7
- package/src/chat/mentions.ts +56 -0
- package/src/chat/pending-prompts.ts +16 -0
- package/src/chat/persisted-json.ts +15 -0
- package/src/chat/preferences.ts +16 -0
- package/src/chat/prompt-hold.ts +2 -0
- package/src/chat/quote.ts +7 -0
- package/src/chat/reading-width.ts +1 -0
- package/src/chat/recent-messages.ts +5 -0
- package/src/chat/resume-compaction-dismissals.ts +1 -0
- package/src/chat/stash.ts +12 -0
- package/src/chat/subagent-conversation.ts +24 -4
- package/src/chat/subagent-list.ts +14 -0
- package/src/chat/subagent-support.ts +1 -0
- package/src/chat/subagent-view.ts +9 -0
- package/src/chat/thumbnails.ts +14 -0
- package/src/chat/timeline-flash.ts +2 -0
- package/src/chat/timeline-scroll.ts +23 -0
- package/src/chat/ui/AccountPill.tsx +4 -0
- package/src/chat/ui/AnsiOutput.tsx +1 -0
- package/src/chat/ui/BookmarkMarker.tsx +5 -0
- package/src/chat/ui/BookmarkSubmenu.tsx +1 -0
- package/src/chat/ui/ChatActivity.tsx +13 -0
- package/src/chat/ui/CodeBlock.tsx +8 -0
- package/src/chat/ui/Composer.tsx +27 -3
- package/src/chat/ui/ComposerInput.tsx +22 -0
- package/src/chat/ui/DiffPool.tsx +3 -0
- package/src/chat/ui/EditDiff.tsx +5 -0
- package/src/chat/ui/GoalDock.tsx +13 -1
- package/src/chat/ui/ImageView.tsx +11 -0
- package/src/chat/ui/LimitState.tsx +23 -6
- package/src/chat/ui/Markdown.tsx +41 -0
- package/src/chat/ui/MermaidBlock.tsx +16 -0
- package/src/chat/ui/MessageActions.tsx +8 -0
- package/src/chat/ui/ModelMarks.tsx +1 -0
- package/src/chat/ui/Pickers.tsx +30 -1
- package/src/chat/ui/PromptComposer.tsx +2 -0
- package/src/chat/ui/PromptEditor.tsx +136 -0
- package/src/chat/ui/QuoteButton.tsx +12 -0
- package/src/chat/ui/ReferenceChip.tsx +3 -0
- package/src/chat/ui/ResumeCompactionDock.tsx +1 -0
- package/src/chat/ui/RowTime.tsx +1 -0
- package/src/chat/ui/RunSettings.tsx +38 -0
- package/src/chat/ui/Scrubber.tsx +14 -0
- package/src/chat/ui/SessionLine.tsx +1 -0
- package/src/chat/ui/ShellCommand.tsx +32 -0
- package/src/chat/ui/SubagentControls.tsx +7 -0
- package/src/chat/ui/SubagentInfo.tsx +1 -0
- package/src/chat/ui/SubagentStopButton.tsx +6 -0
- package/src/chat/ui/SubagentTimeline.tsx +10 -0
- package/src/chat/ui/Timeline.tsx +141 -1
- package/src/chat/ui/TimelineMenu.tsx +5 -0
- package/src/chat/ui/UnifiedDiff.tsx +6 -0
- package/src/chat/ui/UploadThumb.tsx +2 -0
- package/src/chat/ui/VisualDialogs.tsx +3 -0
- package/src/chat/ui/VisualFrame.tsx +15 -0
- package/src/chat/ui/Welcome.tsx +1 -0
- package/src/chat/ui/chip-clipboard.ts +20 -0
- package/src/chat/ui/code-lines.ts +14 -0
- package/src/chat/ui/code-theme.ts +1 -0
- package/src/chat/ui/composer/ComposerControls.tsx +70 -15
- package/src/chat/ui/composer/ComposerMenu.tsx +12 -0
- package/src/chat/ui/composer/DraftParts.tsx +2 -0
- package/src/chat/ui/composer/QueuedMessages.tsx +4 -0
- package/src/chat/ui/composer/chip-paste.ts +16 -0
- package/src/chat/ui/composer/chips.ts +6 -0
- package/src/chat/ui/composer/editor.ts +9 -0
- package/src/chat/ui/composer/insert.ts +2 -0
- package/src/chat/ui/composer/key-handler.ts +42 -9
- package/src/chat/ui/composer/keys.ts +23 -0
- package/src/chat/ui/composer/placeholder.ts +6 -0
- package/src/chat/ui/composer/prompt-picks.ts +18 -0
- package/src/chat/ui/composer/surface.ts +5 -0
- package/src/chat/ui/composer/use-chat-controls.ts +13 -0
- package/src/chat/ui/composer/use-compaction-offer.ts +3 -0
- package/src/chat/ui/composer/use-composer-menus.ts +20 -275
- package/src/chat/ui/composer/use-composer-submit.ts +27 -0
- package/src/chat/ui/composer/use-continuable-turn.ts +3 -5
- package/src/chat/ui/composer/use-draft-files.ts +24 -0
- package/src/chat/ui/composer/use-effort-stepper.ts +8 -0
- package/src/chat/ui/composer/use-notice.ts +4 -1
- package/src/chat/ui/composer/use-pending-requests.ts +3 -0
- package/src/chat/ui/composer/use-prompt-history.ts +20 -11
- package/src/chat/ui/composer/use-prompt-hold.ts +3 -0
- package/src/chat/ui/composer/use-prompt-menus.ts +361 -0
- package/src/chat/ui/composer/use-take-back.ts +20 -2
- package/src/chat/ui/diff/ApprovalReview.tsx +18 -15
- package/src/chat/ui/diff/DiffFallbackBoundary.tsx +1 -0
- package/src/chat/ui/diff/DiffOmitted.tsx +1 -0
- package/src/chat/ui/diff/DiffStack.tsx +2 -0
- package/src/chat/ui/diff/DiffUnavailable.tsx +1 -0
- package/src/chat/ui/diff/EditorEditDiff.tsx +13 -0
- package/src/chat/ui/diff/EditorPatchDiff.tsx +4 -0
- package/src/chat/ui/diff/LegacyDiffPool.tsx +2 -0
- package/src/chat/ui/diff/LegacyEditDiff.tsx +2 -0
- package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +8 -0
- package/src/chat/ui/diff/RawDiff.tsx +1 -0
- package/src/chat/ui/diff/ReviewDiff.tsx +1 -0
- package/src/chat/ui/diff/diff-source-budget.ts +8 -0
- package/src/chat/ui/diff/editor-edit-model.ts +4 -0
- package/src/chat/ui/diff/review-plan.ts +9 -0
- package/src/chat/ui/diff/review-settle.ts +1 -0
- package/src/chat/ui/diff/syntax-runtime.ts +52 -0
- package/src/chat/ui/diff/use-review-stack.ts +9 -0
- package/src/chat/ui/diff-syntax.ts +12 -0
- package/src/chat/ui/diff-theme.ts +2 -0
- package/src/chat/ui/file-links.ts +2 -0
- package/src/chat/ui/find-reveal.ts +1 -0
- package/src/chat/ui/full-diff.ts +1 -0
- package/src/chat/ui/highlighter.ts +29 -15
- package/src/chat/ui/icons.tsx +2 -4
- package/src/chat/ui/index.ts +7 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +25 -0
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +1 -0
- package/src/chat/ui/intelligent-ui/UiReply.tsx +63 -0
- package/src/chat/ui/intelligent-ui/block-local.ts +5 -0
- package/src/chat/ui/intelligent-ui/chart-data.ts +15 -0
- package/src/chat/ui/intelligent-ui/node-text.ts +17 -0
- package/src/chat/ui/intelligent-ui/renderers/binding.ts +1 -0
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +31 -0
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +4 -0
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +18 -0
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +11 -0
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +13 -0
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +9 -0
- package/src/chat/ui/intelligent-ui/renderers/map-slots.ts +15 -0
- package/src/chat/ui/intelligent-ui/renderers/map-style.ts +15 -0
- package/src/chat/ui/intelligent-ui/renderers/map-view.tsx +48 -0
- package/src/chat/ui/intelligent-ui/renderers/map.tsx +20 -0
- package/src/chat/ui/intelligent-ui/renderers/place-point.ts +3 -0
- package/src/chat/ui/intelligent-ui/renderers/structure.tsx +3 -0
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +8 -0
- package/src/chat/ui/intelligent-ui/reply-parts.ts +5 -0
- package/src/chat/ui/intelligent-ui/table-data.ts +17 -0
- package/src/chat/ui/intelligent-ui/use-in-view.ts +4 -0
- package/src/chat/ui/intelligent-ui/use-later.ts +4 -0
- package/src/chat/ui/intelligent-ui/use-truncated.ts +5 -0
- package/src/chat/ui/intelligent-ui/use-ui-links.ts +29 -0
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +60 -0
- package/src/chat/ui/markdown-blocks.ts +64 -0
- package/src/chat/ui/mermaid.ts +4 -0
- package/src/chat/ui/quote-selection.ts +10 -0
- package/src/chat/ui/rehype-chips.ts +5 -0
- package/src/chat/ui/rehype-fade.ts +2 -0
- package/src/chat/ui/remark-html-as-text.ts +5 -0
- package/src/chat/ui/reveal.ts +18 -0
- package/src/chat/ui/rows/ForksRow.tsx +5 -0
- package/src/chat/ui/rows/GeneratedImageRow.tsx +10 -0
- package/src/chat/ui/rows/MessageRows.tsx +29 -5
- package/src/chat/ui/rows/RowContainer.tsx +1 -0
- package/src/chat/ui/rows/Rows.tsx +2 -0
- package/src/chat/ui/rows/SubagentRow.tsx +13 -0
- package/src/chat/ui/rows/TaskRow.tsx +7 -0
- package/src/chat/ui/rows/VisualRow.tsx +4 -0
- package/src/chat/ui/rows/WorkRows.tsx +46 -0
- package/src/chat/ui/rows/WorkflowRow.tsx +10 -0
- package/src/chat/ui/rows/row-rhythm.ts +7 -5
- package/src/chat/ui/theme-watch.ts +8 -7
- package/src/chat/ui/thread-references.ts +7 -0
- package/src/chat/ui/use-diff-source-tokens.ts +8 -0
- package/src/chat/ui/useToggleSet.ts +3 -0
- package/src/chat/ui/visual-theme.ts +30 -0
- package/src/chat/visual-bridge.ts +23 -0
- package/src/chat/visual-viewport.ts +12 -0
- package/src/host-hooks.ts +15 -5
- package/src/host.ts +29 -1
- package/src/index.ts +5 -0
- package/src/lazy.tsx +9 -0
- package/src/locales/en/agent-chat.ts +1 -0
- package/src/locales/nl/agent-chat.ts +1 -0
- package/src/mounted-registry.ts +4 -0
- package/src/port-transport.ts +32 -0
- package/src/prompts/index.ts +2 -0
- package/src/prompts/logic/focus.ts +5 -0
- package/src/prompts/logic/keys.ts +12 -0
- package/src/prompts/logic/prompts.ts +8 -0
- package/src/prompts/logic/subjects.ts +8 -0
- package/src/prompts/logic/useFocusAfterAnswer.ts +2 -0
- package/src/prompts/logic/usePromptSession.ts +13 -1
- package/src/prompts/ui/ApprovalBody.tsx +2 -0
- package/src/prompts/ui/PromptActions.tsx +2 -0
- package/src/prompts/ui/PromptCard.tsx +6 -0
- package/src/prompts/ui/PromptView.tsx +6 -0
- package/src/prompts/ui/QuestionBody.tsx +11 -0
- package/src/providers/AccountColors.tsx +1 -0
- package/src/providers/AccountDetail.tsx +7 -0
- package/src/providers/AccountVariables.tsx +2 -0
- package/src/providers/AddAccountForm.tsx +5 -1
- package/src/providers/CliDetail.tsx +6 -5
- package/src/providers/InstallPage.tsx +11 -0
- package/src/providers/ProviderList.tsx +3 -0
- package/src/providers/ProvidersPane.tsx +18 -0
- package/src/providers/account-actions.ts +10 -8
- package/src/providers/index.ts +1 -0
- package/src/providers/install-steps.ts +3 -0
- package/src/providers/pane-target.ts +5 -0
- package/src/providers/provider-abilities.ts +1 -0
- package/src/providers/provider-groups.ts +6 -0
- package/src/providers/variables.ts +12 -0
- package/src/scope.ts +2 -0
- package/src/state/chats.ts +61 -14
- package/src/state/index.ts +2 -0
- package/src/state/provider-accounts.ts +7 -9
- package/src/state/providers.ts +1 -0
- package/src/state/usage.ts +6 -0
- package/src/storage.ts +18 -0
- package/src/testing/prompts.ts +5 -0
- package/src/theme.css +3 -0
- package/src/transport.ts +18 -0
- package/src/usage/ChartLogo.tsx +12 -0
- package/src/usage/LimitsList.tsx +7 -8
- package/src/usage/UsageBreakdown.tsx +11 -4
- package/src/usage/UsageChart.tsx +5 -0
- package/src/usage/UsageLimits.tsx +10 -7
- package/src/usage/UsageLimitsCard.tsx +2 -0
- package/src/usage/UsagePage.tsx +16 -19
- package/src/usage/UsageSummary.tsx +4 -5
- package/src/usage/UsageTiles.tsx +2 -0
- package/src/usage/format.ts +7 -0
- package/src/usage/limit-groups.ts +14 -0
- package/src/usage/limits.ts +11 -13
- package/src/usage/money.ts +1 -0
- package/src/usage/summary.ts +15 -7
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef, type SetStateAction } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Surface } from '@adecore/ui';
|
|
4
|
+
import type { ChatSkill } from '@adecore/agent-contracts';
|
|
5
|
+
import type { Referables } from '../chat-references';
|
|
6
|
+
import { ComposerInput, type ComposerInputHandle } from './ComposerInput';
|
|
7
|
+
import { chipDecorations } from './composer/chips';
|
|
8
|
+
import { ComposerMenu } from './composer/ComposerMenu';
|
|
9
|
+
import { pickInMenu } from './composer/key-handler';
|
|
10
|
+
import { usePromptMenus, type PromptDraft } from './composer/use-prompt-menus';
|
|
11
|
+
|
|
12
|
+
interface PromptEditorProps {
|
|
13
|
+
value: PromptDraft;
|
|
14
|
+
onChange(next: PromptDraft): void;
|
|
15
|
+
/* The editor's accessible name; a label element cannot point at the editable element. */
|
|
16
|
+
label: string;
|
|
17
|
+
placeholder: string;
|
|
18
|
+
skills: readonly ChatSkill[];
|
|
19
|
+
/* What the CLI answers after `/`, besides `/goal` where `goals` is set. */
|
|
20
|
+
slashCommands?: readonly string[];
|
|
21
|
+
goals?: boolean;
|
|
22
|
+
/* Where `@` searches for files; null offers none. */
|
|
23
|
+
cwd: string | null;
|
|
24
|
+
referables: Referables;
|
|
25
|
+
providerName?: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NO_COMMANDS: readonly string[] = [];
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* A prompt written now and sent later, as a form field: the composer's editor with its pickers and chips,
|
|
34
|
+
* without a chat behind it. The host hands in what the CLI it goes to has, and keeps what was picked.
|
|
35
|
+
*/
|
|
36
|
+
export function PromptEditor({
|
|
37
|
+
value,
|
|
38
|
+
onChange,
|
|
39
|
+
label,
|
|
40
|
+
placeholder,
|
|
41
|
+
skills,
|
|
42
|
+
slashCommands = NO_COMMANDS,
|
|
43
|
+
goals = false,
|
|
44
|
+
cwd,
|
|
45
|
+
referables,
|
|
46
|
+
providerName,
|
|
47
|
+
disabled = false,
|
|
48
|
+
className
|
|
49
|
+
}: PromptEditorProps) {
|
|
50
|
+
const inputRef = useRef<ComposerInputHandle>(null);
|
|
51
|
+
// A pick writes twice in one event (the text, then what it picked), and the second must build on the first.
|
|
52
|
+
const latest = useRef(value);
|
|
53
|
+
|
|
54
|
+
useLayoutEffect(() => {
|
|
55
|
+
latest.current = value;
|
|
56
|
+
}, [value]);
|
|
57
|
+
|
|
58
|
+
const setDraft = (action: SetStateAction<PromptDraft>): void => {
|
|
59
|
+
const next = typeof action === 'function' ? action(latest.current) : action;
|
|
60
|
+
latest.current = next;
|
|
61
|
+
onChange(next);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const setText = (next: string, mentions = value.mentions, chosen = value.skills): void => {
|
|
65
|
+
setDraft((current) => ({ ...current, text: next, mentions, skills: chosen }));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const menus = usePromptMenus({
|
|
69
|
+
draft: { draft: value, setDraft, setText },
|
|
70
|
+
inputRef,
|
|
71
|
+
cwd,
|
|
72
|
+
skills,
|
|
73
|
+
slashCommands,
|
|
74
|
+
goals,
|
|
75
|
+
providerName,
|
|
76
|
+
referables,
|
|
77
|
+
label
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const extensions = chipDecorations({ mentions: value.mentions, skills: value.skills, references: menus.referenceChips });
|
|
81
|
+
|
|
82
|
+
const onKeyDown = (e: KeyboardEvent): boolean => {
|
|
83
|
+
if (e.isComposing) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Escape closes the picker, and only that: the dialog around the field stays.
|
|
88
|
+
if (e.key === 'Escape' && menus.menuOpen) {
|
|
89
|
+
menus.close();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Enter adds a line here rather than sending, so on a command it completes like Tab.
|
|
96
|
+
if (e.key === 'Enter' && menus.commandMenuOpen) {
|
|
97
|
+
menus.completeCommand(menus.commands[menus.activeIndex]!);
|
|
98
|
+
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return pickInMenu(e, menus);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div className={clsx('relative', className)}>
|
|
107
|
+
{/* Over the form under the field, not inside it, so what a person types never moves while the picker opens and closes. */}
|
|
108
|
+
{menus.menuOpen && (
|
|
109
|
+
<Surface className="absolute inset-x-0 top-full z-50 mt-1 overflow-hidden rounded-lg bg-clip-padding *:border-b-0">
|
|
110
|
+
<ComposerMenu menus={menus} />
|
|
111
|
+
</Surface>
|
|
112
|
+
)}
|
|
113
|
+
<div className="field h-auto overflow-hidden p-0 focus-ring-within">
|
|
114
|
+
<ComposerInput
|
|
115
|
+
ref={inputRef}
|
|
116
|
+
dictationToolbar={null}
|
|
117
|
+
className="composer-input prompt-editor select-text text-sm leading-normal text-text"
|
|
118
|
+
value={value.text}
|
|
119
|
+
placeholder={placeholder}
|
|
120
|
+
disabled={disabled}
|
|
121
|
+
tabbable
|
|
122
|
+
extensions={extensions}
|
|
123
|
+
aria={menus.inputAria}
|
|
124
|
+
onChange={(text, selection, state) => {
|
|
125
|
+
setText(text);
|
|
126
|
+
menus.setMenuIndex(0);
|
|
127
|
+
menus.trackTriggers(text, selection, state);
|
|
128
|
+
}}
|
|
129
|
+
onSelectionChange={menus.trackTriggers}
|
|
130
|
+
onBlur={menus.close}
|
|
131
|
+
onKeyDown={onKeyDown}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -18,6 +18,7 @@ interface Offer {
|
|
|
18
18
|
/* The last line the range covers. A triple-click ends on an empty rect at the start of the next row. */
|
|
19
19
|
function lastLineOf(range: Range): DOMRect | null {
|
|
20
20
|
const rects = [...range.getClientRects()].filter((rect) => rect.width > 0);
|
|
21
|
+
|
|
21
22
|
return rects.at(-1) ?? null;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -39,15 +40,19 @@ export function QuoteButton({
|
|
|
39
40
|
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
let pressed = false;
|
|
43
|
+
|
|
42
44
|
const read = (): void => {
|
|
43
45
|
const selection = document.getSelection();
|
|
44
46
|
const quote = taker.current === null ? null : selectedAnswerQuote(selection, thread.current);
|
|
45
47
|
const box = frame.current;
|
|
46
48
|
const line = quote === null || selection === null ? null : lastLineOf(selection.getRangeAt(0));
|
|
49
|
+
|
|
47
50
|
if (quote === null || box === null || line === null) {
|
|
48
51
|
setOffer(null);
|
|
52
|
+
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
55
|
+
|
|
51
56
|
const bounds = box.getBoundingClientRect();
|
|
52
57
|
// The canvas may zoom the cell; the button is placed in the frame's own pixels.
|
|
53
58
|
const zoom = box.offsetWidth > 0 ? bounds.width / box.offsetWidth : 1;
|
|
@@ -55,23 +60,30 @@ export function QuoteButton({
|
|
|
55
60
|
const y = (line.bottom - bounds.top) / zoom + GAP_PX;
|
|
56
61
|
setOffer({ quote: quote.text, itemId: quote.itemId, x: Math.round(x), y: Math.round(y) });
|
|
57
62
|
};
|
|
63
|
+
|
|
58
64
|
const press = (): void => {
|
|
59
65
|
pressed = true;
|
|
60
66
|
};
|
|
67
|
+
|
|
61
68
|
const release = (): void => {
|
|
62
69
|
pressed = false;
|
|
63
70
|
read();
|
|
64
71
|
};
|
|
72
|
+
|
|
65
73
|
const change = (): void => {
|
|
66
74
|
if (pressed) {
|
|
67
75
|
setOffer(null);
|
|
76
|
+
|
|
68
77
|
return;
|
|
69
78
|
}
|
|
79
|
+
|
|
70
80
|
read();
|
|
71
81
|
};
|
|
82
|
+
|
|
72
83
|
document.addEventListener('pointerdown', press, true);
|
|
73
84
|
document.addEventListener('pointerup', release, true);
|
|
74
85
|
document.addEventListener('selectionchange', change);
|
|
86
|
+
|
|
75
87
|
return () => {
|
|
76
88
|
document.removeEventListener('pointerdown', press, true);
|
|
77
89
|
document.removeEventListener('pointerup', release, true);
|
|
@@ -31,6 +31,7 @@ export function TokenChip({ kind, value, sigil, icon, label, tone, onOpen, class
|
|
|
31
31
|
onOpen();
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
+
|
|
34
35
|
return (
|
|
35
36
|
<span
|
|
36
37
|
{...rest}
|
|
@@ -55,11 +56,13 @@ export function ReferenceChip({ id }: { id: string }) {
|
|
|
55
56
|
const { t } = useTranslation('agent-chat');
|
|
56
57
|
const { items, open } = useContext(ReferenceLookupContext);
|
|
57
58
|
const item = items.find((candidate) => candidate.id === id);
|
|
59
|
+
|
|
58
60
|
if (item === undefined) {
|
|
59
61
|
return (
|
|
60
62
|
<TokenChip kind="gone" icon={<Icon icon={CircleOff} size={14} className={TOKEN_ICON} />} label={t('references.gone')} tone={REFERENCE_GONE_TONE} />
|
|
61
63
|
);
|
|
62
64
|
}
|
|
65
|
+
|
|
63
66
|
return (
|
|
64
67
|
<TokenChip
|
|
65
68
|
kind="reference"
|
|
@@ -20,6 +20,7 @@ interface ResumeCompactionDockProps {
|
|
|
20
20
|
export function ResumeCompactionDock({ tokens, reason = 'idle', onCompact, onDismiss }: ResumeCompactionDockProps) {
|
|
21
21
|
const { t } = useTranslation('agent-chat');
|
|
22
22
|
const cold = reason === 'cache-expired';
|
|
23
|
+
|
|
23
24
|
return (
|
|
24
25
|
<div className="flex items-center gap-2 border-b border-border px-3.5 py-2.5">
|
|
25
26
|
<Icon icon={Minimize2} size={16} className="shrink-0 text-text-faint" />
|
package/src/chat/ui/RowTime.tsx
CHANGED
|
@@ -92,9 +92,11 @@ interface AccountPick {
|
|
|
92
92
|
|
|
93
93
|
function optionSummary(option: ModelOptionDescriptor, selection: ModelSelection): string | null {
|
|
94
94
|
const value = selection.options[option.id];
|
|
95
|
+
|
|
95
96
|
if (option.type === 'select') {
|
|
96
97
|
return option.choices.find((choice) => choice.id === (value ?? option.defaultChoice))?.label ?? null;
|
|
97
98
|
}
|
|
99
|
+
|
|
98
100
|
return (value ?? option.defaultValue) === true ? option.label : null;
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -148,16 +150,21 @@ export function RunSettings({
|
|
|
148
150
|
const current = modelName(selection.model, owner?.models);
|
|
149
151
|
const short = shortModelName(current, owner?.models);
|
|
150
152
|
const options = orderOptions((model?.options ?? []).map(localizedOption));
|
|
153
|
+
|
|
151
154
|
const summary = options.flatMap((option) => {
|
|
152
155
|
const label = optionSummary(option, selection);
|
|
156
|
+
|
|
153
157
|
return label === null ? [] : [{ id: option.id, label, ring: option.id === CONTEXT_OPTION }];
|
|
154
158
|
});
|
|
159
|
+
|
|
155
160
|
// A CLI without a context choice still fills one, so the ring stands on its own next to the size.
|
|
156
161
|
const hasContextOption = options.some((option) => option.id === CONTEXT_OPTION);
|
|
157
162
|
const standaloneContext = !hasContextOption && Boolean(usage.contextWindow);
|
|
163
|
+
|
|
158
164
|
if (standaloneContext) {
|
|
159
165
|
summary.push({ id: CONTEXT_OPTION, label: formatTokens(usage.contextWindow ?? 0), ring: true });
|
|
160
166
|
}
|
|
167
|
+
|
|
161
168
|
const grouped = providers.length > 1;
|
|
162
169
|
const live = modelGroupsOf(providers, false);
|
|
163
170
|
const legacy = modelGroupsOf(providers, true);
|
|
@@ -176,7 +183,9 @@ export function RunSettings({
|
|
|
176
183
|
|
|
177
184
|
const searchable = wantsModelSearch(providers);
|
|
178
185
|
const favorites = useChatPreferences((preferences) => preferences.favoriteModels);
|
|
186
|
+
|
|
179
187
|
const favoritesOf = (kind: AgentKind): ReadonlySet<string> => favoriteModelsOf(favorites, kind);
|
|
188
|
+
|
|
180
189
|
const found = query.trim() === '' ? null : searchModels(inPickerOrder(providers), query, favoritesOf);
|
|
181
190
|
|
|
182
191
|
// The menu moves the focus to its popup as it opens, so the search field takes it a frame later.
|
|
@@ -184,7 +193,9 @@ export function RunSettings({
|
|
|
184
193
|
if (!open || !searchable) {
|
|
185
194
|
return;
|
|
186
195
|
}
|
|
196
|
+
|
|
187
197
|
const frame = requestAnimationFrame(() => searchRef.current?.focus());
|
|
198
|
+
|
|
188
199
|
return () => cancelAnimationFrame(frame);
|
|
189
200
|
}, [open, searchable]);
|
|
190
201
|
|
|
@@ -192,6 +203,7 @@ export function RunSettings({
|
|
|
192
203
|
if (!next) {
|
|
193
204
|
setQuery('');
|
|
194
205
|
}
|
|
206
|
+
|
|
195
207
|
onOpenChange(next);
|
|
196
208
|
};
|
|
197
209
|
|
|
@@ -200,10 +212,13 @@ export function RunSettings({
|
|
|
200
212
|
if (!searchable || event.target === searchRef.current || event.metaKey || event.ctrlKey || event.altKey) {
|
|
201
213
|
return;
|
|
202
214
|
}
|
|
215
|
+
|
|
203
216
|
const typed = event.key.length === 1 && event.key !== ' ';
|
|
217
|
+
|
|
204
218
|
if (!typed && event.key !== 'Backspace') {
|
|
205
219
|
return;
|
|
206
220
|
}
|
|
221
|
+
|
|
207
222
|
event.preventDefault();
|
|
208
223
|
event.stopPropagation();
|
|
209
224
|
setQuery((now) => (typed ? now + event.key : now.slice(0, -1)));
|
|
@@ -216,17 +231,22 @@ export function RunSettings({
|
|
|
216
231
|
event.preventDefault();
|
|
217
232
|
event.stopPropagation();
|
|
218
233
|
setQuery('');
|
|
234
|
+
|
|
219
235
|
return;
|
|
220
236
|
}
|
|
237
|
+
|
|
221
238
|
if (event.key === 'Enter') {
|
|
222
239
|
event.preventDefault();
|
|
223
240
|
const first = found?.[0];
|
|
241
|
+
|
|
224
242
|
if (first !== undefined) {
|
|
225
243
|
onModel(first.entry.kind, first.models[0]!.slug);
|
|
226
244
|
changeOpen(false);
|
|
227
245
|
}
|
|
246
|
+
|
|
228
247
|
return;
|
|
229
248
|
}
|
|
249
|
+
|
|
230
250
|
if (event.key !== 'Escape' && event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Tab') {
|
|
231
251
|
event.stopPropagation();
|
|
232
252
|
}
|
|
@@ -235,6 +255,7 @@ export function RunSettings({
|
|
|
235
255
|
// The name wraps rather than truncates, so a long name a provider gives is read whole.
|
|
236
256
|
const modelRow = (entry: ProviderInfo, row: ModelInfo) => {
|
|
237
257
|
const starred = favoritesOf(entry.kind).has(row.slug);
|
|
258
|
+
|
|
238
259
|
return (
|
|
239
260
|
<Menu.Row key={row.slug} aria-label={row.name}>
|
|
240
261
|
<Menu.RadioItem value={modelValue(entry.kind, row.slug)} closeOnClick>
|
|
@@ -256,10 +277,13 @@ export function RunSettings({
|
|
|
256
277
|
// Every model of one provider, the starred ones first and the rest under a label per maker where the provider names them.
|
|
257
278
|
const byMaker = (entry: ProviderInfo, models: ModelInfo[]) => {
|
|
258
279
|
const starred = favoritesOf(entry.kind);
|
|
280
|
+
|
|
259
281
|
return favoritesFirst(models, starred).map((row, i, sorted) => {
|
|
260
282
|
const previous = sorted[i - 1];
|
|
283
|
+
|
|
261
284
|
const labeled =
|
|
262
285
|
row.vendor !== undefined && !starred.has(row.slug) && (previous === undefined || starred.has(previous.slug) || previous.vendor !== row.vendor);
|
|
286
|
+
|
|
263
287
|
return (
|
|
264
288
|
<Fragment key={row.slug}>
|
|
265
289
|
{labeled && <Menu.GroupLabel>{row.vendor}</Menu.GroupLabel>}
|
|
@@ -273,9 +297,11 @@ export function RunSettings({
|
|
|
273
297
|
const providerModels = (entry: ProviderInfo, models: ModelInfo[]) => {
|
|
274
298
|
const starred = favoritesOf(entry.kind);
|
|
275
299
|
const shortlist = shortlistOf(entry.kind, models, { provider, model: selection.model }, starred);
|
|
300
|
+
|
|
276
301
|
if (shortlist === null) {
|
|
277
302
|
return [...models.filter((row) => starred.has(row.slug)), ...models.filter((row) => !starred.has(row.slug))].map((row) => modelRow(entry, row));
|
|
278
303
|
}
|
|
304
|
+
|
|
279
305
|
return (
|
|
280
306
|
<>
|
|
281
307
|
{shortlist.map((row) => modelRow(entry, row))}
|
|
@@ -468,6 +494,7 @@ function AccountSubmenu({ provider, account }: { provider: AgentKind; account: A
|
|
|
468
494
|
const host = useChatHost();
|
|
469
495
|
const { t } = useTranslation('agent-chat');
|
|
470
496
|
const { choice, started, onPick } = account;
|
|
497
|
+
|
|
471
498
|
return (
|
|
472
499
|
<Menu.SubmenuRoot>
|
|
473
500
|
<Menu.SubmenuTrigger className="text-text-muted">
|
|
@@ -502,9 +529,11 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
|
|
|
502
529
|
const { t } = useTranslation('agent-chat');
|
|
503
530
|
const limits = useUsageLimits();
|
|
504
531
|
const now = useNow(MINUTE_MS);
|
|
532
|
+
|
|
505
533
|
return choice.offered.map((entry) => {
|
|
506
534
|
const session = sessionWindow(limitsOfAccount(limits, entry.id));
|
|
507
535
|
const disabled = locked(entry.id);
|
|
536
|
+
|
|
508
537
|
const row = (
|
|
509
538
|
<Menu.RadioItem key={entry.id} value={entry.id} disabled={disabled} closeOnClick className="items-start data-disabled:opacity-50">
|
|
510
539
|
<span className="flex min-w-0 grow flex-col">
|
|
@@ -516,6 +545,7 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
|
|
|
516
545
|
</span>
|
|
517
546
|
</Menu.RadioItem>
|
|
518
547
|
);
|
|
548
|
+
|
|
519
549
|
return disabled ? (
|
|
520
550
|
<Tooltip key={entry.id} label={t('pickers.account.forkToSwitch', { account: choice.nameOf(entry) })} side="right" sideOffset={12}>
|
|
521
551
|
{row}
|
|
@@ -529,6 +559,7 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
|
|
|
529
559
|
/* How full the context is, drawn small enough to sit in the pill beside the window's size. */
|
|
530
560
|
function ContextRing({ usage }: { usage: ChatUsage }) {
|
|
531
561
|
const fraction = contextFraction(usage);
|
|
562
|
+
|
|
532
563
|
return (
|
|
533
564
|
<svg width="14" height="14" viewBox="0 0 14 14" className={clsx('shrink-0', contextTone(fraction))} aria-hidden="true">
|
|
534
565
|
<circle cx="7" cy="7" r={RING_RADIUS} fill="none" strokeWidth="2" className="stroke-border-strong" />
|
|
@@ -554,6 +585,7 @@ function ContextUsage({ usage, disabled, onCompact }: { usage: ChatUsage; disabl
|
|
|
554
585
|
const percent = Math.round(fraction * 100);
|
|
555
586
|
const segments = contextSegments(usage);
|
|
556
587
|
const used = formatTokens(usage.contextTokens);
|
|
588
|
+
|
|
557
589
|
return (
|
|
558
590
|
<Menu.Group className="flex flex-col gap-2 px-2.5 py-1.5">
|
|
559
591
|
<div className="flex items-center gap-2">
|
|
@@ -623,14 +655,17 @@ function ContextUsage({ usage, disabled, onCompact }: { usage: ChatUsage; disabl
|
|
|
623
655
|
*/
|
|
624
656
|
function OptionRow({ option, selection, onChange }: { option: ModelOptionDescriptor; selection: ModelSelection; onChange(value: string | boolean): void }) {
|
|
625
657
|
const icon = <Icon icon={optionIcon(option)} size={14} className="shrink-0 text-text-faint" />;
|
|
658
|
+
|
|
626
659
|
if (option.type === 'boolean') {
|
|
627
660
|
const checked = (selection.options[option.id] ?? option.defaultValue) === true;
|
|
661
|
+
|
|
628
662
|
const item = (
|
|
629
663
|
<Menu.CheckboxItem indicator="end" className="text-text-muted" checked={checked} onCheckedChange={onChange} closeOnClick={false}>
|
|
630
664
|
{icon}
|
|
631
665
|
<span className="min-w-0 grow truncate">{option.label}</span>
|
|
632
666
|
</Menu.CheckboxItem>
|
|
633
667
|
);
|
|
668
|
+
|
|
634
669
|
return option.description ? (
|
|
635
670
|
<Tooltip label={option.description} side="right" sideOffset={12}>
|
|
636
671
|
{item}
|
|
@@ -639,7 +674,9 @@ function OptionRow({ option, selection, onChange }: { option: ModelOptionDescrip
|
|
|
639
674
|
item
|
|
640
675
|
);
|
|
641
676
|
}
|
|
677
|
+
|
|
642
678
|
const value = String(selection.options[option.id] ?? option.defaultChoice);
|
|
679
|
+
|
|
643
680
|
if (option.choices.length > INLINE_CHOICES) {
|
|
644
681
|
return (
|
|
645
682
|
<Menu.SubmenuRoot>
|
|
@@ -663,6 +700,7 @@ function OptionRow({ option, selection, onChange }: { option: ModelOptionDescrip
|
|
|
663
700
|
</Menu.SubmenuRoot>
|
|
664
701
|
);
|
|
665
702
|
}
|
|
703
|
+
|
|
666
704
|
return (
|
|
667
705
|
<div className="flex items-center gap-2.5 px-2.5 py-1.5 text-sm text-text-muted">
|
|
668
706
|
{icon}
|
package/src/chat/ui/Scrubber.tsx
CHANGED
|
@@ -99,8 +99,10 @@ export const Scrubber = memo(function Scrubber({
|
|
|
99
99
|
if (strip === null || typeof ResizeObserver === 'undefined') {
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
+
|
|
102
103
|
const observer = new ResizeObserver(() => setHeight(strip.offsetHeight));
|
|
103
104
|
observer.observe(strip);
|
|
105
|
+
|
|
104
106
|
return () => observer.disconnect();
|
|
105
107
|
}, [strip]);
|
|
106
108
|
|
|
@@ -115,13 +117,16 @@ export const Scrubber = memo(function Scrubber({
|
|
|
115
117
|
|
|
116
118
|
const anchor = useMemo(() => {
|
|
117
119
|
const element = strip;
|
|
120
|
+
|
|
118
121
|
if (anchorY === null || element === null) {
|
|
119
122
|
return undefined;
|
|
120
123
|
}
|
|
124
|
+
|
|
121
125
|
return {
|
|
122
126
|
contextElement: element,
|
|
123
127
|
getBoundingClientRect: () => {
|
|
124
128
|
const rect = element.getBoundingClientRect();
|
|
129
|
+
|
|
125
130
|
return DOMRect.fromRect({ x: rect.left, y: rect.top + anchorY + TICK_HEIGHT_PX / 2, width: rect.width, height: 0 });
|
|
126
131
|
}
|
|
127
132
|
};
|
|
@@ -131,10 +136,13 @@ export const Scrubber = memo(function Scrubber({
|
|
|
131
136
|
|
|
132
137
|
const actionsFor = (tick: ScrubberTick | null): CardAction[] =>
|
|
133
138
|
tick === null ? [] : Object.values(CARD_ACTIONS).filter((action) => action.offers(tick, chat));
|
|
139
|
+
|
|
134
140
|
const actions = actionsFor(hoveredTick);
|
|
141
|
+
|
|
135
142
|
/* The message a bookmark goes on: the one it is on already, or the person's own. A wake is the machine's and holds none. */
|
|
136
143
|
const markableOf = (tick: ScrubberTick | null): string | null =>
|
|
137
144
|
tick === null || chat === null ? null : (tick.bookmark?.itemId ?? (tick.kind === 'person' ? tick.id : null));
|
|
145
|
+
|
|
138
146
|
const menuTick = menuIndex === null ? null : (ticks[menuIndex] ?? null);
|
|
139
147
|
const menuMarkable = markableOf(menuTick);
|
|
140
148
|
|
|
@@ -142,10 +150,13 @@ export const Scrubber = memo(function Scrubber({
|
|
|
142
150
|
// The hover card is a child here as well, and a click in it is about no tick.
|
|
143
151
|
const index = strip === null || cameThroughPortal(e) ? null : messageAt(layout, e.clientY - strip.getBoundingClientRect().top);
|
|
144
152
|
const tick = index === null ? null : (ticks[index] ?? null);
|
|
153
|
+
|
|
145
154
|
if (tick === null || (actionsFor(tick).length === 0 && markableOf(tick) === null)) {
|
|
146
155
|
e.preventBaseUIHandler();
|
|
156
|
+
|
|
147
157
|
return;
|
|
148
158
|
}
|
|
159
|
+
|
|
149
160
|
setMenuIndex(index);
|
|
150
161
|
};
|
|
151
162
|
|
|
@@ -165,6 +176,7 @@ export const Scrubber = memo(function Scrubber({
|
|
|
165
176
|
onPointerMove={(e) => {
|
|
166
177
|
setPointing(true);
|
|
167
178
|
const index = pointAt(e);
|
|
179
|
+
|
|
168
180
|
if (index !== hovered) {
|
|
169
181
|
setHovered(index);
|
|
170
182
|
}
|
|
@@ -172,6 +184,7 @@ export const Scrubber = memo(function Scrubber({
|
|
|
172
184
|
onPointerLeave={() => setPointing(false)}
|
|
173
185
|
onClick={(e) => {
|
|
174
186
|
const index = pointAt(e);
|
|
187
|
+
|
|
175
188
|
if (index !== null) {
|
|
176
189
|
onPick(index);
|
|
177
190
|
}
|
|
@@ -183,6 +196,7 @@ export const Scrubber = memo(function Scrubber({
|
|
|
183
196
|
const distance = pointing && hoveredSlot !== null ? Math.abs(index - hoveredSlot) : null;
|
|
184
197
|
// A bookmark is a place to find again from anywhere, so it never dims with the rest.
|
|
185
198
|
const bright = slot.marked || slotInView(slot, inView) || distance === 0;
|
|
199
|
+
|
|
186
200
|
return (
|
|
187
201
|
<Fragment key={slot.first}>
|
|
188
202
|
<span
|
|
@@ -20,6 +20,7 @@ export function SessionLine({ window, now }: { window: UsageWindow; now: number
|
|
|
20
20
|
const tone = sessionTone(window.used);
|
|
21
21
|
const mark = evenPace(window, now)?.percent ?? null;
|
|
22
22
|
const resetsAt = window.resetsAt;
|
|
23
|
+
|
|
23
24
|
return (
|
|
24
25
|
<span className="flex items-center gap-2 text-xs text-text-faint tabular-nums">
|
|
25
26
|
<span aria-hidden className="relative h-1 w-14 shrink-0 rounded-full bg-surface-sunken">
|
|
@@ -32,6 +32,7 @@ const cache = new Map<string, Run[]>();
|
|
|
32
32
|
function runsOf(highlighter: Highlighter, code: string, language: string, theme: string, offset: number): Run[] {
|
|
33
33
|
const fg = highlighter.getTheme(theme).fg.toLowerCase();
|
|
34
34
|
const { tokens } = highlighter.codeToTokens(code, { lang: language as BundledLanguage, theme });
|
|
35
|
+
|
|
35
36
|
return tokens.flat().map((token) => ({
|
|
36
37
|
start: offset + token.offset,
|
|
37
38
|
end: offset + token.offset + token.content.length,
|
|
@@ -44,18 +45,23 @@ function runsOf(highlighter: Highlighter, code: string, language: string, theme:
|
|
|
44
45
|
/* `base` with `[start, end)` cut out of it and `inner` in its place. */
|
|
45
46
|
function overlay(base: readonly Run[], start: number, end: number, inner: readonly Run[]): Run[] {
|
|
46
47
|
const kept: Run[] = [];
|
|
48
|
+
|
|
47
49
|
for (const run of base) {
|
|
48
50
|
if (run.end <= start || run.start >= end) {
|
|
49
51
|
kept.push(run);
|
|
52
|
+
|
|
50
53
|
continue;
|
|
51
54
|
}
|
|
55
|
+
|
|
52
56
|
if (run.start < start) {
|
|
53
57
|
kept.push({ ...run, end: start, content: run.content.slice(0, start - run.start) });
|
|
54
58
|
}
|
|
59
|
+
|
|
55
60
|
if (run.end > end) {
|
|
56
61
|
kept.push({ ...run, start: end, content: run.content.slice(end - run.start) });
|
|
57
62
|
}
|
|
58
63
|
}
|
|
64
|
+
|
|
59
65
|
return [...kept, ...inner].sort((left, right) => left.start - right.start);
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -74,25 +80,34 @@ function languagesOf(command: string, extra: readonly EmbeddedCode[]): Set<strin
|
|
|
74
80
|
function highlight(command: string, extra: readonly EmbeddedCode[], theme: string, code: CodeHighlighter): Run[] | null {
|
|
75
81
|
const key = `${theme}\n${extra.map(({ start, end, language }) => `${start}:${end}:${language}`).join(',')}\n${command}`;
|
|
76
82
|
const known = cache.get(key);
|
|
83
|
+
|
|
77
84
|
if (known !== undefined) {
|
|
78
85
|
// Touched entries move to the end, so the oldest falls out first.
|
|
79
86
|
cache.delete(key);
|
|
80
87
|
cache.set(key, known);
|
|
88
|
+
|
|
81
89
|
return known;
|
|
82
90
|
}
|
|
91
|
+
|
|
83
92
|
const embedded = codeIn(command, extra);
|
|
84
93
|
const highlighter = code.ready([SHELL_LANGUAGE, ...embedded.map(({ language }) => language)], theme);
|
|
94
|
+
|
|
85
95
|
if (highlighter === null) {
|
|
86
96
|
return null;
|
|
87
97
|
}
|
|
98
|
+
|
|
88
99
|
let runs = runsOf(highlighter, command, code.language(SHELL_LANGUAGE), theme, 0);
|
|
100
|
+
|
|
89
101
|
for (const part of embedded) {
|
|
90
102
|
runs = overlay(runs, part.start, part.end, runsOf(highlighter, command.slice(part.start, part.end), code.language(part.language), theme, part.start));
|
|
91
103
|
}
|
|
104
|
+
|
|
92
105
|
cache.set(key, runs);
|
|
106
|
+
|
|
93
107
|
if (cache.size > CACHE_LIMIT) {
|
|
94
108
|
cache.delete(cache.keys().next().value!);
|
|
95
109
|
}
|
|
110
|
+
|
|
96
111
|
return runs;
|
|
97
112
|
}
|
|
98
113
|
|
|
@@ -101,20 +116,26 @@ function draw(command: string, runs: readonly Run[] | null, from: number, to: nu
|
|
|
101
116
|
if (runs === null) {
|
|
102
117
|
return [command.slice(from, to)];
|
|
103
118
|
}
|
|
119
|
+
|
|
104
120
|
const parts: ReactNode[] = [];
|
|
105
121
|
let at = from;
|
|
122
|
+
|
|
106
123
|
for (const run of runs) {
|
|
107
124
|
if (run.end <= from || run.start >= to) {
|
|
108
125
|
continue;
|
|
109
126
|
}
|
|
127
|
+
|
|
110
128
|
const start = Math.max(run.start, from);
|
|
111
129
|
const end = Math.min(run.end, to);
|
|
130
|
+
|
|
112
131
|
// Shiki leaves the line breaks out of its tokens; they come from the command itself.
|
|
113
132
|
if (start > at) {
|
|
114
133
|
parts.push(command.slice(at, start));
|
|
115
134
|
}
|
|
135
|
+
|
|
116
136
|
const content = run.content.slice(start - run.start, end - run.start);
|
|
117
137
|
const style = tokenStyle(run);
|
|
138
|
+
|
|
118
139
|
parts.push(
|
|
119
140
|
style === undefined ? (
|
|
120
141
|
content
|
|
@@ -124,11 +145,14 @@ function draw(command: string, runs: readonly Run[] | null, from: number, to: nu
|
|
|
124
145
|
</span>
|
|
125
146
|
)
|
|
126
147
|
);
|
|
148
|
+
|
|
127
149
|
at = end;
|
|
128
150
|
}
|
|
151
|
+
|
|
129
152
|
if (at < to) {
|
|
130
153
|
parts.push(command.slice(at, to));
|
|
131
154
|
}
|
|
155
|
+
|
|
132
156
|
return parts;
|
|
133
157
|
}
|
|
134
158
|
|
|
@@ -148,14 +172,18 @@ function Name({ named }: { named: NamedWord }) {
|
|
|
148
172
|
function drawNamed(command: string, runs: readonly Run[] | null, names: readonly NamedWord[]): ReactNode[] {
|
|
149
173
|
const parts: ReactNode[] = [];
|
|
150
174
|
let at = 0;
|
|
175
|
+
|
|
151
176
|
for (const named of names) {
|
|
152
177
|
if (named.start < at) {
|
|
153
178
|
continue;
|
|
154
179
|
}
|
|
180
|
+
|
|
155
181
|
parts.push(...draw(command, runs, at, named.start), <Name key={`name-${named.start}`} named={named} />);
|
|
156
182
|
at = named.end;
|
|
157
183
|
}
|
|
184
|
+
|
|
158
185
|
parts.push(...draw(command, runs, at, command.length));
|
|
186
|
+
|
|
159
187
|
return parts;
|
|
160
188
|
}
|
|
161
189
|
|
|
@@ -180,10 +208,13 @@ export function ShellCommand({ command, code = NO_CODE, names = NO_NAMES }: { co
|
|
|
180
208
|
if (runs !== null || tooLong) {
|
|
181
209
|
return;
|
|
182
210
|
}
|
|
211
|
+
|
|
183
212
|
let cancelled = false;
|
|
213
|
+
|
|
184
214
|
Promise.all([highlighter.loadTheme(theme, custom), ...[...languagesOf(command, code)].map((lang) => highlighter.loadLanguage(lang))])
|
|
185
215
|
.then(() => !cancelled && setLoads((count) => count + 1))
|
|
186
216
|
.catch(() => undefined);
|
|
217
|
+
|
|
187
218
|
return () => {
|
|
188
219
|
cancelled = true;
|
|
189
220
|
};
|
|
@@ -192,5 +223,6 @@ export function ShellCommand({ command, code = NO_CODE, names = NO_NAMES }: { co
|
|
|
192
223
|
if (names.length > 0) {
|
|
193
224
|
return <>{drawNamed(command, runs, names)}</>;
|
|
194
225
|
}
|
|
226
|
+
|
|
195
227
|
return <>{runs === null ? command : draw(command, runs, 0, command.length)}</>;
|
|
196
228
|
}
|