@adecore/agents-react 0.39.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.js +2 -1
- 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/GoalDock.js +2 -1
- package/dist/chat/ui/LimitState.js +4 -6
- 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.js +216 -253
- 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/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/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/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/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 +4 -6
- 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 +15 -0
- 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 +26 -0
- package/src/chat/ui/PromptComposer.tsx +2 -0
- package/src/chat/ui/PromptEditor.tsx +6 -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 +119 -0
- 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 +5 -0
- 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 +39 -0
- 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 +1 -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 +2 -0
- 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 +34 -14
- 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 +4 -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 +6 -4
- package/src/host.ts +2 -0
- package/src/index.ts +3 -0
- package/src/lazy.tsx +9 -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/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/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
package/src/chat/attachments.ts
CHANGED
|
@@ -29,10 +29,13 @@ export function isImageAttachment(mime: string): boolean {
|
|
|
29
29
|
/* The type on a file card: the extension, as long as it is short enough to read as one. */
|
|
30
30
|
export function fileBadge(name: string): string | null {
|
|
31
31
|
const dot = name.lastIndexOf('.');
|
|
32
|
+
|
|
32
33
|
if (dot <= 0 || dot === name.length - 1) {
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
36
|
+
|
|
35
37
|
const extension = name.slice(dot + 1);
|
|
38
|
+
|
|
36
39
|
return extension.length <= 5 ? extension.toUpperCase() : null;
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -46,6 +49,7 @@ export function checkAttachmentLimits<T extends IncomingFile>(current: number, i
|
|
|
46
49
|
const accepted: T[] = [];
|
|
47
50
|
const rejected: Array<{ name: string; reason: string }> = [];
|
|
48
51
|
let bytes = currentBytes;
|
|
52
|
+
|
|
49
53
|
for (const file of incoming) {
|
|
50
54
|
if (file.bytes === 0) {
|
|
51
55
|
rejected.push({ name: file.name, reason: i18next.t('agent-chat:attachments.rejected.empty') });
|
|
@@ -63,6 +67,7 @@ export function checkAttachmentLimits<T extends IncomingFile>(current: number, i
|
|
|
63
67
|
bytes += file.bytes;
|
|
64
68
|
}
|
|
65
69
|
}
|
|
70
|
+
|
|
66
71
|
return { accepted, rejected };
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -75,10 +80,12 @@ function readAsBase64(file: File): Promise<string> {
|
|
|
75
80
|
return new Promise((resolve, reject) => {
|
|
76
81
|
const reader = new FileReader();
|
|
77
82
|
reader.onerror = () => reject(reader.error ?? new Error(i18next.t('agent-chat:attachments.unreadable', { name: file.name })));
|
|
83
|
+
|
|
78
84
|
reader.onload = () => {
|
|
79
85
|
const url = String(reader.result);
|
|
80
86
|
resolve(url.slice(url.indexOf(',') + 1));
|
|
81
87
|
};
|
|
88
|
+
|
|
82
89
|
reader.readAsDataURL(file);
|
|
83
90
|
});
|
|
84
91
|
}
|
|
@@ -104,6 +111,7 @@ export async function readStoredAttachments(
|
|
|
104
111
|
stored: readonly ChatAttachment[]
|
|
105
112
|
): Promise<ChatAttachmentUpload[]> {
|
|
106
113
|
const files = await Promise.all(stored.map(async (attachment) => new File([await read(attachment)], attachment.name, { type: attachment.mime })));
|
|
114
|
+
|
|
107
115
|
return readAttachments(files);
|
|
108
116
|
}
|
|
109
117
|
|
package/src/chat/bookmarks.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { create } from 'zustand';
|
|
|
3
3
|
import type { ChatBookmark } from '@adecore/agent-contracts';
|
|
4
4
|
import type { ChatHost } from '../host';
|
|
5
5
|
import type { ChatScope } from '../scope';
|
|
6
|
+
import { errorMessage } from '../transport';
|
|
6
7
|
import { jumpToTimelineItem } from './timeline-scroll';
|
|
7
8
|
|
|
8
9
|
/* The one message in the window whose bookmark has its name field open, by the scope's key of its chat. */
|
|
@@ -23,15 +24,17 @@ export const useBookmarkNaming = create<BookmarkNaming>((set) => ({
|
|
|
23
24
|
type Notifier = Pick<ChatHost, 'notify'>;
|
|
24
25
|
|
|
25
26
|
function failed(host: Notifier, title: string, e: unknown): void {
|
|
26
|
-
host.notify({ title, description:
|
|
27
|
+
host.notify({ title, description: errorMessage(e), kind: 'error' });
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
async function add(scope: ChatScope, host: Notifier, chatId: string, itemId: string, name?: string): Promise<boolean> {
|
|
30
31
|
try {
|
|
31
32
|
await scope.chats.addBookmark(chatId, itemId, name);
|
|
33
|
+
|
|
32
34
|
return true;
|
|
33
35
|
} catch (e) {
|
|
34
36
|
failed(host, i18next.t('agent-chat:bookmarks.addFailed'), e);
|
|
37
|
+
|
|
35
38
|
return false;
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -39,6 +42,7 @@ async function add(scope: ChatScope, host: Notifier, chatId: string, itemId: str
|
|
|
39
42
|
/* Marks a message and opens the field for its name at once, before the host answered. */
|
|
40
43
|
export async function placeBookmark(scope: ChatScope, host: Notifier, chatId: string, itemId: string): Promise<void> {
|
|
41
44
|
useBookmarkNaming.getState().open(scope.keyOf(chatId), itemId);
|
|
45
|
+
|
|
42
46
|
if (!(await add(scope, host, chatId, itemId))) {
|
|
43
47
|
useBookmarkNaming.getState().close();
|
|
44
48
|
}
|
|
@@ -57,13 +61,17 @@ export async function nameBookmark(
|
|
|
57
61
|
previous: string | undefined
|
|
58
62
|
): Promise<void> {
|
|
59
63
|
const next = name.trim();
|
|
64
|
+
|
|
60
65
|
if (next === (previous ?? '')) {
|
|
61
66
|
return;
|
|
62
67
|
}
|
|
68
|
+
|
|
63
69
|
if (next !== '') {
|
|
64
70
|
await add(scope, host, chatId, itemId, next);
|
|
71
|
+
|
|
65
72
|
return;
|
|
66
73
|
}
|
|
74
|
+
|
|
67
75
|
try {
|
|
68
76
|
await scope.chats.renameBookmark(chatId, itemId, '');
|
|
69
77
|
} catch (e) {
|
|
@@ -77,8 +85,10 @@ export async function removeBookmark(scope: ChatScope, host: Notifier, chatId: s
|
|
|
77
85
|
await scope.chats.removeBookmark(chatId, bookmark.itemId);
|
|
78
86
|
} catch (e) {
|
|
79
87
|
failed(host, i18next.t('agent-chat:bookmarks.removeFailed'), e);
|
|
88
|
+
|
|
80
89
|
return;
|
|
81
90
|
}
|
|
91
|
+
|
|
82
92
|
host.notify({
|
|
83
93
|
title: i18next.t('agent-chat:bookmarks.removed'),
|
|
84
94
|
kind: 'deleted',
|
|
@@ -89,8 +99,10 @@ export async function removeBookmark(scope: ChatScope, host: Notifier, chatId: s
|
|
|
89
99
|
/* Scrolls the chat's thread to the message, and opens its name field for a rename. False when no thread of it is on screen yet. */
|
|
90
100
|
export function goToBookmark(scope: ChatScope, chatId: string, itemId: string, rename = false): boolean {
|
|
91
101
|
const key = scope.keyOf(chatId);
|
|
102
|
+
|
|
92
103
|
if (rename) {
|
|
93
104
|
useBookmarkNaming.getState().open(key, itemId);
|
|
94
105
|
}
|
|
106
|
+
|
|
95
107
|
return jumpToTimelineItem(key, itemId);
|
|
96
108
|
}
|
package/src/chat/chat-client.ts
CHANGED
|
@@ -78,10 +78,12 @@ export class ChatClient {
|
|
|
78
78
|
this.transport = transport;
|
|
79
79
|
this.sink = sink;
|
|
80
80
|
this.providers = providers;
|
|
81
|
+
|
|
81
82
|
this.unsubscribe.push(
|
|
82
83
|
transport.on('chat.event', ({ chatId, event, seq }) => {
|
|
83
84
|
const previous = this.sequence(chatId);
|
|
84
85
|
this.sink.apply(chatId, event);
|
|
86
|
+
|
|
85
87
|
if (previous !== undefined || (!this.sink.historyIdentity && this.mounted.get(chatId)?.attached)) {
|
|
86
88
|
this.rememberSequence(chatId, seq);
|
|
87
89
|
}
|
|
@@ -92,6 +94,7 @@ export class ChatClient {
|
|
|
92
94
|
transport.on('chat.visuals', ({ chatId, visuals }) => this.sink.visuals(chatId, visuals)),
|
|
93
95
|
transport.subscribeStatus((status) => this.onStatus(status))
|
|
94
96
|
);
|
|
97
|
+
|
|
95
98
|
if (transport.status === 'open') {
|
|
96
99
|
void this.loadProviders();
|
|
97
100
|
void this.loadStatuses();
|
|
@@ -100,39 +103,52 @@ export class ChatClient {
|
|
|
100
103
|
|
|
101
104
|
inspect(chatId: string): Promise<ChatAttachResult> {
|
|
102
105
|
const pending = this.inspections.get(chatId);
|
|
106
|
+
|
|
103
107
|
if (pending) {
|
|
104
108
|
return pending;
|
|
105
109
|
}
|
|
110
|
+
|
|
106
111
|
const read = this.transport.request('chat.attach', { chatId, historyLimit: 100 }).finally(async () => {
|
|
107
112
|
this.inspections.delete(chatId);
|
|
113
|
+
|
|
108
114
|
// A visible chat owns its attachment; an inspection must never release it.
|
|
109
115
|
if (!this.mounted.get(chatId)) {
|
|
110
116
|
await this.transport.request('chat.detach', { chatId }).catch(() => undefined);
|
|
111
117
|
}
|
|
112
118
|
});
|
|
119
|
+
|
|
113
120
|
this.inspections.set(chatId, read);
|
|
121
|
+
|
|
114
122
|
return read;
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
/* Shares a live attachment without creating a chat; a visible composer and several ancestors may hold it together. */
|
|
118
126
|
observe(chatId: string): () => void {
|
|
119
127
|
this.observers.set(chatId, (this.observers.get(chatId) ?? 0) + 1);
|
|
128
|
+
|
|
120
129
|
if (!this.mounted.has(chatId)) {
|
|
121
130
|
this.mounted.set(chatId, { attached: false, observerOnly: true });
|
|
122
131
|
void this.attach(chatId).catch(() => undefined);
|
|
123
132
|
}
|
|
133
|
+
|
|
124
134
|
let released = false;
|
|
135
|
+
|
|
125
136
|
return () => {
|
|
126
137
|
if (released) {
|
|
127
138
|
return;
|
|
128
139
|
}
|
|
140
|
+
|
|
129
141
|
released = true;
|
|
130
142
|
const remaining = (this.observers.get(chatId) ?? 1) - 1;
|
|
143
|
+
|
|
131
144
|
if (remaining > 0) {
|
|
132
145
|
this.observers.set(chatId, remaining);
|
|
146
|
+
|
|
133
147
|
return;
|
|
134
148
|
}
|
|
149
|
+
|
|
135
150
|
this.observers.delete(chatId);
|
|
151
|
+
|
|
136
152
|
if (this.mounted.get(chatId)?.observerOnly) {
|
|
137
153
|
void this.detach(chatId);
|
|
138
154
|
}
|
|
@@ -142,13 +158,16 @@ export class ChatClient {
|
|
|
142
158
|
/* Answers false when the transport is not connected; the chat opens once it is. */
|
|
143
159
|
async open(chatId: string, options: ChatOpenOptions): Promise<boolean> {
|
|
144
160
|
this.mounted.set(chatId, { ...options, attached: false });
|
|
161
|
+
|
|
145
162
|
try {
|
|
146
163
|
await this.attach(chatId);
|
|
164
|
+
|
|
147
165
|
return true;
|
|
148
166
|
} catch (e) {
|
|
149
167
|
if (isConnectionError(e)) {
|
|
150
168
|
return false;
|
|
151
169
|
}
|
|
170
|
+
|
|
152
171
|
throw e;
|
|
153
172
|
}
|
|
154
173
|
}
|
|
@@ -160,19 +179,23 @@ export class ChatClient {
|
|
|
160
179
|
*/
|
|
161
180
|
async retarget(chatId: string, provider: AgentKind, selection: ModelSelection): Promise<void> {
|
|
162
181
|
const entry = this.mounted.get(chatId);
|
|
182
|
+
|
|
163
183
|
if (!entry) {
|
|
164
184
|
return;
|
|
165
185
|
}
|
|
186
|
+
|
|
166
187
|
// The account belonged to the CLI the chat leaves, and the seq to its stream: the new chat's
|
|
167
188
|
// stream starts over, so offering it the old seq would read as nothing missed and reset nothing.
|
|
168
189
|
const { attached: _attached, account: _account, seq: _seq, observerOnly: _observerOnly, ...options } = entry;
|
|
169
190
|
this.rememberSequence(chatId, undefined);
|
|
170
191
|
this.mounted.delete(chatId);
|
|
171
192
|
await this.transport.request('chat.kill', { chatId });
|
|
193
|
+
|
|
172
194
|
try {
|
|
173
195
|
await this.open(chatId, { ...options, provider, selection });
|
|
174
196
|
} catch (e) {
|
|
175
197
|
this.sink.forget(chatId);
|
|
198
|
+
|
|
176
199
|
throw e;
|
|
177
200
|
}
|
|
178
201
|
}
|
|
@@ -188,14 +211,19 @@ export class ChatClient {
|
|
|
188
211
|
|
|
189
212
|
async detach(chatId: string): Promise<void> {
|
|
190
213
|
const entry = this.mounted.get(chatId);
|
|
214
|
+
|
|
191
215
|
if (entry && this.observers.has(chatId)) {
|
|
192
216
|
entry.observerOnly = true;
|
|
217
|
+
|
|
193
218
|
return;
|
|
194
219
|
}
|
|
220
|
+
|
|
195
221
|
this.mounted.delete(chatId);
|
|
222
|
+
|
|
196
223
|
if (!entry?.attached) {
|
|
197
224
|
return;
|
|
198
225
|
}
|
|
226
|
+
|
|
199
227
|
try {
|
|
200
228
|
await this.transport.request('chat.detach', { chatId });
|
|
201
229
|
} catch {
|
|
@@ -221,29 +249,34 @@ export class ChatClient {
|
|
|
221
249
|
/* Marks a message; the list every client of the chat holds comes back as `chat.bookmarks`. */
|
|
222
250
|
async addBookmark(chatId: string, itemId: string, name?: string): Promise<ChatBookmark[]> {
|
|
223
251
|
const { bookmarks } = await this.transport.request('chat.addBookmark', { chatId, itemId, ...(name === undefined ? {} : { name }) });
|
|
252
|
+
|
|
224
253
|
return bookmarks;
|
|
225
254
|
}
|
|
226
255
|
|
|
227
256
|
/* An empty name takes the name away. */
|
|
228
257
|
async renameBookmark(chatId: string, itemId: string, name: string): Promise<ChatBookmark[]> {
|
|
229
258
|
const { bookmarks } = await this.transport.request('chat.renameBookmark', { chatId, itemId, name });
|
|
259
|
+
|
|
230
260
|
return bookmarks;
|
|
231
261
|
}
|
|
232
262
|
|
|
233
263
|
async removeBookmark(chatId: string, itemId: string): Promise<ChatBookmark[]> {
|
|
234
264
|
const { bookmarks } = await this.transport.request('chat.removeBookmark', { chatId, itemId });
|
|
265
|
+
|
|
235
266
|
return bookmarks;
|
|
236
267
|
}
|
|
237
268
|
|
|
238
269
|
/* Takes a visual and its page out of the chat for good; the list every client of the chat holds comes back as `chat.visuals`. */
|
|
239
270
|
async removeVisual(chatId: string, visualId: string): Promise<ChatVisual[]> {
|
|
240
271
|
const { visuals } = await this.transport.request('chat.removeVisual', { chatId, visualId });
|
|
272
|
+
|
|
241
273
|
return visuals;
|
|
242
274
|
}
|
|
243
275
|
|
|
244
276
|
/* What this chat's CLI would run as a skill, for the composer's `$` picker. */
|
|
245
277
|
async listSkills(chatId: string): Promise<ChatSkill[]> {
|
|
246
278
|
const { skills } = await this.transport.request('skills.list', { chatId });
|
|
279
|
+
|
|
247
280
|
return skills;
|
|
248
281
|
}
|
|
249
282
|
|
|
@@ -273,6 +306,7 @@ export class ChatClient {
|
|
|
273
306
|
async loadStatuses(): Promise<void> {
|
|
274
307
|
try {
|
|
275
308
|
const { chats } = await this.transport.request('chat.list', {});
|
|
309
|
+
|
|
276
310
|
for (const info of chats) {
|
|
277
311
|
this.sink.status(info.chatId, info);
|
|
278
312
|
}
|
|
@@ -285,6 +319,7 @@ export class ChatClient {
|
|
|
285
319
|
if (!this.providers) {
|
|
286
320
|
return;
|
|
287
321
|
}
|
|
322
|
+
|
|
288
323
|
try {
|
|
289
324
|
const { providers } = await this.transport.request('provider.list', {});
|
|
290
325
|
this.providers.setProviders(providers);
|
|
@@ -299,9 +334,11 @@ export class ChatClient {
|
|
|
299
334
|
*/
|
|
300
335
|
loadEarlier(chatId: string, cursor: string): Promise<void> {
|
|
301
336
|
const running = this.earlier.get(chatId);
|
|
337
|
+
|
|
302
338
|
if (running) {
|
|
303
339
|
return running;
|
|
304
340
|
}
|
|
341
|
+
|
|
305
342
|
const read = this.transport
|
|
306
343
|
.request('chat.history', { chatId, cursor, limit: HISTORY_PAGE })
|
|
307
344
|
.then((page) => {
|
|
@@ -311,14 +348,18 @@ export class ChatClient {
|
|
|
311
348
|
})
|
|
312
349
|
.catch(async (e: unknown) => {
|
|
313
350
|
const entry = this.mounted.get(chatId);
|
|
351
|
+
|
|
314
352
|
if (errorCode(e) !== 'history-expired' || !entry) {
|
|
315
353
|
throw e;
|
|
316
354
|
}
|
|
355
|
+
|
|
317
356
|
this.rememberSequence(chatId, undefined);
|
|
318
357
|
await this.attach(chatId);
|
|
319
358
|
})
|
|
320
359
|
.finally(() => this.earlier.delete(chatId));
|
|
360
|
+
|
|
321
361
|
this.earlier.set(chatId, read);
|
|
362
|
+
|
|
322
363
|
return read;
|
|
323
364
|
}
|
|
324
365
|
|
|
@@ -331,9 +372,11 @@ export class ChatClient {
|
|
|
331
372
|
for (const off of this.unsubscribe) {
|
|
332
373
|
off();
|
|
333
374
|
}
|
|
375
|
+
|
|
334
376
|
this.unsubscribe.length = 0;
|
|
335
377
|
this.observers.clear();
|
|
336
378
|
this.checkpoints = new WeakMap();
|
|
379
|
+
|
|
337
380
|
for (const chatId of [...this.mounted.keys()]) {
|
|
338
381
|
void this.detach(chatId);
|
|
339
382
|
}
|
|
@@ -346,9 +389,11 @@ export class ChatClient {
|
|
|
346
389
|
*/
|
|
347
390
|
private async attach(chatId: string): Promise<void> {
|
|
348
391
|
const entry = this.mounted.get(chatId);
|
|
392
|
+
|
|
349
393
|
if (!entry) {
|
|
350
394
|
return;
|
|
351
395
|
}
|
|
396
|
+
|
|
352
397
|
if (!entry.observerOnly) {
|
|
353
398
|
await this.transport.request('chat.create', {
|
|
354
399
|
chatId,
|
|
@@ -360,26 +405,34 @@ export class ChatClient {
|
|
|
360
405
|
runtimeMode: entry.runtimeMode
|
|
361
406
|
});
|
|
362
407
|
}
|
|
408
|
+
|
|
363
409
|
// Closed or opened again in the meantime: an opening that is still there attaches itself.
|
|
364
410
|
if (this.mounted.get(chatId) !== entry) {
|
|
365
411
|
return;
|
|
366
412
|
}
|
|
413
|
+
|
|
367
414
|
const since = this.sequence(chatId);
|
|
368
415
|
const result = await this.transport.request('chat.attach', { chatId, historyLimit: HISTORY_PAGE, ...(since === undefined ? {} : { since }) });
|
|
369
416
|
const current = this.mounted.get(chatId);
|
|
417
|
+
|
|
370
418
|
if (current !== entry) {
|
|
371
419
|
// Its detach found nothing attached yet, and the host would stream this chat here until the socket closes.
|
|
372
420
|
if (!current) {
|
|
373
421
|
await this.transport.request('chat.detach', { chatId }).catch(() => undefined);
|
|
374
422
|
}
|
|
423
|
+
|
|
375
424
|
return;
|
|
376
425
|
}
|
|
426
|
+
|
|
377
427
|
// A cache eviction while the request was in flight also invalidates its replay base.
|
|
378
428
|
if (result.events && since !== undefined && this.sequence(chatId) === undefined) {
|
|
379
429
|
await this.attach(chatId);
|
|
430
|
+
|
|
380
431
|
return;
|
|
381
432
|
}
|
|
433
|
+
|
|
382
434
|
entry.attached = true;
|
|
435
|
+
|
|
383
436
|
if (result.events && since !== undefined) {
|
|
384
437
|
if (this.sink.replay) {
|
|
385
438
|
this.sink.replay(chatId, result.events);
|
|
@@ -391,8 +444,10 @@ export class ChatClient {
|
|
|
391
444
|
} else {
|
|
392
445
|
const page =
|
|
393
446
|
result.history === undefined ? undefined : { history: result.history, pending: result.pending ?? [], subagents: result.subagents ?? [] };
|
|
447
|
+
|
|
394
448
|
this.sink.reset(chatId, result.info, result.items, page);
|
|
395
449
|
}
|
|
450
|
+
|
|
396
451
|
this.rememberSequence(chatId, result.seq);
|
|
397
452
|
// A host without bookmarks or visuals sends none, and then this chat has none.
|
|
398
453
|
this.sink.bookmarks(chatId, result.bookmarks ?? []);
|
|
@@ -403,16 +458,21 @@ export class ChatClient {
|
|
|
403
458
|
if (!this.sink.historyIdentity) {
|
|
404
459
|
return this.mounted.get(chatId)?.seq;
|
|
405
460
|
}
|
|
461
|
+
|
|
406
462
|
const identity = this.sink.historyIdentity(chatId);
|
|
463
|
+
|
|
407
464
|
return identity === undefined ? undefined : this.checkpoints.get(identity);
|
|
408
465
|
}
|
|
409
466
|
|
|
410
467
|
private rememberSequence(chatId: string, seq: number | undefined): void {
|
|
411
468
|
const entry = this.mounted.get(chatId);
|
|
469
|
+
|
|
412
470
|
if (entry) {
|
|
413
471
|
entry.seq = seq;
|
|
414
472
|
}
|
|
473
|
+
|
|
415
474
|
const identity = this.sink.historyIdentity?.(chatId);
|
|
475
|
+
|
|
416
476
|
if (identity !== undefined) {
|
|
417
477
|
if (seq === undefined) {
|
|
418
478
|
this.checkpoints.delete(identity);
|
|
@@ -426,6 +486,7 @@ export class ChatClient {
|
|
|
426
486
|
if (this.preferences === null) {
|
|
427
487
|
return;
|
|
428
488
|
}
|
|
489
|
+
|
|
429
490
|
void this.transport.request('chat.setPreferences', this.preferences).catch(() => undefined);
|
|
430
491
|
}
|
|
431
492
|
|
|
@@ -435,8 +496,10 @@ export class ChatClient {
|
|
|
435
496
|
void this.loadProviders();
|
|
436
497
|
void this.loadStatuses();
|
|
437
498
|
void this.mounted.reattachAll((chatId) => this.attach(chatId));
|
|
499
|
+
|
|
438
500
|
return;
|
|
439
501
|
}
|
|
502
|
+
|
|
440
503
|
this.mounted.detachAll();
|
|
441
504
|
}
|
|
442
505
|
}
|
|
@@ -38,6 +38,7 @@ export function referablesOfChats(chats: readonly ProjectChat[]): Referables {
|
|
|
38
38
|
export function referenceTokens(ids: readonly string[], items: readonly Referable[], spelled: Readonly<Record<string, string>> = {}): ReferenceToken[] {
|
|
39
39
|
return [...new Set(ids)].flatMap((id) => {
|
|
40
40
|
const name = spelled[id] ?? items.find((item) => item.id === id)?.name;
|
|
41
|
+
|
|
41
42
|
return name === undefined || name === '' ? [] : [{ id, name }];
|
|
42
43
|
});
|
|
43
44
|
}
|
|
@@ -45,8 +46,10 @@ export function referenceTokens(ids: readonly string[], items: readonly Referabl
|
|
|
45
46
|
/* What a `#query` offers, the canvas first: never an id in `exclude`, and only chats where the machine takes no more. */
|
|
46
47
|
export function referenceSuggestions(referables: Referables, query: string, exclude: readonly string[], limit: number): Referable[] {
|
|
47
48
|
const needle = query.toLowerCase();
|
|
49
|
+
|
|
48
50
|
const matches = referables.items.filter(
|
|
49
51
|
(item) => item.name !== '' && (!referables.chatsOnly || item.kind === 'chat') && !exclude.includes(item.id) && item.name.toLowerCase().includes(needle)
|
|
50
52
|
);
|
|
53
|
+
|
|
51
54
|
return [...matches.filter((item) => item.group === 'canvas'), ...matches.filter((item) => item.group === 'views')].slice(0, limit);
|
|
52
55
|
}
|
|
@@ -11,8 +11,10 @@ const steppers = new Map<string, Stepper[]>();
|
|
|
11
11
|
|
|
12
12
|
export function registerEffortStepper(key: string, stepper: Stepper): () => void {
|
|
13
13
|
steppers.set(key, [...(steppers.get(key) ?? []), stepper]);
|
|
14
|
+
|
|
14
15
|
return () => {
|
|
15
16
|
const left = (steppers.get(key) ?? []).filter((entry) => entry !== stepper);
|
|
17
|
+
|
|
16
18
|
if (left.length > 0) {
|
|
17
19
|
steppers.set(key, left);
|
|
18
20
|
} else {
|
|
@@ -24,9 +26,12 @@ export function registerEffortStepper(key: string, stepper: Stepper): () => void
|
|
|
24
26
|
/* False when that chat has no composer on screen. At either end of the model's efforts the composer says so and nothing changes. */
|
|
25
27
|
export function stepEffortFromComposer(key: string, step: EffortStep): boolean {
|
|
26
28
|
const stepper = steppers.get(key)?.at(-1);
|
|
29
|
+
|
|
27
30
|
if (stepper === undefined) {
|
|
28
31
|
return false;
|
|
29
32
|
}
|
|
33
|
+
|
|
30
34
|
stepper(step);
|
|
35
|
+
|
|
31
36
|
return true;
|
|
32
37
|
}
|
|
@@ -9,6 +9,7 @@ const senders = new Map<string, (how: ComposerSend) => void>();
|
|
|
9
9
|
|
|
10
10
|
export function registerComposerSender(key: string, send: (how: ComposerSend) => void): () => void {
|
|
11
11
|
senders.set(key, send);
|
|
12
|
+
|
|
12
13
|
return () => {
|
|
13
14
|
if (senders.get(key) === send) {
|
|
14
15
|
senders.delete(key);
|
|
@@ -19,9 +20,12 @@ export function registerComposerSender(key: string, send: (how: ComposerSend) =>
|
|
|
19
20
|
/* False when that chat has no composer on screen. An empty or unsendable draft sends nothing, as with the keys. */
|
|
20
21
|
export function sendFromComposer(key: string, how: ComposerSend = 'default'): boolean {
|
|
21
22
|
const send = senders.get(key);
|
|
23
|
+
|
|
22
24
|
if (send === undefined) {
|
|
23
25
|
return false;
|
|
24
26
|
}
|
|
27
|
+
|
|
25
28
|
send(how);
|
|
29
|
+
|
|
26
30
|
return true;
|
|
27
31
|
}
|
package/src/chat/drafts.ts
CHANGED
|
@@ -57,21 +57,20 @@ export const useDrafts = createChatStore<{ ids: string[] }>(
|
|
|
57
57
|
(store) => store.setState({ ids: Object.keys(storage.read()) })
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
-
export function useHasDraft(chatId: string): boolean {
|
|
61
|
-
return useDrafts((s) => s.ids.includes(chatId));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
60
|
function trackDraft(chatId: string, held: boolean): void {
|
|
65
61
|
const { ids } = useDrafts.getState();
|
|
62
|
+
|
|
66
63
|
if (ids.includes(chatId) === held) {
|
|
67
64
|
return;
|
|
68
65
|
}
|
|
66
|
+
|
|
69
67
|
useDrafts.setState({ ids: held ? [...ids, chatId] : ids.filter((id) => id !== chatId) });
|
|
70
68
|
}
|
|
71
69
|
|
|
72
70
|
/* An unsent prompt per chat node, kept across reloads so a half-written message is never lost. */
|
|
73
71
|
export function readDraft(chatId: string): ChatDraft {
|
|
74
72
|
const record = storage.read()[chatId];
|
|
73
|
+
|
|
75
74
|
return record
|
|
76
75
|
? {
|
|
77
76
|
text: record.text,
|
|
@@ -90,37 +89,37 @@ export function readDraft(chatId: string): ChatDraft {
|
|
|
90
89
|
export function writeDraft(chatId: string, draft: ChatDraft): void {
|
|
91
90
|
const drafts = storage.read();
|
|
92
91
|
trackDraft(chatId, !isEmptyDraft(draft));
|
|
92
|
+
|
|
93
93
|
if (isEmptyDraft(draft)) {
|
|
94
94
|
delete drafts[chatId];
|
|
95
95
|
storage.write(drafts);
|
|
96
|
+
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
skills: draft.skills,
|
|
102
|
-
chats: draft.chats,
|
|
103
|
-
attachments: draft.attachments,
|
|
104
|
-
quote: draft.quote,
|
|
105
|
-
...(draft.quoteSource === null ? {} : { quoteSource: draft.quoteSource }),
|
|
106
|
-
references: draft.references,
|
|
107
|
-
...namesOf(draft)
|
|
108
|
-
};
|
|
99
|
+
|
|
100
|
+
drafts[chatId] = recordOf(draft, true);
|
|
101
|
+
|
|
109
102
|
if (storage.write(drafts)) {
|
|
110
103
|
return;
|
|
111
104
|
}
|
|
105
|
+
|
|
112
106
|
// A file can outgrow the storage quota; the text is the part worth keeping then.
|
|
113
|
-
drafts[chatId] =
|
|
107
|
+
drafts[chatId] = recordOf(draft, false);
|
|
108
|
+
storage.write(drafts);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function recordOf(draft: ChatDraft, withFiles: boolean): DraftRecord {
|
|
112
|
+
return {
|
|
114
113
|
text: draft.text,
|
|
115
114
|
mentions: draft.mentions,
|
|
116
115
|
skills: draft.skills,
|
|
117
116
|
chats: draft.chats,
|
|
117
|
+
...(withFiles ? { attachments: draft.attachments } : {}),
|
|
118
118
|
quote: draft.quote,
|
|
119
119
|
...(draft.quoteSource === null ? {} : { quoteSource: draft.quoteSource }),
|
|
120
120
|
references: draft.references,
|
|
121
121
|
...namesOf(draft)
|
|
122
122
|
};
|
|
123
|
-
storage.write(drafts);
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
function namesOf(draft: ChatDraft): Pick<DraftRecord, 'referenceNames'> {
|
|
@@ -150,6 +149,7 @@ export function takeBackIntoDraft(
|
|
|
150
149
|
taken.attachments.map((upload) => ({ name: upload.name, mime: upload.mime, bytes: uploadBytes(upload), upload })),
|
|
151
150
|
current.attachments.reduce((bytes, upload) => bytes + uploadBytes(upload), 0)
|
|
152
151
|
);
|
|
152
|
+
|
|
153
153
|
return {
|
|
154
154
|
draft: {
|
|
155
155
|
text: current.text.trim() === '' ? taken.text : joinDraftText(taken.text, current.text),
|
|
@@ -173,6 +173,7 @@ const takers = new Map<string, DraftTaker>();
|
|
|
173
173
|
|
|
174
174
|
export function takeDraftOffers(chatId: string, take: DraftTaker): () => void {
|
|
175
175
|
takers.set(chatId, take);
|
|
176
|
+
|
|
176
177
|
return () => {
|
|
177
178
|
if (takers.get(chatId) === take) {
|
|
178
179
|
takers.delete(chatId);
|
|
@@ -186,10 +187,13 @@ export function takeDraftOffers(chatId: string, take: DraftTaker): () => void {
|
|
|
186
187
|
*/
|
|
187
188
|
export function offerDraft(chatId: string, text: string): void {
|
|
188
189
|
const take = takers.get(chatId);
|
|
190
|
+
|
|
189
191
|
if (take) {
|
|
190
192
|
take(text);
|
|
193
|
+
|
|
191
194
|
return;
|
|
192
195
|
}
|
|
196
|
+
|
|
193
197
|
const current = readDraft(chatId);
|
|
194
198
|
writeDraft(chatId, { ...current, text: joinDraftText(current.text, text) });
|
|
195
199
|
}
|
package/src/chat/forks.ts
CHANGED
|
@@ -23,11 +23,13 @@ function joinedForks(
|
|
|
23
23
|
): (state: { statusByKey: ChatStatuses }) => string {
|
|
24
24
|
let statuses: ChatStatuses | null = null;
|
|
25
25
|
let joined = '';
|
|
26
|
+
|
|
26
27
|
return (state) => {
|
|
27
28
|
if (state.statusByKey !== statuses) {
|
|
28
29
|
statuses = state.statusByKey;
|
|
29
30
|
joined = [...collect(forkInfosOn(statuses, owns))].join('\n');
|
|
30
31
|
}
|
|
32
|
+
|
|
31
33
|
return joined;
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -41,6 +43,7 @@ export function useForkedTurns(chatId: string): ReadonlySet<string> {
|
|
|
41
43
|
const { owns } = useChatScope();
|
|
42
44
|
const select = joinedForks(owns, (infos) => forkedTurnIds(infos, chatId));
|
|
43
45
|
const joined = useChats(select);
|
|
46
|
+
|
|
44
47
|
return new Set(splitIds(joined));
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -49,5 +52,6 @@ export function useForkIdsAfter(chatId: string, turnId: string): string[] {
|
|
|
49
52
|
const { owns } = useChatScope();
|
|
50
53
|
const select = joinedForks(owns, (infos) => forkIdsAfter(infos, chatId, turnId));
|
|
51
54
|
const joined = useChats(select);
|
|
55
|
+
|
|
52
56
|
return splitIds(joined);
|
|
53
57
|
}
|
package/src/chat/guards.ts
CHANGED
|
@@ -23,9 +23,11 @@ export function pasteBecomesAttachment(text: string): boolean {
|
|
|
23
23
|
export function pastedTextName(taken: string[]): string {
|
|
24
24
|
const names = new Set(taken);
|
|
25
25
|
let index = 1;
|
|
26
|
+
|
|
26
27
|
while (names.has(`paste-${index}.txt`)) {
|
|
27
28
|
index += 1;
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
return `paste-${index}.txt`;
|
|
30
32
|
}
|
|
31
33
|
|