@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,318 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { ContextMenu } from '@base-ui-components/react/context-menu';
|
|
5
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
6
|
+
import clsx from 'clsx';
|
|
7
|
+
import { useForkedTurns } from '../forks';
|
|
8
|
+
import { bookmarkRows } from '../logic/bookmarks';
|
|
9
|
+
import { deriveTimelineRows, findSubagentBranch } from '../logic/timeline';
|
|
10
|
+
import { openFromMain, useSubagentTrail } from '../subagent-view';
|
|
11
|
+
import { registerItemJumper, registerMessageStepper, registerTimeline, setTimelineAtEnd } from '../timeline-scroll';
|
|
12
|
+
import { SCRUBBER_MIN_TICKS, STRIP_INSET_PX, STRIP_WIDTH_PX, messagesInView, stepMessage, threadPaddingLeft, tickOfRow, ticksOf, ticksWithHits } from '../logic/scrubber';
|
|
13
|
+
import { EMPTY_TARGET, readTimelineTarget, withCurrentText } from '../logic/timeline-target';
|
|
14
|
+
import { forkRefusal } from '../logic/fork';
|
|
15
|
+
import { FindRevealContext } from './find-reveal';
|
|
16
|
+
import { BookmarkMarker } from './BookmarkMarker';
|
|
17
|
+
import { MessageActions } from './MessageActions';
|
|
18
|
+
import { Scrubber } from './Scrubber';
|
|
19
|
+
import { TimelineMenuPopup } from './TimelineMenu';
|
|
20
|
+
import { FOLLOW_THRESHOLD_PX, rowRhythm } from './rows/row-rhythm';
|
|
21
|
+
import { QuoteButton } from './QuoteButton';
|
|
22
|
+
import { QuoteTakerContext } from './quote-selection';
|
|
23
|
+
import { useToggleSet } from './useToggleSet';
|
|
24
|
+
import { Row } from './rows/Rows';
|
|
25
|
+
import { useChatRow, useChats } from '../../state/chats';
|
|
26
|
+
import { chatHost } from '../../host';
|
|
27
|
+
import { useChatScope } from '../../scope';
|
|
28
|
+
import { FileLinkContext } from './file-links';
|
|
29
|
+
import { AgentIcon } from '../../agents/AgentIcon';
|
|
30
|
+
import { useModelName } from '../../agents/model-name';
|
|
31
|
+
import { SECTION_LABEL } from '@ruimte/ui/classes';
|
|
32
|
+
import { EmptyState } from '@ruimte/ui/EmptyState';
|
|
33
|
+
import { ErrorBoundary } from '@ruimte/ui/ErrorBoundary';
|
|
34
|
+
const ESTIMATED_ROW_PX = 56;
|
|
35
|
+
/*
|
|
36
|
+
* A thread before its first message: which model answers, what the lines into this node let it read,
|
|
37
|
+
* and the folder it works in, so a first question can lean on all three.
|
|
38
|
+
*/
|
|
39
|
+
function EmptyThread({ chatId }) {
|
|
40
|
+
const { t } = useTranslation('agent-chat');
|
|
41
|
+
const info = useChatRow(chatId, (row) => row?.info ?? null);
|
|
42
|
+
const model = useModelName(info?.provider, info?.selection.model ?? '');
|
|
43
|
+
const sources = chatHost().useContextSources(chatId);
|
|
44
|
+
return (_jsx("div", { className: "flex grow items-center justify-center", children: _jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 px-6 py-8 text-center", children: [_jsx(EmptyState, { icon: info ? _jsx(AgentIcon, { kind: info.provider }) : undefined, className: "p-0", children: info ? t('timeline.empty.ready', { model }) : t('timeline.empty.ask') }), sources.length > 0 && (_jsxs("div", { className: "flex max-w-full flex-col items-center gap-1", children: [_jsx("span", { className: SECTION_LABEL, children: t('timeline.empty.canRead') }), _jsx("span", { className: "max-w-full text-xs text-text-muted", children: sources.map((source) => source.title).join(', ') })] })), info?.cwd && (_jsxs("div", { className: "flex max-w-full flex-col items-center gap-1", children: [_jsx("span", { className: SECTION_LABEL, children: t('timeline.empty.worksIn') }), _jsx("span", { className: "max-w-full font-mono text-xs break-all text-text-muted", children: info.cwd })] }))] }) }));
|
|
45
|
+
}
|
|
46
|
+
/*
|
|
47
|
+
* A row of the thread behind the chapter line of its bookmark. A message, the only row that takes a
|
|
48
|
+
* bookmark, gets its actions under it; every other row carries a bookmark only for a reply it folds.
|
|
49
|
+
*/
|
|
50
|
+
function MarkableRow({ row, chatId, bookmark, children }) {
|
|
51
|
+
if (row.kind !== 'user' && row.kind !== 'assistant') {
|
|
52
|
+
return (_jsxs(_Fragment, { children: [bookmark !== null && _jsx(BookmarkMarker, { chatId: chatId, itemId: bookmark.itemId, bookmark: bookmark }), children] }));
|
|
53
|
+
}
|
|
54
|
+
return (_jsxs(_Fragment, { children: [_jsx(BookmarkMarker, { chatId: chatId, itemId: row.id, bookmark: bookmark }), _jsxs("div", { className: "group/message", children: [children, _jsx(MessageActions, { chatId: chatId, row: row })] })] }));
|
|
55
|
+
}
|
|
56
|
+
export function Timeline({ chatId, composer }) {
|
|
57
|
+
const { t } = useTranslation('agent-chat');
|
|
58
|
+
const order = useChatRow(chatId, (row) => row?.order);
|
|
59
|
+
// The structure, not the items. A delta growing a reply must not derive every row again.
|
|
60
|
+
const items = useChatRow(chatId, (row) => row?.structure);
|
|
61
|
+
const activeTurnId = useChatRow(chatId, (row) => row?.info.activeTurnId ?? null);
|
|
62
|
+
const bookmarks = useChatRow(chatId, (row) => row?.bookmarks);
|
|
63
|
+
const forkedTurns = useForkedTurns(chatId);
|
|
64
|
+
const info = useChatRow(chatId, (row) => row?.info ?? null);
|
|
65
|
+
const { keyOf } = useChatScope();
|
|
66
|
+
// Read when the menu opens rather than subscribed to, for the same reason the rows use the structure.
|
|
67
|
+
const fullItems = () => useChats.getState().byKey[keyOf(chatId)]?.items;
|
|
68
|
+
const scrollRef = useRef(null);
|
|
69
|
+
const threadRef = useRef(null);
|
|
70
|
+
const contentRef = useRef(null);
|
|
71
|
+
const quoteTakerRef = useRef(null);
|
|
72
|
+
const registerQuoteTaker = useCallback((take) => {
|
|
73
|
+
quoteTakerRef.current = take;
|
|
74
|
+
return () => {
|
|
75
|
+
if (quoteTakerRef.current === take) {
|
|
76
|
+
quoteTakerRef.current = null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
80
|
+
const followRef = useRef(true);
|
|
81
|
+
const stopFollowing = useCallback(() => {
|
|
82
|
+
followRef.current = false;
|
|
83
|
+
}, []);
|
|
84
|
+
const groups = useToggleSet(stopFollowing);
|
|
85
|
+
const turns = useToggleSet(stopFollowing);
|
|
86
|
+
const subagents = useToggleSet(stopFollowing);
|
|
87
|
+
const scrollGeometry = useRef({ top: 0, height: 0, viewport: 0 });
|
|
88
|
+
const [target, setTarget] = useState(EMPTY_TARGET);
|
|
89
|
+
const { trail, show } = useSubagentTrail(chatId);
|
|
90
|
+
const frameRef = useRef(null);
|
|
91
|
+
const composerRef = useRef(null);
|
|
92
|
+
const [composerHeight, setComposerHeight] = useState(0);
|
|
93
|
+
const [frame, setFrame] = useState({ width: 0, height: 0, column: false });
|
|
94
|
+
const rows = useMemo(() => {
|
|
95
|
+
if (!order || !items) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
return deriveTimelineRows(order.map((id) => items[id]), { expandedGroups: groups.ids, expandedTurns: turns.ids, expandedSubagents: subagents.ids, activeTurnId, forkedTurns });
|
|
99
|
+
}, [order, items, groups.ids, turns.ids, subagents.ids, activeTurnId, forkedTurns]);
|
|
100
|
+
const empty = rows.length === 0;
|
|
101
|
+
const marks = useMemo(() => bookmarkRows(rows, bookmarks ?? [], items ?? {}), [rows, bookmarks, items]);
|
|
102
|
+
const ticks = useMemo(() => ticksOf(rows, marks), [rows, marks]);
|
|
103
|
+
// A message a jump is on its way to; it may first have to open the turn its reply folded into.
|
|
104
|
+
const [seeking, setSeeking] = useState(null);
|
|
105
|
+
// Whether the thread's text clears the strip depends on the width of the view, which only the thread's own frame tells.
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const element = frameRef.current;
|
|
108
|
+
if (element === null || typeof ResizeObserver === 'undefined') {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const column = element.closest('.chat-column') !== null;
|
|
112
|
+
const measure = () => setFrame((current) => current.width === element.offsetWidth && current.height === element.offsetHeight && current.column === column
|
|
113
|
+
? current
|
|
114
|
+
: { width: element.offsetWidth, height: element.offsetHeight, column });
|
|
115
|
+
const observer = new ResizeObserver(measure);
|
|
116
|
+
observer.observe(element);
|
|
117
|
+
measure();
|
|
118
|
+
return () => observer.disconnect();
|
|
119
|
+
}, []);
|
|
120
|
+
useLayoutEffect(() => {
|
|
121
|
+
const element = composerRef.current;
|
|
122
|
+
if (element === null) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const measure = () => setComposerHeight(element.offsetHeight);
|
|
126
|
+
const observer = new ResizeObserver(measure);
|
|
127
|
+
observer.observe(element);
|
|
128
|
+
measure();
|
|
129
|
+
return () => observer.disconnect();
|
|
130
|
+
}, []);
|
|
131
|
+
// Oversized prompts scroll in full instead of sticking with their top outside the viewport.
|
|
132
|
+
const stickyComposer = composerHeight < frame.height - FOLLOW_THRESHOLD_PX;
|
|
133
|
+
const coveredHeight = stickyComposer ? composerHeight : 0;
|
|
134
|
+
const coveredHeightRef = useRef(coveredHeight);
|
|
135
|
+
coveredHeightRef.current = coveredHeight;
|
|
136
|
+
// The composer pages through the thread with PageUp and PageDown; this is the element it moves.
|
|
137
|
+
useEffect(() => registerTimeline(chatId, scrollRef.current, (enabled) => {
|
|
138
|
+
followRef.current = enabled;
|
|
139
|
+
}, () => coveredHeightRef.current), [chatId]);
|
|
140
|
+
// The client does not run the React Compiler, so its memoization rule has nothing to break here.
|
|
141
|
+
// oxlint-disable-next-line react/incompatible-library
|
|
142
|
+
const virtualizer = useVirtualizer({
|
|
143
|
+
count: rows.length,
|
|
144
|
+
getScrollElement: () => scrollRef.current,
|
|
145
|
+
estimateSize: () => ESTIMATED_ROW_PX,
|
|
146
|
+
getItemKey: (index) => rows[index].id,
|
|
147
|
+
overscan: 8,
|
|
148
|
+
scrollPaddingEnd: coveredHeight
|
|
149
|
+
});
|
|
150
|
+
// A change in what the last row says (a streaming delta) must also pull the view down.
|
|
151
|
+
const lastRow = rows[rows.length - 1];
|
|
152
|
+
const lastReplyLength = useChatRow(chatId, (row) => {
|
|
153
|
+
const item = lastRow?.kind === 'assistant' ? row?.items[lastRow.id] : undefined;
|
|
154
|
+
return item?.kind === 'assistant' ? item.text.length : null;
|
|
155
|
+
});
|
|
156
|
+
const tail = lastReplyLength ?? rows.length;
|
|
157
|
+
/*
|
|
158
|
+
* The rows are measured as they render, so the end of the list moves while it is drawn: a diff
|
|
159
|
+
* that highlights, an image that loads, a tool row that grows a line of output. `scrollToIndex`
|
|
160
|
+
* aims at where the row was when it was asked, which is what left the thread short of the end.
|
|
161
|
+
* The scroller's own bottom is a fact rather than an estimate, and the total size changing is
|
|
162
|
+
* what says a row was measured again, so this runs for every one of those.
|
|
163
|
+
*/
|
|
164
|
+
const totalSize = virtualizer.getTotalSize();
|
|
165
|
+
useLayoutEffect(() => {
|
|
166
|
+
const element = scrollRef.current;
|
|
167
|
+
if (!followRef.current || rows.length === 0 || element === null) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// The composer is in the scroll flow, so its measured height is part of this end position.
|
|
171
|
+
element.scrollTop = element.scrollHeight;
|
|
172
|
+
scrollGeometry.current = { top: element.scrollTop, height: element.scrollHeight, viewport: element.clientHeight };
|
|
173
|
+
}, [rows.length, tail, totalSize, composerHeight, frame.height]);
|
|
174
|
+
// A sub-agent in the thread's place hides the thread, and the strip and the keyboard go with it.
|
|
175
|
+
const onMainAgent = trail.length === 0;
|
|
176
|
+
// A node on a canvas has no strip. It is too narrow to give up a column of its width.
|
|
177
|
+
const showsScrubber = frame.column && onMainAgent && ticks.length >= SCRUBBER_MIN_TICKS;
|
|
178
|
+
const paddingLeft = threadPaddingLeft(frame.width, showsScrubber);
|
|
179
|
+
/*
|
|
180
|
+
* Where each message starts, from the virtualizer's measurements (an estimate for a row it never
|
|
181
|
+
* drew), so reading the scroll position costs no layout. The virtualizer renders this component on
|
|
182
|
+
* every scroll already.
|
|
183
|
+
*/
|
|
184
|
+
const measurements = virtualizer.measurementsCache;
|
|
185
|
+
const starts = ticks.map((tick) => measurements[tick.rowIndex]?.start ?? tick.rowIndex * ESTIMATED_ROW_PX);
|
|
186
|
+
const scrollTop = virtualizer.scrollOffset ?? 0;
|
|
187
|
+
const viewport = virtualizer.scrollRect?.height ?? 0;
|
|
188
|
+
const visibleHeight = Math.max(0, viewport - coveredHeight);
|
|
189
|
+
const inView = showsScrubber ? messagesInView({ starts, scrollTop, visibleHeight }) : null;
|
|
190
|
+
const chatFind = chatHost().useTimelineFind({
|
|
191
|
+
chatId,
|
|
192
|
+
frame: frameRef,
|
|
193
|
+
thread: threadRef,
|
|
194
|
+
scroller: scrollRef,
|
|
195
|
+
enabled: onMainAgent,
|
|
196
|
+
rows,
|
|
197
|
+
structure: items,
|
|
198
|
+
order,
|
|
199
|
+
firstRowInView: () => {
|
|
200
|
+
const top = scrollRef.current?.scrollTop ?? 0;
|
|
201
|
+
return virtualizer.getVirtualItems().find((virtualRow) => virtualRow.end > top)?.index ?? 0;
|
|
202
|
+
},
|
|
203
|
+
openTurn: turns.add,
|
|
204
|
+
openGroup: groups.add,
|
|
205
|
+
openSubagent: subagents.add,
|
|
206
|
+
// A row already on screen stays put; the find scrolls to the match itself once it is drawn.
|
|
207
|
+
scrollToRow: (index) => {
|
|
208
|
+
const top = scrollRef.current?.scrollTop ?? 0;
|
|
209
|
+
const bottom = top + visibleHeight;
|
|
210
|
+
if (!virtualizer.getVirtualItems().some((virtualRow) => virtualRow.index === index && virtualRow.end > top && virtualRow.start < bottom)) {
|
|
211
|
+
virtualizer.scrollToIndex(index, { align: 'center' });
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
coveredHeight: () => coveredHeightRef.current,
|
|
215
|
+
stopFollowing
|
|
216
|
+
});
|
|
217
|
+
const searching = chatFind.open;
|
|
218
|
+
const found = useMemo(() => (searching ? ticksWithHits(ticks, chatFind.hitRows) : []), [searching, ticks, chatFind.hitRows]);
|
|
219
|
+
const foundCurrent = chatFind.currentRow === null ? null : tickOfRow(ticks, chatFind.currentRow);
|
|
220
|
+
const jumpTo = (index) => {
|
|
221
|
+
const tick = ticks[index];
|
|
222
|
+
if (!tick) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (tick.bookmark !== null) {
|
|
226
|
+
setSeeking(tick.bookmark.itemId);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
followRef.current = false;
|
|
230
|
+
virtualizer.scrollToIndex(tick.rowIndex, { align: 'start' });
|
|
231
|
+
};
|
|
232
|
+
useLayoutEffect(() => {
|
|
233
|
+
if (seeking === null) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const index = rows.findIndex((row) => row.id === seeking);
|
|
237
|
+
if (index !== -1) {
|
|
238
|
+
setSeeking(null);
|
|
239
|
+
followRef.current = false;
|
|
240
|
+
virtualizer.scrollToIndex(index, { align: 'start' });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const turnId = items?.[seeking]?.turnId ?? null;
|
|
244
|
+
if (turnId !== null && !turns.ids.has(turnId)) {
|
|
245
|
+
turns.add(turnId);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
setSeeking(null);
|
|
249
|
+
}, [seeking, rows, items, turns, virtualizer]);
|
|
250
|
+
useEffect(() => registerItemJumper(keyOf(chatId), setSeeking), [keyOf, chatId]);
|
|
251
|
+
// The strip is memoized and draws on every scroll, so it gets one callback for its life.
|
|
252
|
+
const jumpRef = useRef(jumpTo);
|
|
253
|
+
jumpRef.current = jumpTo;
|
|
254
|
+
const pick = useCallback((index) => jumpRef.current(index), []);
|
|
255
|
+
// Read when the card asks rather than on every render, so the strip's memo holds while the thread streams.
|
|
256
|
+
const cardChat = useMemo(() => ({
|
|
257
|
+
chatId,
|
|
258
|
+
canFork: (turnId) => {
|
|
259
|
+
if (chatHost().fork === null) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const row = useChats.getState().byKey[keyOf(chatId)];
|
|
263
|
+
return forkRefusal(row?.info ?? null, row?.structure[turnId]) === null;
|
|
264
|
+
},
|
|
265
|
+
fork: (turnId) => chatHost().fork?.(chatId, turnId)
|
|
266
|
+
}), [keyOf, chatId]);
|
|
267
|
+
const stepRef = useRef(() => false);
|
|
268
|
+
stepRef.current = (direction) => {
|
|
269
|
+
if (!onMainAgent) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const index = stepMessage(starts, (i) => ticks[i].kind === 'person', scrollTop, direction);
|
|
273
|
+
if (index === null) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
jumpTo(index);
|
|
277
|
+
return true;
|
|
278
|
+
};
|
|
279
|
+
useEffect(() => registerMessageStepper(keyOf(chatId), (direction) => stepRef.current(direction)), [keyOf, chatId]);
|
|
280
|
+
/* The whole of what a sub-agent did, in the thread's place. Its row only keeps the beginning. */
|
|
281
|
+
const openConversation = (step) => {
|
|
282
|
+
show(openFromMain(step));
|
|
283
|
+
};
|
|
284
|
+
/* The header of a turn a sub-agent woke. It points at the row that agent worked in, or the one it hangs under. */
|
|
285
|
+
const openSubagent = (toolUseId) => {
|
|
286
|
+
const found = findSubagentBranch(rows, toolUseId);
|
|
287
|
+
if (!found) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
subagents.add(found.branch.id);
|
|
291
|
+
followRef.current = false;
|
|
292
|
+
virtualizer.scrollToIndex(found.index, { align: 'start' });
|
|
293
|
+
};
|
|
294
|
+
return (_jsx(FileLinkContext.Provider, { value: info?.cwd ?? null, children: _jsxs("div", { ref: frameRef, className: "relative flex min-h-0 grow flex-col", children: [searching && chatFind.bar, _jsxs(ContextMenu.Root, { children: [_jsx("div", { ref: scrollRef,
|
|
295
|
+
// No CSS scroll-padding here: Chromium counts the sticky composer's caret as hidden behind
|
|
296
|
+
// it and scrolls the thread toward the end on every keystroke.
|
|
297
|
+
className: "chat-scroll min-h-0 grow overflow-auto", onScrollCapture: (e) => {
|
|
298
|
+
const el = e.currentTarget;
|
|
299
|
+
// Capture the reader's movement before virtualization remeasures rows and shifts the scroll offset.
|
|
300
|
+
const previous = scrollGeometry.current;
|
|
301
|
+
const layoutChanged = previous.height !== el.scrollHeight || previous.viewport !== el.clientHeight;
|
|
302
|
+
const atEnd = el.scrollHeight - el.scrollTop - el.clientHeight < FOLLOW_THRESHOLD_PX;
|
|
303
|
+
if (!layoutChanged && el.scrollTop < previous.top) {
|
|
304
|
+
followRef.current = false;
|
|
305
|
+
}
|
|
306
|
+
else if (atEnd) {
|
|
307
|
+
followRef.current = true;
|
|
308
|
+
}
|
|
309
|
+
scrollGeometry.current = { top: el.scrollTop, height: el.scrollHeight, viewport: el.clientHeight };
|
|
310
|
+
setTimelineAtEnd(chatId, followRef.current);
|
|
311
|
+
}, children: _jsxs("div", { ref: contentRef, className: "relative flex min-h-full flex-col", children: [_jsx(ContextMenu.Trigger, { ref: threadRef, className: "chat-thread flex grow flex-col px-4 pt-4", style: { paddingLeft }, onContextMenu: (e) => setTarget(readTimelineTarget(e.target, threadRef.current, withCurrentText(rows, fullItems()))), children: empty ? (_jsx(EmptyThread, { chatId: chatId })) : (_jsx("div", { className: "chat-column-content relative w-full shrink-0", style: { height: virtualizer.getTotalSize() }, children: virtualizer.getVirtualItems().map((virtualRow) => {
|
|
312
|
+
const row = rows[virtualRow.index];
|
|
313
|
+
// Put turn gaps inside the measured row so the virtualizer includes them.
|
|
314
|
+
const question = row.kind === 'user';
|
|
315
|
+
const previous = virtualRow.index > 0 ? rows[virtualRow.index - 1] : null;
|
|
316
|
+
return (_jsx("div", { "data-index": virtualRow.index, "data-item-id": row.id, ref: virtualizer.measureElement, className: clsx('absolute left-0 top-0 w-full', rowRhythm(row, previous), question && virtualRow.index > 0 && 'pt-(--chat-turn-gap)'), style: { transform: `translateY(${virtualRow.start}px)` }, children: _jsx(FindRevealContext.Provider, { value: chatFind.reveal, children: _jsx(MarkableRow, { row: row, chatId: chatId, bookmark: marks.get(row.id) ?? null, children: _jsx(Row, { row: row, chatId: chatId, toggleGroup: groups.toggle, toggleTurn: turns.toggle, toggleSubagent: subagents.toggle, openSubagent: openSubagent, openConversation: openConversation }) }) }) }, row.id));
|
|
317
|
+
}) })) }), _jsx(QuoteButton, { thread: threadRef, frame: contentRef, taker: quoteTakerRef }), _jsx("div", { ref: composerRef, className: clsx('relative z-10 shrink-0', composer && 'px-3 pb-3 pt-3'), style: { position: stickyComposer ? 'sticky' : 'relative', bottom: 0 }, children: _jsx(QuoteTakerContext.Provider, { value: registerQuoteTaker, children: composer }) })] }) }), _jsx(TimelineMenuPopup, { target: target, thread: threadRef, chatId: onMainAgent ? chatId : null })] }), showsScrubber && (_jsx("div", { className: "absolute top-4", style: { left: STRIP_INSET_PX, width: STRIP_WIDTH_PX, bottom: coveredHeight }, children: _jsx(ErrorBoundary, { label: t('timeline.stripFailed'), resetKeys: [ticks.length], children: _jsx(Scrubber, { ticks: ticks, firstInView: inView?.first ?? null, lastInView: inView?.last ?? null, onPick: pick, chat: cardChat, found: found, foundCurrent: foundCurrent }) }) }))] }) }));
|
|
318
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RefObject } from 'react';
|
|
2
|
+
import type { TimelineTarget } from '../logic/timeline-target';
|
|
3
|
+
export declare function TimelineMenuPopup({ target, thread, chatId }: {
|
|
4
|
+
target: TimelineTarget;
|
|
5
|
+
thread: RefObject<HTMLDivElement | null>;
|
|
6
|
+
chatId?: string | null;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
8
|
+
export declare function BookmarkMenuItems({ chatId, itemId, onName }: {
|
|
9
|
+
chatId: string;
|
|
10
|
+
itemId: string;
|
|
11
|
+
onName?: () => void;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ContextMenu } from '@base-ui-components/react/context-menu';
|
|
4
|
+
import { BookmarkPlus, BookmarkX, Braces, Copy, Eye, FileText, GitFork, MessageSquare, Pencil, Scan } from 'lucide-react';
|
|
5
|
+
import { placeBookmark, removeBookmark, useBookmarkNaming } from '../bookmarks';
|
|
6
|
+
import { forkRefusal, turnIdOfRow } from '../logic/fork';
|
|
7
|
+
import { markdownOf, messageTextOf } from '../logic/timeline-copy';
|
|
8
|
+
import { chatHost } from '../../host';
|
|
9
|
+
import { useChatScope } from '../../scope';
|
|
10
|
+
import { useChatRow } from '../../state/chats';
|
|
11
|
+
import { openFileLink, useFileLinkCwd } from './file-links';
|
|
12
|
+
import { MENU_SEPARATOR } from '@ruimte/ui/classes';
|
|
13
|
+
import { DisabledReason } from '@ruimte/ui/DisabledReason';
|
|
14
|
+
import { copyText } from '@ruimte/ui/clipboard';
|
|
15
|
+
import { Icon } from '@ruimte/ui/Icon';
|
|
16
|
+
import { selectAllWithin } from '@ruimte/ui/selection';
|
|
17
|
+
import { EDIT_SHORTCUTS } from '@ruimte/ui/shortcut';
|
|
18
|
+
import { Kbd } from '@ruimte/ui/Kbd';
|
|
19
|
+
/*
|
|
20
|
+
* The menu behind a right-click in a thread. Copy is the reason it exists. Everything in a thread
|
|
21
|
+
* is text a person may want out of it, and a whole message, a code block or the markdown an answer
|
|
22
|
+
* was written in are each a different amount of that.
|
|
23
|
+
*/
|
|
24
|
+
export function TimelineMenuPopup({ target, thread, chatId = null }) {
|
|
25
|
+
const { t } = useTranslation('agent-chat');
|
|
26
|
+
const message = target.row === null ? null : messageTextOf(target.row);
|
|
27
|
+
const turnId = chatId === null || target.row === null ? null : turnIdOfRow(target.row);
|
|
28
|
+
const forkBlocked = useChatRow(chatId ?? '', (row) => (turnId === null ? null : forkRefusal(row?.info ?? null, row?.structure[turnId])));
|
|
29
|
+
const markdown = target.row === null ? null : markdownOf(target.row);
|
|
30
|
+
const markable = chatId !== null && (target.row?.kind === 'user' || target.row?.kind === 'assistant') ? target.row.id : null;
|
|
31
|
+
// A thread outside a chat (a sub-agent's transcript) has no cwd of its own; the app answers for it then.
|
|
32
|
+
const cwd = useFileLinkCwd();
|
|
33
|
+
const { fork, fileLinks } = chatHost();
|
|
34
|
+
const openInPreview = () => {
|
|
35
|
+
if (target.path !== null) {
|
|
36
|
+
openFileLink(cwd, { path: target.path, ...(target.line === null ? {} : { line: target.line }), directory: false });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return (_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", disabled: target.selection === '', onClick: () => copyText(target.selection), children: [_jsx(Icon, { icon: Copy, size: 14 }), " ", t('common.action.copy'), " ", _jsx(Kbd, { shortcut: EDIT_SHORTCUTS.copy })] }), message !== null && (_jsxs(ContextMenu.Item, { className: "menu-item", onClick: () => copyText(message), children: [_jsx(Icon, { icon: MessageSquare, size: 14 }), " ", t('timeline.menu.copyMessage')] })), target.code !== null && (_jsxs(ContextMenu.Item, { className: "menu-item", onClick: () => copyText(target.code ?? ''), children: [_jsx(Icon, { icon: Braces, size: 14 }), " ", t('timeline.menu.copyCode')] })), markdown !== null && (_jsxs(ContextMenu.Item, { className: "menu-item", onClick: () => copyText(markdown), children: [_jsx(Icon, { icon: FileText, size: 14 }), " ", t('timeline.menu.copyMarkdown')] })), chatId !== null && turnId !== null && fork !== null && (_jsx(DisabledReason, { reason: forkBlocked, children: _jsxs(ContextMenu.Item, { className: "menu-item", disabled: forkBlocked !== null, onClick: () => fork(chatId, turnId), children: [_jsx(Icon, { icon: GitFork, size: 14 }), " ", t('timeline.menu.forkFromHere')] }) })), chatId !== null && markable !== null && _jsx(BookmarkMenuItems, { chatId: chatId, itemId: markable }), _jsx(ContextMenu.Separator, { className: MENU_SEPARATOR }), _jsxs(ContextMenu.Item, { className: "menu-item", onClick: () => selectAllWithin(thread.current), children: [_jsx(Icon, { icon: Scan, size: 14 }), " ", t('common.action.selectAll')] }), target.path !== null && fileLinks !== null && (_jsxs(_Fragment, { children: [_jsx(ContextMenu.Separator, { className: MENU_SEPARATOR }), _jsxs(ContextMenu.Item, { className: "menu-item", onClick: openInPreview, children: [_jsx(Icon, { icon: Eye, size: 14 }), " ", t('timeline.menu.openInPreview')] })] }))] }) }) }));
|
|
40
|
+
}
|
|
41
|
+
/*
|
|
42
|
+
* Place a bookmark on a message, or rename and remove the one it has: the same rows from the thread
|
|
43
|
+
* and from the strip beside it. `onName` runs when a row opens the name field, which sits on the
|
|
44
|
+
* bookmark's line over the message and so has to be on screen; the strip scrolls there, the thread
|
|
45
|
+
* was clicked on it.
|
|
46
|
+
*/
|
|
47
|
+
export function BookmarkMenuItems({ chatId, itemId, onName }) {
|
|
48
|
+
const { t } = useTranslation('agent-chat');
|
|
49
|
+
const scope = useChatScope();
|
|
50
|
+
const bookmark = useChatRow(chatId, (row) => row?.bookmarks?.find((candidate) => candidate.itemId === itemId) ?? null);
|
|
51
|
+
if (bookmark === null) {
|
|
52
|
+
return (_jsxs(ContextMenu.Item, { className: "menu-item", onClick: () => {
|
|
53
|
+
void placeBookmark(scope, chatId, itemId);
|
|
54
|
+
onName?.();
|
|
55
|
+
}, children: [_jsx(Icon, { icon: BookmarkPlus, size: 14 }), " ", t('bookmarks.add')] }));
|
|
56
|
+
}
|
|
57
|
+
return (_jsxs(_Fragment, { children: [_jsxs(ContextMenu.Item, { className: "menu-item", onClick: () => {
|
|
58
|
+
useBookmarkNaming.getState().open(scope.keyOf(chatId), bookmark.itemId);
|
|
59
|
+
onName?.();
|
|
60
|
+
}, children: [_jsx(Icon, { icon: Pencil, size: 14 }), " ", t('bookmarks.rename')] }), _jsxs(ContextMenu.Item, { className: "menu-item", onClick: () => void removeBookmark(scope, chatId, bookmark), children: [_jsx(Icon, { icon: BookmarkX, size: 14 }), " ", t('bookmarks.remove')] })] }));
|
|
61
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChatFileChange } from '@ruimte/agent-contracts';
|
|
2
|
+
interface UnifiedDiffProps {
|
|
3
|
+
change: ChatFileChange;
|
|
4
|
+
overflow?: 'wrap' | 'scroll';
|
|
5
|
+
diffStyle?: 'unified' | 'split';
|
|
6
|
+
}
|
|
7
|
+
export default function UnifiedDiff({ change, overflow, diffStyle }: UnifiedDiffProps): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { PatchDiff } from '@pierre/diffs/react';
|
|
5
|
+
import { chatHost } from '../../host';
|
|
6
|
+
import { useDiffTheme } from './diff-theme';
|
|
7
|
+
/*
|
|
8
|
+
* A patch the CLI reported itself (Codex writes unified diffs). The renderer needs a file header
|
|
9
|
+
* to know what it is looking at, and a CLI that only sends hunks gets one from the path we know.
|
|
10
|
+
* A diff without hunks at all is shown as its own lines, so nothing is ever swallowed.
|
|
11
|
+
*/
|
|
12
|
+
const asPatch = (change) => {
|
|
13
|
+
if (!/^@@/m.test(change.diff)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (/^\+\+\+ /m.test(change.diff)) {
|
|
17
|
+
return change.diff;
|
|
18
|
+
}
|
|
19
|
+
const from = change.kind === 'add' ? '/dev/null' : `a/${change.path}`;
|
|
20
|
+
const to = change.kind === 'delete' ? '/dev/null' : `b/${change.path}`;
|
|
21
|
+
return `--- ${from}\n+++ ${to}\n${change.diff}`;
|
|
22
|
+
};
|
|
23
|
+
const lineClass = (line) => {
|
|
24
|
+
if (line.startsWith('+')) {
|
|
25
|
+
return 'text-term-green';
|
|
26
|
+
}
|
|
27
|
+
if (line.startsWith('-')) {
|
|
28
|
+
return 'text-term-red';
|
|
29
|
+
}
|
|
30
|
+
return 'text-text-muted';
|
|
31
|
+
};
|
|
32
|
+
export default function UnifiedDiff({ change, overflow = 'wrap', diffStyle = 'unified' }) {
|
|
33
|
+
const resolved = chatHost().code.useMode();
|
|
34
|
+
const theme = useDiffTheme();
|
|
35
|
+
const patch = useMemo(() => asPatch(change), [change]);
|
|
36
|
+
const options = useMemo(() => ({
|
|
37
|
+
theme,
|
|
38
|
+
themeType: resolved,
|
|
39
|
+
disableFileHeader: true,
|
|
40
|
+
diffStyle,
|
|
41
|
+
overflow,
|
|
42
|
+
hunkSeparators: 'simple'
|
|
43
|
+
}), [diffStyle, overflow, theme, resolved]);
|
|
44
|
+
if (patch === null) {
|
|
45
|
+
return (_jsx("pre", { className: "max-h-64 overflow-auto px-3 py-2 font-mono text-code select-text", children: change.diff
|
|
46
|
+
.replace(/\n$/, '')
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map((line, index) => (_jsx("div", { className: clsx(lineClass(line), 'whitespace-pre-wrap'), children: line }, index))) }));
|
|
49
|
+
}
|
|
50
|
+
return (_jsx("div", { className: "chat-diff select-text", children: _jsx(PatchDiff, { patch: patch, options: options }) }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useUploadThumbnail } from '../thumbnails';
|
|
3
|
+
const BOX = 'h-14 w-14 rounded-lg border border-border';
|
|
4
|
+
/* An image the composer holds, drawn from its thumbnail; an empty box of the same size stands in while that is made. */
|
|
5
|
+
export function UploadThumb({ upload }) {
|
|
6
|
+
const url = useUploadThumbnail(upload);
|
|
7
|
+
if (url === null) {
|
|
8
|
+
return _jsx("span", { role: "img", "aria-label": upload.name, className: `block bg-surface-sunken ${BOX}` });
|
|
9
|
+
}
|
|
10
|
+
return _jsx("img", { src: url, alt: upload.name, className: `object-cover ${BOX}` });
|
|
11
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const MENTION_TONE = "bg-accent-soft text-accent shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--accent)_25%,transparent)]";
|
|
2
|
+
export declare const SKILL_TONE = "bg-skill-soft text-skill shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--skill)_25%,transparent)]";
|
|
3
|
+
export declare const CHIP_IN_EDITOR = "box-decoration-clone rounded-md px-1 py-0.5";
|
|
4
|
+
export declare const CHIP_IN_MESSAGE = "inline-flex h-5 max-w-full items-center gap-1 rounded-md px-1.5 align-[-3px] text-xs/none font-medium";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/* An `@file` or `$skill` token drawn as a pill: the tone says which of the two it is, the shape
|
|
2
|
+
says where it is drawn. */
|
|
3
|
+
export const MENTION_TONE = 'bg-accent-soft text-accent shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--accent)_25%,transparent)]';
|
|
4
|
+
/* A skill is the same token shape as a file mention, in its own hue. */
|
|
5
|
+
export const SKILL_TONE = 'bg-skill-soft text-skill shadow-[inset_0_0_0_1px_color-mix(in_srgb,var(--skill)_25%,transparent)]';
|
|
6
|
+
/* The pill in the composer's editor, around the token's own characters: the editor places the caret
|
|
7
|
+
from what it drew, so padding is safe, and a chip that wraps keeps it on every fragment. */
|
|
8
|
+
export const CHIP_IN_EDITOR = 'box-decoration-clone rounded-md px-1 py-0.5';
|
|
9
|
+
/* The same pill in the transcript: a real inline box a shade smaller than the text around it, with
|
|
10
|
+
the icon in place of the sigil. */
|
|
11
|
+
export const CHIP_IN_MESSAGE = 'inline-flex h-5 max-w-full items-center gap-1 rounded-md px-1.5 align-[-3px] text-xs/none font-medium';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CodeToken {
|
|
2
|
+
content: string;
|
|
3
|
+
color?: string;
|
|
4
|
+
fontStyle?: number;
|
|
5
|
+
}
|
|
6
|
+
export type Tokenize<State> = (code: string, state: State | undefined) => {
|
|
7
|
+
lines: CodeToken[][];
|
|
8
|
+
state: State | undefined;
|
|
9
|
+
};
|
|
10
|
+
export declare class IncrementalLines<State> {
|
|
11
|
+
private readonly tokenize;
|
|
12
|
+
private source;
|
|
13
|
+
private lines;
|
|
14
|
+
private state;
|
|
15
|
+
constructor(tokenize: Tokenize<State>);
|
|
16
|
+
update(code: string, complete: boolean): CodeToken[][];
|
|
17
|
+
private reset;
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The lines of a code block that grows at the end. A line that ended keeps the tokens it got, and
|
|
3
|
+
* the grammar state after it is where the next line starts, so a delta tokenizes the lines that are
|
|
4
|
+
* new plus the one still being written, however long the block already is. The last line is always
|
|
5
|
+
* tokenized again, since the next delta may turn it into anything (the start of a comment, a string).
|
|
6
|
+
*/
|
|
7
|
+
export class IncrementalLines {
|
|
8
|
+
tokenize;
|
|
9
|
+
// The text of the lines that are kept: empty, or ending in a newline while the block grows.
|
|
10
|
+
source = '';
|
|
11
|
+
lines = [];
|
|
12
|
+
state = undefined;
|
|
13
|
+
constructor(tokenize) {
|
|
14
|
+
this.tokenize = tokenize;
|
|
15
|
+
}
|
|
16
|
+
/* The tokens of every line of `code`; `complete` says nothing more arrives, so the last line is kept too. */
|
|
17
|
+
update(code, complete) {
|
|
18
|
+
// A carriage return splits lines in ways a cut at newlines does not see, so that code starts over every time.
|
|
19
|
+
if (code.includes('\r')) {
|
|
20
|
+
this.reset();
|
|
21
|
+
return this.tokenize(code, undefined).lines;
|
|
22
|
+
}
|
|
23
|
+
const end = complete ? code.length : code.lastIndexOf('\n') + 1;
|
|
24
|
+
const stable = code.slice(0, end);
|
|
25
|
+
const continues = (this.source === '' || this.source.endsWith('\n')) && stable.startsWith(this.source);
|
|
26
|
+
if (!continues) {
|
|
27
|
+
this.reset();
|
|
28
|
+
}
|
|
29
|
+
const added = stable.slice(this.source.length);
|
|
30
|
+
if (added !== '') {
|
|
31
|
+
const result = this.tokenize(added.endsWith('\n') ? added.slice(0, -1) : added, this.state);
|
|
32
|
+
this.lines = [...this.lines, ...result.lines];
|
|
33
|
+
this.state = result.state;
|
|
34
|
+
this.source = stable;
|
|
35
|
+
}
|
|
36
|
+
if (complete) {
|
|
37
|
+
return this.lines;
|
|
38
|
+
}
|
|
39
|
+
return [...this.lines, ...this.tokenize(code.slice(end), this.state).lines];
|
|
40
|
+
}
|
|
41
|
+
reset() {
|
|
42
|
+
this.source = '';
|
|
43
|
+
this.lines = [];
|
|
44
|
+
this.state = undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CodeStreamingContext: import("react").Context<boolean>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { chatHost } from '../../host';
|
|
2
|
+
/* The Shiki theme code is drawn in under the app's current light or dark. */
|
|
3
|
+
export const useCodeTheme = () => {
|
|
4
|
+
const { code } = chatHost();
|
|
5
|
+
const mode = code.useMode();
|
|
6
|
+
const themes = code.useThemes();
|
|
7
|
+
return mode === 'dark' ? themes.dark : themes.light;
|
|
8
|
+
};
|
|
9
|
+
/* What to hand Shiki for a theme id: a theme the app ships itself, or the id of a bundled one for Shiki to load. */
|
|
10
|
+
export const shikiThemeOf = (id) => chatHost().code.custom.find((theme) => theme.name === id) ?? id;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { syntaxTree } from '@codemirror/language';
|
|
2
|
+
import { Facet, RangeSetBuilder, StateField } from '@codemirror/state';
|
|
3
|
+
import { Decoration, EditorView } from '@codemirror/view';
|
|
4
|
+
import { chipRanges } from '../../mentions';
|
|
5
|
+
import { CHIP_IN_EDITOR, MENTION_TONE, SKILL_TONE } from '../chips';
|
|
6
|
+
import { codeRanges } from './editor';
|
|
7
|
+
const NO_CHOICES = { mentions: [], skills: [] };
|
|
8
|
+
const choices = Facet.define({ combine: (values) => values[0] ?? NO_CHOICES });
|
|
9
|
+
const mentionMark = Decoration.mark({ class: `${MENTION_TONE} ${CHIP_IN_EDITOR}` });
|
|
10
|
+
const skillMark = Decoration.mark({ class: `${SKILL_TONE} ${CHIP_IN_EDITOR}` });
|
|
11
|
+
const chipMarks = (state) => {
|
|
12
|
+
const { mentions, skills } = state.facet(choices);
|
|
13
|
+
const builder = new RangeSetBuilder();
|
|
14
|
+
// `@a.ts` in backticks is code, not a mention.
|
|
15
|
+
for (const range of chipRanges(state.doc.toString(), mentions, skills, codeRanges(state))) {
|
|
16
|
+
builder.add(range.from, range.to, range.kind === 'skill' ? skillMark : mentionMark);
|
|
17
|
+
}
|
|
18
|
+
return builder.finish();
|
|
19
|
+
};
|
|
20
|
+
const chipField = StateField.define({
|
|
21
|
+
create: chipMarks,
|
|
22
|
+
// The parser finishes a long text after the edit that asked for it, and a code span it finds then unmakes a chip.
|
|
23
|
+
update: (marks, tr) => tr.docChanged || tr.startState.facet(choices) !== tr.state.facet(choices) || syntaxTree(tr.startState) !== syntaxTree(tr.state)
|
|
24
|
+
? chipMarks(tr.state)
|
|
25
|
+
: marks,
|
|
26
|
+
provide: (field) => EditorView.decorations.from(field)
|
|
27
|
+
});
|
|
28
|
+
/* Draws the chosen `@path` and `$name` tokens as chips; a new `ChipChoices` object is what redraws them. */
|
|
29
|
+
export const chipDecorations = (value) => [choices.of(value), chipField];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Language } from '@codemirror/language';
|
|
2
|
+
import { type EditorState, type Extension } from '@codemirror/state';
|
|
3
|
+
export declare const externalChange: import("@codemirror/state").AnnotationType<boolean>;
|
|
4
|
+
export declare const markdownLanguage: Language;
|
|
5
|
+
export interface CodeRange {
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
kind: 'inline' | 'fence';
|
|
9
|
+
}
|
|
10
|
+
export declare const codeRanges: (state: EditorState, from?: number, to?: number) => CodeRange[];
|
|
11
|
+
export declare const composerEditorExtensions: () => Extension;
|