@adecore/agents-react 0.23.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -11
- package/dist/agents/AccountDot.js +3 -1
- package/dist/agents/accounts.d.ts +3 -2
- package/dist/agents/accounts.js +5 -6
- package/dist/chat/actions.d.ts +2 -2
- package/dist/chat/actions.js +8 -5
- package/dist/chat/bookmarks.d.ts +5 -3
- package/dist/chat/bookmarks.js +13 -14
- package/dist/chat/chat-client.d.ts +1 -0
- package/dist/chat/chat-client.js +7 -0
- package/dist/chat/composer-send.d.ts +3 -0
- package/dist/chat/composer-send.js +22 -0
- package/dist/chat/forks.js +22 -5
- package/dist/chat/index.d.ts +31 -0
- package/dist/chat/index.js +28 -0
- package/dist/chat/logic/embedded-code.d.ts +8 -0
- package/dist/chat/logic/embedded-code.js +371 -0
- package/dist/chat/logic/fork.js +1 -0
- package/dist/chat/logic/goal.d.ts +6 -0
- package/dist/chat/logic/goal.js +26 -0
- package/dist/chat/logic/scrubber.d.ts +1 -1
- package/dist/chat/logic/scrubber.js +6 -5
- package/dist/chat/logic/thought.d.ts +3 -0
- package/dist/chat/logic/thought.js +35 -0
- package/dist/chat/logic/thread-cards.js +2 -0
- package/dist/chat/logic/timeline.d.ts +5 -0
- package/dist/chat/logic/timeline.js +22 -1
- package/dist/chat/logic/tool-catalog.d.ts +1 -0
- package/dist/chat/logic/tool-catalog.js +3 -1
- package/dist/chat/logic/tools.d.ts +14 -0
- package/dist/chat/logic/tools.js +57 -0
- package/dist/chat/preferences.d.ts +6 -0
- package/dist/chat/preferences.js +19 -3
- package/dist/chat/reading-width.d.ts +9 -0
- package/dist/chat/reading-width.js +27 -0
- package/dist/chat/send-key.d.ts +10 -0
- package/dist/chat/send-key.js +24 -0
- package/dist/chat/ui/BookmarkMarker.js +4 -2
- package/dist/chat/ui/BookmarkSubmenu.js +4 -3
- package/dist/chat/ui/ChatActivity.js +14 -10
- package/dist/chat/ui/ChatReferenceChip.js +3 -4
- package/dist/chat/ui/CodeBlock.d.ts +2 -1
- package/dist/chat/ui/CodeBlock.js +14 -96
- package/dist/chat/ui/Composer.d.ts +1 -1
- package/dist/chat/ui/Composer.js +117 -906
- package/dist/chat/ui/ComposerInput.js +5 -4
- package/dist/chat/ui/EditDiff.js +3 -2
- package/dist/chat/ui/GoalDock.d.ts +9 -0
- package/dist/chat/ui/GoalDock.js +63 -0
- package/dist/chat/ui/LimitState.js +5 -3
- package/dist/chat/ui/Markdown.d.ts +2 -1
- package/dist/chat/ui/Markdown.js +70 -31
- package/dist/chat/ui/MermaidBlock.d.ts +3 -0
- package/dist/chat/ui/MermaidBlock.js +73 -0
- package/dist/chat/ui/MessageActions.js +12 -7
- package/dist/chat/ui/Pickers.js +6 -5
- package/dist/chat/ui/PromptComposer.js +4 -3
- package/dist/chat/ui/RowTime.d.ts +3 -0
- package/dist/chat/ui/RowTime.js +12 -0
- package/dist/chat/ui/RunSettings.js +5 -4
- package/dist/chat/ui/ShellCommand.d.ts +3 -0
- package/dist/chat/ui/ShellCommand.js +110 -0
- package/dist/chat/ui/SubagentInfo.js +4 -3
- package/dist/chat/ui/SubagentStopButton.js +4 -3
- package/dist/chat/ui/SubagentTimeline.js +4 -1
- package/dist/chat/ui/Timeline.js +70 -40
- package/dist/chat/ui/TimelineMenu.d.ts +2 -1
- package/dist/chat/ui/TimelineMenu.js +18 -8
- package/dist/chat/ui/UnifiedDiff.js +3 -2
- package/dist/chat/ui/VisualFrame.js +6 -7
- package/dist/chat/ui/code-theme.d.ts +2 -1
- package/dist/chat/ui/code-theme.js +4 -4
- package/dist/chat/ui/composer/ComposerControls.d.ts +27 -0
- package/dist/chat/ui/composer/ComposerControls.js +34 -0
- package/dist/chat/ui/composer/ComposerMenu.d.ts +4 -0
- package/dist/chat/ui/composer/ComposerMenu.js +46 -0
- package/dist/chat/ui/composer/DraftParts.d.ts +18 -0
- package/dist/chat/ui/composer/DraftParts.js +25 -0
- package/dist/chat/ui/composer/QueuedMessages.d.ts +10 -0
- package/dist/chat/ui/composer/QueuedMessages.js +20 -0
- package/dist/chat/ui/composer/key-handler.d.ts +20 -0
- package/dist/chat/ui/composer/key-handler.js +150 -0
- package/dist/chat/ui/composer/keys.d.ts +7 -2
- package/dist/chat/ui/composer/keys.js +19 -9
- package/dist/chat/ui/composer/placeholder.d.ts +9 -0
- package/dist/chat/ui/composer/placeholder.js +38 -0
- package/dist/chat/ui/composer/use-chat-controls.d.ts +28 -0
- package/dist/chat/ui/composer/use-chat-controls.js +112 -0
- package/dist/chat/ui/composer/use-compaction-offer.d.ts +2 -0
- package/dist/chat/ui/composer/use-compaction-offer.js +23 -0
- package/dist/chat/ui/composer/use-composer-draft.d.ts +14 -0
- package/dist/chat/ui/composer/use-composer-draft.js +39 -0
- package/dist/chat/ui/composer/use-composer-menus.d.ts +52 -0
- package/dist/chat/ui/composer/use-composer-menus.js +232 -0
- package/dist/chat/ui/composer/use-composer-submit.d.ts +32 -0
- package/dist/chat/ui/composer/use-composer-submit.js +113 -0
- package/dist/chat/ui/composer/use-draft-files.d.ts +27 -0
- package/dist/chat/ui/composer/use-draft-files.js +111 -0
- package/dist/chat/ui/composer/use-notice.d.ts +6 -0
- package/dist/chat/ui/composer/use-notice.js +18 -0
- package/dist/chat/ui/composer/use-pending-requests.d.ts +2 -0
- package/dist/chat/ui/composer/use-pending-requests.js +22 -0
- package/dist/chat/ui/composer/use-prompt-history.d.ts +8 -0
- package/dist/chat/ui/composer/use-prompt-history.js +57 -0
- package/dist/chat/ui/composer/use-prompt-hold.d.ts +6 -0
- package/dist/chat/ui/composer/use-prompt-hold.js +48 -0
- package/dist/chat/ui/composer/use-take-back.d.ts +13 -0
- package/dist/chat/ui/composer/use-take-back.js +82 -0
- package/dist/chat/ui/diff/EditorEditDiff.js +4 -3
- package/dist/chat/ui/diff/LegacyDiffPool.js +3 -2
- package/dist/chat/ui/diff/LegacyEditDiff.js +3 -2
- package/dist/chat/ui/diff/LegacyUnifiedDiff.js +3 -2
- package/dist/chat/ui/diff/syntax-runtime.d.ts +2 -1
- package/dist/chat/ui/diff/syntax-runtime.js +9 -5
- package/dist/chat/ui/diff-syntax.d.ts +2 -2
- package/dist/chat/ui/diff-syntax.js +1 -2
- package/dist/chat/ui/diff-theme.js +10 -9
- package/dist/chat/ui/file-links.d.ts +1 -1
- package/dist/chat/ui/file-links.js +7 -5
- package/dist/chat/ui/highlighter.d.ts +27 -0
- package/dist/chat/ui/highlighter.js +151 -0
- package/dist/chat/ui/index.d.ts +67 -0
- package/dist/chat/ui/index.js +64 -0
- package/dist/chat/ui/intelligent-ui/UiReply.js +3 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +3 -2
- package/dist/chat/ui/intelligent-ui/use-ui-links.js +2 -2
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +4 -3
- package/dist/chat/ui/markdown-blocks.d.ts +8 -0
- package/dist/chat/ui/markdown-blocks.js +260 -12
- package/dist/chat/ui/mermaid.d.ts +23 -0
- package/dist/chat/ui/mermaid.js +93 -0
- package/dist/chat/ui/rows/ForksRow.js +5 -3
- package/dist/chat/ui/rows/GeneratedImageRow.js +6 -4
- package/dist/chat/ui/rows/MessageRows.d.ts +4 -0
- package/dist/chat/ui/rows/MessageRows.js +36 -13
- package/dist/chat/ui/rows/Rows.js +3 -1
- package/dist/chat/ui/rows/SubagentRow.js +3 -2
- package/dist/chat/ui/rows/VisualRow.js +3 -2
- package/dist/chat/ui/rows/WorkRows.d.ts +6 -3
- package/dist/chat/ui/rows/WorkRows.js +57 -20
- package/dist/chat/ui/rows/WorkflowRow.js +3 -2
- package/dist/chat/ui/timestamp-format.d.ts +1 -0
- package/dist/chat/ui/timestamp-format.js +2 -0
- package/dist/chat/ui/use-diff-source-tokens.js +6 -3
- package/dist/chat/ui/visual-theme.js +3 -2
- package/dist/host.d.ts +9 -6
- package/dist/host.js +19 -8
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/locales/en/agent-chat.d.ts +658 -0
- package/dist/locales/en/agent-chat.js +657 -0
- package/dist/locales/en/agent-prompts.d.ts +44 -0
- package/dist/locales/en/agent-prompts.js +43 -0
- package/dist/locales/en/agent-providers.d.ts +132 -0
- package/dist/locales/en/agent-providers.js +131 -0
- package/dist/locales/en/agent-usage.d.ts +132 -0
- package/dist/locales/en/agent-usage.js +131 -0
- package/dist/locales/nl/agent-chat.d.ts +658 -0
- package/dist/locales/nl/agent-chat.js +657 -0
- package/dist/locales/nl/agent-prompts.d.ts +44 -0
- package/dist/locales/nl/agent-prompts.js +43 -0
- package/dist/locales/nl/agent-providers.d.ts +132 -0
- package/dist/locales/nl/agent-providers.js +131 -0
- package/dist/locales/nl/agent-usage.d.ts +132 -0
- package/dist/locales/nl/agent-usage.js +131 -0
- package/dist/locales.js +9 -8
- package/dist/prompts/index.d.ts +11 -0
- package/dist/prompts/index.js +12 -0
- package/dist/prompts/logic/subjects.d.ts +2 -1
- package/dist/prompts/logic/subjects.js +6 -4
- package/dist/prompts/ui/ApprovalBody.js +6 -4
- package/dist/prompts/ui/PromptCard.js +5 -4
- package/dist/prompts/ui/PromptView.js +3 -2
- package/dist/prompts/ui/QuestionBody.js +3 -2
- package/dist/providers/AccountColors.js +2 -2
- package/dist/providers/AccountDetail.js +7 -6
- package/dist/providers/AccountVariables.js +3 -1
- package/dist/providers/AddAccountForm.js +3 -2
- package/dist/providers/ProvidersPane.js +4 -3
- package/dist/providers/account-actions.d.ts +4 -3
- package/dist/providers/account-actions.js +6 -7
- package/dist/providers/index.d.ts +16 -0
- package/dist/providers/index.js +17 -0
- package/dist/providers/provider-abilities.d.ts +2 -1
- package/dist/providers/provider-abilities.js +4 -0
- package/dist/state/chats.js +4 -2
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +2 -0
- package/dist/{prompts/logic/prompts.fixtures.d.ts → testing/prompts.d.ts} +1 -1
- package/dist/theme.css +13 -3
- package/dist/usage/UsageBreakdown.js +3 -2
- package/dist/usage/UsageLimits.js +4 -3
- package/dist/usage/UsagePage.js +3 -2
- package/dist/usage/index.d.ts +15 -0
- package/dist/usage/index.js +15 -0
- package/package.json +53 -1031
- package/src/agents/AccountDot.tsx +3 -1
- package/src/agents/accounts.ts +6 -6
- package/src/chat/actions.ts +8 -5
- package/src/chat/bookmarks.ts +23 -14
- package/src/chat/chat-client.ts +8 -0
- package/src/chat/composer-send.ts +27 -0
- package/src/chat/forks.ts +27 -6
- package/src/chat/index.ts +98 -0
- package/src/chat/logic/embedded-code.ts +409 -0
- package/src/chat/logic/fork.ts +1 -0
- package/src/chat/logic/goal.ts +33 -0
- package/src/chat/logic/scrubber.ts +6 -5
- package/src/chat/logic/thought.ts +40 -0
- package/src/chat/logic/thread-cards.ts +2 -0
- package/src/chat/logic/timeline.ts +25 -1
- package/src/chat/logic/tool-catalog.ts +5 -1
- package/src/chat/logic/tools.ts +76 -0
- package/src/chat/preferences.ts +26 -3
- package/src/chat/reading-width.ts +39 -0
- package/src/chat/send-key.ts +38 -0
- package/src/chat/ui/BookmarkMarker.tsx +4 -2
- package/src/chat/ui/BookmarkSubmenu.tsx +4 -3
- package/src/chat/ui/ChatActivity.tsx +14 -10
- package/src/chat/ui/ChatReferenceChip.tsx +3 -4
- package/src/chat/ui/CodeBlock.tsx +33 -124
- package/src/chat/ui/Composer.tsx +162 -1324
- package/src/chat/ui/ComposerInput.tsx +5 -4
- package/src/chat/ui/EditDiff.tsx +3 -2
- package/src/chat/ui/GoalDock.tsx +104 -0
- package/src/chat/ui/LimitState.tsx +5 -3
- package/src/chat/ui/Markdown.tsx +113 -39
- package/src/chat/ui/MermaidBlock.tsx +119 -0
- package/src/chat/ui/MessageActions.tsx +13 -8
- package/src/chat/ui/Pickers.tsx +8 -7
- package/src/chat/ui/PromptComposer.tsx +4 -3
- package/src/chat/ui/RowTime.tsx +21 -0
- package/src/chat/ui/RunSettings.tsx +9 -4
- package/src/chat/ui/ShellCommand.tsx +134 -0
- package/src/chat/ui/SubagentInfo.tsx +4 -3
- package/src/chat/ui/SubagentStopButton.tsx +4 -3
- package/src/chat/ui/SubagentTimeline.tsx +5 -1
- package/src/chat/ui/Timeline.tsx +167 -105
- package/src/chat/ui/TimelineMenu.tsx +42 -7
- package/src/chat/ui/UnifiedDiff.tsx +3 -2
- package/src/chat/ui/VisualFrame.tsx +6 -7
- package/src/chat/ui/code-theme.ts +4 -4
- package/src/chat/ui/composer/ComposerControls.tsx +147 -0
- package/src/chat/ui/composer/ComposerMenu.tsx +138 -0
- package/src/chat/ui/composer/DraftParts.tsx +86 -0
- package/src/chat/ui/composer/QueuedMessages.tsx +64 -0
- package/src/chat/ui/composer/key-handler.ts +174 -0
- package/src/chat/ui/composer/keys.ts +32 -10
- package/src/chat/ui/composer/placeholder.ts +48 -0
- package/src/chat/ui/composer/use-chat-controls.ts +134 -0
- package/src/chat/ui/composer/use-compaction-offer.ts +25 -0
- package/src/chat/ui/composer/use-composer-draft.ts +71 -0
- package/src/chat/ui/composer/use-composer-menus.ts +289 -0
- package/src/chat/ui/composer/use-composer-submit.ts +163 -0
- package/src/chat/ui/composer/use-draft-files.ts +135 -0
- package/src/chat/ui/composer/use-notice.ts +28 -0
- package/src/chat/ui/composer/use-pending-requests.ts +23 -0
- package/src/chat/ui/composer/use-prompt-history.ts +76 -0
- package/src/chat/ui/composer/use-prompt-hold.ts +58 -0
- package/src/chat/ui/composer/use-take-back.ts +91 -0
- package/src/chat/ui/diff/DiffFallbackBoundary.tsx +2 -2
- package/src/chat/ui/diff/EditorEditDiff.tsx +4 -3
- package/src/chat/ui/diff/LegacyDiffPool.tsx +3 -2
- package/src/chat/ui/diff/LegacyEditDiff.tsx +3 -2
- package/src/chat/ui/diff/LegacyUnifiedDiff.tsx +3 -2
- package/src/chat/ui/diff/syntax-runtime.ts +10 -5
- package/src/chat/ui/diff-syntax.ts +2 -2
- package/src/chat/ui/diff-theme.ts +10 -9
- package/src/chat/ui/file-links.ts +7 -5
- package/src/chat/ui/highlighter.ts +185 -0
- package/src/chat/ui/index.ts +109 -0
- package/src/chat/ui/intelligent-ui/UiReply.tsx +5 -4
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +3 -2
- package/src/chat/ui/intelligent-ui/use-ui-links.ts +2 -2
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +4 -3
- package/src/chat/ui/markdown-blocks.ts +300 -12
- package/src/chat/ui/mermaid.ts +118 -0
- package/src/chat/ui/rows/ForksRow.tsx +5 -3
- package/src/chat/ui/rows/GeneratedImageRow.tsx +6 -4
- package/src/chat/ui/rows/MessageRows.tsx +73 -18
- package/src/chat/ui/rows/Rows.tsx +14 -1
- package/src/chat/ui/rows/SubagentRow.tsx +3 -2
- package/src/chat/ui/rows/VisualRow.tsx +3 -2
- package/src/chat/ui/rows/WorkRows.tsx +148 -47
- package/src/chat/ui/rows/WorkflowRow.tsx +3 -2
- package/src/chat/ui/timestamp-format.ts +2 -0
- package/src/chat/ui/use-diff-source-tokens.ts +10 -4
- package/src/chat/ui/visual-theme.ts +3 -2
- package/src/host.ts +31 -15
- package/src/index.ts +49 -0
- package/src/locales/en/agent-chat.ts +657 -0
- package/src/locales/en/agent-prompts.ts +43 -0
- package/src/locales/en/agent-providers.ts +131 -0
- package/src/locales/en/agent-usage.ts +131 -0
- package/src/locales/nl/agent-chat.ts +657 -0
- package/src/locales/nl/agent-prompts.ts +43 -0
- package/src/locales/nl/agent-providers.ts +131 -0
- package/src/locales/nl/agent-usage.ts +132 -0
- package/src/locales.ts +9 -8
- package/src/prompts/index.ts +35 -0
- package/src/prompts/logic/subjects.ts +12 -4
- package/src/prompts/ui/ApprovalBody.tsx +6 -4
- package/src/prompts/ui/PromptCard.tsx +11 -4
- package/src/prompts/ui/PromptView.tsx +3 -2
- package/src/prompts/ui/QuestionBody.tsx +3 -2
- package/src/providers/AccountColors.tsx +2 -2
- package/src/providers/AccountDetail.tsx +13 -7
- package/src/providers/AccountVariables.tsx +3 -1
- package/src/providers/AddAccountForm.tsx +3 -2
- package/src/providers/ProvidersPane.tsx +4 -3
- package/src/providers/account-actions.ts +7 -7
- package/src/providers/index.ts +17 -0
- package/src/providers/provider-abilities.ts +6 -1
- package/src/state/chats.ts +4 -2
- package/src/state/index.ts +39 -0
- package/src/testing/index.ts +2 -0
- package/src/{prompts/logic/prompts.fixtures.ts → testing/prompts.ts} +1 -1
- package/src/theme.css +13 -3
- package/src/usage/UsageBreakdown.tsx +3 -2
- package/src/usage/UsageLimits.tsx +4 -3
- package/src/usage/UsagePage.tsx +3 -2
- package/src/usage/index.ts +16 -0
- package/dist/locales/en/agent-chat.json +0 -597
- package/dist/locales/en/agent-prompts.json +0 -43
- package/dist/locales/en/agent-providers.json +0 -131
- package/dist/locales/en/agent-usage.json +0 -131
- package/dist/locales/nl/agent-chat.json +0 -597
- package/dist/locales/nl/agent-prompts.json +0 -43
- package/dist/locales/nl/agent-providers.json +0 -131
- package/dist/locales/nl/agent-usage.json +0 -131
- package/src/locales/en/agent-chat.json +0 -597
- package/src/locales/en/agent-prompts.json +0 -43
- package/src/locales/en/agent-providers.json +0 -131
- package/src/locales/en/agent-usage.json +0 -131
- package/src/locales/nl/agent-chat.json +0 -597
- package/src/locales/nl/agent-prompts.json +0 -43
- package/src/locales/nl/agent-providers.json +0 -131
- package/src/locales/nl/agent-usage.json +0 -131
- /package/dist/{prompts/logic/prompts.fixtures.js → testing/prompts.js} +0 -0
package/src/chat/logic/tools.ts
CHANGED
|
@@ -32,6 +32,82 @@ export function toolSummary(name: string, input: unknown): string {
|
|
|
32
32
|
return '';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// The input keys whose value is a file a call is about.
|
|
36
|
+
const PATH_KEYS = new Set(['file_path', 'notebook_path']);
|
|
37
|
+
|
|
38
|
+
/* Whether the one line of a call is the path of a file, which a row cuts in the middle so the name stays. */
|
|
39
|
+
export function toolSummaryIsPath(name: string, input: unknown): boolean {
|
|
40
|
+
const known = toolEntry(name);
|
|
41
|
+
if (known === undefined || !isRecord(input)) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const key = known.summary.find((candidate) => str(input[candidate]) !== null);
|
|
45
|
+
return key !== undefined && PATH_KEYS.has(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* The command of a shell call, as it was run; null for any other call or one without a command. */
|
|
49
|
+
export function shellCommand(name: string, input: unknown): string | null {
|
|
50
|
+
if (toolEntry(name)?.shell !== true || !isRecord(input)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const command = str(input.command);
|
|
54
|
+
return command === null || command.trim() === '' ? null : command;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* What a call was asked to do, as the lines its opened row shows. */
|
|
58
|
+
export interface ToolCallLines {
|
|
59
|
+
/* The command of a shell call, whole. */
|
|
60
|
+
readonly command: string | null;
|
|
61
|
+
/* The rest of the input as key and value: a string as it is, anything else as compact JSON. */
|
|
62
|
+
readonly args: ReadonlyArray<readonly [key: string, value: string]>;
|
|
63
|
+
/* An input that is not an object, as text. */
|
|
64
|
+
readonly text: string | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A shell call's description is already the detail of its row.
|
|
68
|
+
const SHELL_SHOWN = new Set(['command', 'description']);
|
|
69
|
+
|
|
70
|
+
function valueText(value: unknown): string {
|
|
71
|
+
// An empty string can be the point of a call (clearing a field), so it shows as one.
|
|
72
|
+
return typeof value === 'string' && value !== '' ? value : (JSON.stringify(value) ?? String(value));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* The input of a call as lines: a shell command as itself, every other key as `key value`, null included. */
|
|
76
|
+
export function toolCallLines(name: string, input: unknown): ToolCallLines {
|
|
77
|
+
if (!isRecord(input)) {
|
|
78
|
+
const text = input === null || input === undefined || input === '' ? null : valueText(input);
|
|
79
|
+
return { command: null, args: [], text };
|
|
80
|
+
}
|
|
81
|
+
const command = shellCommand(name, input);
|
|
82
|
+
const args = Object.entries(input)
|
|
83
|
+
.filter(([key, value]) => value !== undefined && !(command !== null && SHELL_SHOWN.has(key)))
|
|
84
|
+
.map(([key, value]) => [key, valueText(value)] as const);
|
|
85
|
+
return { command, args, text: null };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* A failed call's output with the exit code its tool put on the first line ("Exit code 1"), as Claude Code's shell does. */
|
|
89
|
+
export function exitCodeOf(output: string): { code: number | null; rest: string } {
|
|
90
|
+
const match = /^Exit code (-?\d+)[ \t]*(?:\r?\n|$)/.exec(output);
|
|
91
|
+
if (match === null || Number(match[1]) === 0) {
|
|
92
|
+
return { code: null, rest: output };
|
|
93
|
+
}
|
|
94
|
+
return { code: Number(match[1]), rest: output.slice(match[0].length) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Whether an output says anything; whitespace alone does not. */
|
|
98
|
+
export function hasOutput(output: string | null): output is string {
|
|
99
|
+
return output !== null && output.trim() !== '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Whether opening a call shows anything: a diff, the input it was given or what it answered. */
|
|
103
|
+
export function hasToolBody(tool: ChatToolItem): boolean {
|
|
104
|
+
if (hasFileChanges(tool) || hasOutput(tool.output)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
const lines = toolCallLines(tool.name, tool.input);
|
|
108
|
+
return lines.command !== null || lines.args.length > 0 || lines.text !== null;
|
|
109
|
+
}
|
|
110
|
+
|
|
35
111
|
// What `GET /fs/file` will serve; a path with another suffix is not worth asking the host about.
|
|
36
112
|
const IMAGE_SUFFIXES = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'];
|
|
37
113
|
|
package/src/chat/preferences.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { chatStorageKey, chatStorageLegacyKeys, createChatStore, onChatStorageConfiguration } from '../storage';
|
|
2
2
|
import type { AgentKind, ChatPreferencesPayload, ModelSelection, ProviderInfo, ProviderAccounts, RuntimeMode } from '@adecore/agent-contracts';
|
|
3
3
|
import { persistedJson } from './persisted-json';
|
|
4
|
+
import { DEFAULT_READING_WIDTH, isChatReadingWidth, type ChatReadingWidth } from './reading-width';
|
|
5
|
+
import { DEFAULT_SEND_KEY, isChatSendKey, type ChatSendKey } from './send-key';
|
|
4
6
|
|
|
5
7
|
const storageKey = (): string => chatStorageKey('preferences');
|
|
6
8
|
|
|
@@ -14,6 +16,10 @@ export interface ChatPreferences {
|
|
|
14
16
|
terminalRuntimeMode: RuntimeMode;
|
|
15
17
|
/* Per machine, the account a new agent of each CLI starts under; an account id only means something on its own machine. */
|
|
16
18
|
accountByMachine: Record<string, Partial<Record<AgentKind, string>>>;
|
|
19
|
+
/* How wide a chat of its own reads. It never reaches a machine. */
|
|
20
|
+
readingWidth: ChatReadingWidth;
|
|
21
|
+
/* Which key sends from the composer. It never reaches a machine either. */
|
|
22
|
+
sendKey: ChatSendKey;
|
|
17
23
|
/* When a person last changed any of it; what settles whose pick a machine uses when several clients told it one. */
|
|
18
24
|
changedAt: number;
|
|
19
25
|
}
|
|
@@ -24,6 +30,8 @@ export const DEFAULT_CHAT_PREFERENCES: ChatPreferences = {
|
|
|
24
30
|
runtimeMode: 'full-access',
|
|
25
31
|
terminalRuntimeMode: 'full-access',
|
|
26
32
|
accountByMachine: {},
|
|
33
|
+
readingWidth: DEFAULT_READING_WIDTH,
|
|
34
|
+
sendKey: DEFAULT_SEND_KEY,
|
|
27
35
|
changedAt: 0
|
|
28
36
|
};
|
|
29
37
|
|
|
@@ -44,6 +52,8 @@ export function parseChatPreferences(raw: string | null): ChatPreferences {
|
|
|
44
52
|
runtimeMode: stored.runtimeMode ?? DEFAULT_CHAT_PREFERENCES.runtimeMode,
|
|
45
53
|
terminalRuntimeMode: stored.terminalRuntimeMode ?? DEFAULT_CHAT_PREFERENCES.terminalRuntimeMode,
|
|
46
54
|
accountByMachine: { ...stored.accountByMachine },
|
|
55
|
+
readingWidth: isChatReadingWidth(stored.readingWidth) ? stored.readingWidth : DEFAULT_READING_WIDTH,
|
|
56
|
+
sendKey: isChatSendKey(stored.sendKey) ? stored.sendKey : DEFAULT_SEND_KEY,
|
|
47
57
|
changedAt: typeof stored.changedAt === 'number' ? stored.changedAt : 0
|
|
48
58
|
};
|
|
49
59
|
} catch {
|
|
@@ -142,10 +152,13 @@ export function reloadChatPreferences(): void {
|
|
|
142
152
|
useChatPreferences.setState(storage.read(), true);
|
|
143
153
|
}
|
|
144
154
|
|
|
155
|
+
function store(preferences: ChatPreferences): void {
|
|
156
|
+
useChatPreferences.setState(preferences);
|
|
157
|
+
storage.write(preferences);
|
|
158
|
+
}
|
|
159
|
+
|
|
145
160
|
function write(preferences: ChatPreferences): void {
|
|
146
|
-
|
|
147
|
-
useChatPreferences.setState(next);
|
|
148
|
-
storage.write(next);
|
|
161
|
+
store({ ...preferences, changedAt: Date.now() });
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
/* The composer writes here on every change and the settings dialog too; both edit the same remembered default. */
|
|
@@ -153,6 +166,16 @@ export function rememberChatPreferences(patch: Partial<ChatPreferences>): void {
|
|
|
153
166
|
write({ ...useChatPreferences.getState(), ...patch });
|
|
154
167
|
}
|
|
155
168
|
|
|
169
|
+
/* The reading width leaves `changedAt` alone: it is no pick a machine weighs against another client's. */
|
|
170
|
+
export function rememberReadingWidth(readingWidth: ChatReadingWidth): void {
|
|
171
|
+
store({ ...useChatPreferences.getState(), readingWidth });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Leaves `changedAt` alone, like the reading width. */
|
|
175
|
+
export function rememberSendKey(sendKey: ChatSendKey): void {
|
|
176
|
+
store({ ...useChatPreferences.getState(), sendKey });
|
|
177
|
+
}
|
|
178
|
+
|
|
156
179
|
export function rememberChatSelection(provider: AgentKind, selection: ModelSelection): void {
|
|
157
180
|
const current = useChatPreferences.getState();
|
|
158
181
|
const previous = current.selectionByProvider[provider];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import i18next from 'i18next';
|
|
3
|
+
|
|
4
|
+
/* How wide a chat that is a view of its own lets its thread and composer run. A node on a canvas is narrower than any of them. */
|
|
5
|
+
export type ChatReadingWidth = 'narrow' | 'comfortable' | 'full';
|
|
6
|
+
|
|
7
|
+
export const CHAT_READING_WIDTHS: readonly ChatReadingWidth[] = ['narrow', 'comfortable', 'full'];
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_READING_WIDTH: ChatReadingWidth = 'comfortable';
|
|
10
|
+
|
|
11
|
+
// About 75 and 90 characters of the thread's text a line.
|
|
12
|
+
const MEASURES: Readonly<Record<ChatReadingWidth, number>> = {
|
|
13
|
+
narrow: 640,
|
|
14
|
+
comfortable: 768,
|
|
15
|
+
full: Number.POSITIVE_INFINITY
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function isChatReadingWidth(value: unknown): value is ChatReadingWidth {
|
|
19
|
+
return typeof value === 'string' && (CHAT_READING_WIDTHS as readonly string[]).includes(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* The widest the content column runs, in pixels; `full` is unbounded. */
|
|
23
|
+
export function readingMeasure(width: ChatReadingWidth): number {
|
|
24
|
+
return MEASURES[width];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* What `.chat-column-content` and `.chat-composer-content` read their width from. */
|
|
28
|
+
export function readingWidthStyle(width: ChatReadingWidth): CSSProperties {
|
|
29
|
+
const measure = MEASURES[width];
|
|
30
|
+
return { '--chat-measure': Number.isFinite(measure) ? `${measure}px` : '100%' } as CSSProperties;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readingWidthLabel(width: ChatReadingWidth): string {
|
|
34
|
+
return i18next.t(`agent-chat:readingWidth.${width}.label`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function readingWidthHint(width: ChatReadingWidth): string {
|
|
38
|
+
return i18next.t(`agent-chat:readingWidth.${width}.hint`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import i18next from 'i18next';
|
|
2
|
+
import { KEY_SHORTCUTS, shortcut, type Shortcut } from '@adecore/ui';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Which key sends a message: Enter, Enter until the message runs over more than one line and Mod+Enter
|
|
6
|
+
* after that, or Mod+Enter always. Where Enter does not send, it adds a line.
|
|
7
|
+
*/
|
|
8
|
+
export type ChatSendKey = 'enter' | 'mod-enter-multiline' | 'mod-enter';
|
|
9
|
+
|
|
10
|
+
export const CHAT_SEND_KEYS: readonly ChatSendKey[] = ['enter', 'mod-enter-multiline', 'mod-enter'];
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_SEND_KEY: ChatSendKey = 'enter';
|
|
13
|
+
|
|
14
|
+
/* Sends, then opens a new chat through the host's `openNewChat`; a key of the composer, for a menu or a list of keys to print. */
|
|
15
|
+
export const SEND_AND_NEW_SHORTCUT = /* @__PURE__ */ shortcut('Mod+Alt+Enter');
|
|
16
|
+
|
|
17
|
+
export function isChatSendKey(value: unknown): value is ChatSendKey {
|
|
18
|
+
return typeof value === 'string' && (CHAT_SEND_KEYS as readonly string[]).includes(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Whether Enter alone adds a line to a message, so only Mod+Enter sends it. */
|
|
22
|
+
export function modSends(sendKey: ChatSendKey, multiline: boolean): boolean {
|
|
23
|
+
return sendKey === 'mod-enter' || (sendKey === 'mod-enter-multiline' && multiline);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* The key that sends a message, as a hint prints it. */
|
|
27
|
+
export function sendShortcut(sendKey: ChatSendKey, multiline: boolean): Shortcut {
|
|
28
|
+
return modSends(sendKey, multiline) ? KEY_SHORTCUTS.modEnter : KEY_SHORTCUTS.enter;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* `apple` writes Mod as the Command sign, and as Ctrl elsewhere. */
|
|
32
|
+
export function sendKeyLabel(sendKey: ChatSendKey, apple: boolean): string {
|
|
33
|
+
return i18next.t(`agent-chat:sendKey.${sendKey}.label`, { mod: apple ? '⌘' : 'Ctrl' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sendKeyHint(sendKey: ChatSendKey, apple: boolean): string {
|
|
37
|
+
return i18next.t(`agent-chat:sendKey.${sendKey}.hint`, { mod: apple ? '⌘' : 'Ctrl' });
|
|
38
|
+
}
|
|
@@ -5,6 +5,7 @@ import { Bookmark, Pencil, X } from 'lucide-react';
|
|
|
5
5
|
import { CHAT_BOOKMARK_LIMITS, type ChatBookmark } from '@adecore/agent-contracts';
|
|
6
6
|
import { nameBookmark, removeBookmark, useBookmarkNaming } from '../bookmarks';
|
|
7
7
|
import { bookmarkLabel } from '../logic/bookmarks';
|
|
8
|
+
import { useChatHost } from '../../host';
|
|
8
9
|
import { useChatScope } from '../../scope';
|
|
9
10
|
import { ButtonGroup, Icon, IconButton, Input } from '@adecore/ui';
|
|
10
11
|
|
|
@@ -22,6 +23,7 @@ interface BookmarkMarkerProps {
|
|
|
22
23
|
export function BookmarkMarker({ chatId, itemId, bookmark }: BookmarkMarkerProps) {
|
|
23
24
|
const { t } = useTranslation('agent-chat');
|
|
24
25
|
const scope = useChatScope();
|
|
26
|
+
const host = useChatHost();
|
|
25
27
|
const chatKey = scope.keyOf(chatId);
|
|
26
28
|
const naming = useBookmarkNaming((s) => s.chatKey === chatKey && s.itemId === itemId);
|
|
27
29
|
|
|
@@ -38,7 +40,7 @@ export function BookmarkMarker({ chatId, itemId, bookmark }: BookmarkMarkerProps
|
|
|
38
40
|
onDone={(name) => {
|
|
39
41
|
useBookmarkNaming.getState().close();
|
|
40
42
|
if (name !== null) {
|
|
41
|
-
void nameBookmark(scope, chatId, itemId, name, bookmark?.name);
|
|
43
|
+
void nameBookmark(scope, host, chatId, itemId, name, bookmark?.name);
|
|
42
44
|
}
|
|
43
45
|
}}
|
|
44
46
|
/>
|
|
@@ -54,7 +56,7 @@ export function BookmarkMarker({ chatId, itemId, bookmark }: BookmarkMarkerProps
|
|
|
54
56
|
className="-ml-2 max-w-0 shrink-0 overflow-hidden opacity-0 group-hover/bookmark:ml-0 group-hover/bookmark:max-w-none group-hover/bookmark:opacity-100 group-has-focus-visible/bookmark:ml-0 group-has-focus-visible/bookmark:max-w-none group-has-focus-visible/bookmark:opacity-100"
|
|
55
57
|
>
|
|
56
58
|
<IconButton icon={Pencil} size="xs" label={t('bookmarks.rename')} onClick={() => useBookmarkNaming.getState().open(chatKey, itemId)} />
|
|
57
|
-
<IconButton icon={X} size="xs" label={t('bookmarks.remove')} onClick={() => void removeBookmark(scope, chatId, bookmark)} />
|
|
59
|
+
<IconButton icon={X} size="xs" label={t('bookmarks.remove')} onClick={() => void removeBookmark(scope, host, chatId, bookmark)} />
|
|
58
60
|
</ButtonGroup>
|
|
59
61
|
)}
|
|
60
62
|
</div>
|
|
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
4
4
|
import { Bookmark, Pencil, X } from 'lucide-react';
|
|
5
5
|
import { goToBookmark, removeBookmark } from '../bookmarks';
|
|
6
6
|
import { bookmarkLabel, bookmarksInThreadOrder } from '../logic/bookmarks';
|
|
7
|
-
import {
|
|
7
|
+
import { useChatHost } from '../../host';
|
|
8
8
|
import { useChatScope } from '../../scope';
|
|
9
9
|
import { useChatRow } from '../../state/chats';
|
|
10
10
|
import { Icon, Menu } from '@adecore/ui';
|
|
@@ -15,7 +15,8 @@ export function BookmarkSubmenu({ chatId }: { chatId: string }) {
|
|
|
15
15
|
const scope = useChatScope();
|
|
16
16
|
const bookmarks = useChatRow(chatId, (row) => row?.bookmarks);
|
|
17
17
|
const order = useChatRow(chatId, (row) => row?.order);
|
|
18
|
-
const
|
|
18
|
+
const host = useChatHost();
|
|
19
|
+
const place = host.useChatPlace(chatId);
|
|
19
20
|
const sorted = useMemo(() => bookmarksInThreadOrder(bookmarks ?? [], order ?? []), [bookmarks, order]);
|
|
20
21
|
if (sorted.length === 0) {
|
|
21
22
|
return null;
|
|
@@ -40,7 +41,7 @@ export function BookmarkSubmenu({ chatId }: { chatId: string }) {
|
|
|
40
41
|
<span className={clsx('min-w-0 grow truncate', bookmark.name === undefined && 'text-text-muted')}>{bookmarkLabel(bookmark)}</span>
|
|
41
42
|
</Menu.Item>
|
|
42
43
|
<Menu.RowAction icon={Pencil} label={t('bookmarks.rename')} onClick={() => go(bookmark.itemId, true)} />
|
|
43
|
-
<Menu.RowAction icon={X} label={t('bookmarks.remove')} onClick={() => void removeBookmark(scope, chatId, bookmark)} />
|
|
44
|
+
<Menu.RowAction icon={X} label={t('bookmarks.remove')} onClick={() => void removeBookmark(scope, host, chatId, bookmark)} />
|
|
44
45
|
</Menu.Row>
|
|
45
46
|
))}
|
|
46
47
|
</Menu.Popup>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
+
import { useShallow } from 'zustand/shallow';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
import { Activity, MessageCircleQuestionMark, Square, SquareTerminal } from 'lucide-react';
|
|
5
6
|
import type { ChatBackgroundTask, ChatSubagentItem } from '@adecore/agent-contracts';
|
|
@@ -11,7 +12,7 @@ import { SubagentStopButton } from './SubagentStopButton';
|
|
|
11
12
|
import { formatElapsedShort } from '@adecore/ui/format';
|
|
12
13
|
import { Icon, IconButton, Popover, Surface, useNow } from '@adecore/ui';
|
|
13
14
|
import { useChatRow, useChats } from '../../state/chats';
|
|
14
|
-
import {
|
|
15
|
+
import { useChatHost } from '../../host';
|
|
15
16
|
import { useChatScope } from '../../scope';
|
|
16
17
|
import { useChatActions } from '../actions';
|
|
17
18
|
import { statusLookOf, type StatusWord } from '../../agents/status-look';
|
|
@@ -24,23 +25,24 @@ const ROW_BODY = 'flex min-h-9 min-w-0 grow items-center gap-2.5 rounded-md px-2
|
|
|
24
25
|
|
|
25
26
|
/* What the chat keeps working on beside the thread, in one small line over the composer: its sub-agents and what runs in the background. */
|
|
26
27
|
export function ChatActivity({ chatId }: { chatId: string }) {
|
|
28
|
+
const host = useChatHost();
|
|
27
29
|
const { t } = useTranslation('agent-chat');
|
|
28
30
|
const order = useChatRow(chatId, (row) => row?.order) ?? NO_ITEMS;
|
|
29
31
|
const structure = useChatRow(chatId, (row) => row?.structure);
|
|
30
32
|
const subagents = useMemo(() => (structure === undefined ? [] : flyoutSubagents(order, structure)), [order, structure]);
|
|
31
33
|
const { id, keyOf } = useChatScope();
|
|
32
|
-
const tasks =
|
|
33
|
-
|
|
34
|
+
const tasks = host.tasks.useTasks(id);
|
|
35
|
+
// The infos of these sub-agents' own chats, so the info of any other chat changing does not draw this again.
|
|
36
|
+
const children = useChats(useShallow((s) => subagents.map((item) => (item.childId === undefined ? undefined : s.statusByKey[keyOf(item.childId)]?.info))));
|
|
34
37
|
const progress = useMemo(
|
|
35
38
|
() =>
|
|
36
39
|
progressOf(
|
|
37
|
-
subagents.map((item) => {
|
|
40
|
+
subagents.map((item, index) => {
|
|
38
41
|
const taskId = taskIdOf(item);
|
|
39
|
-
|
|
40
|
-
return taskRowStateOf(item, taskId === null ? null : (tasks?.[taskId] ?? null), child);
|
|
42
|
+
return taskRowStateOf(item, taskId === null ? null : (tasks?.[taskId] ?? null), children[index]);
|
|
41
43
|
})
|
|
42
44
|
),
|
|
43
|
-
[subagents, tasks,
|
|
45
|
+
[subagents, tasks, children]
|
|
44
46
|
);
|
|
45
47
|
const words = useMemo(
|
|
46
48
|
() =>
|
|
@@ -55,7 +57,7 @@ export function ChatActivity({ chatId }: { chatId: string }) {
|
|
|
55
57
|
return null;
|
|
56
58
|
}
|
|
57
59
|
const { shells, monitors } = backgroundCounts(background);
|
|
58
|
-
const Slot =
|
|
60
|
+
const Slot = host.SubagentSlot;
|
|
59
61
|
const counting = progress.active > 0 && progress.total > 1;
|
|
60
62
|
const label = counting
|
|
61
63
|
? [t('activity.progress', { done: progress.done, total: progress.total }), progress.waiting > 0 && t('activity.needsYou', { count: progress.waiting })]
|
|
@@ -171,10 +173,11 @@ function SubagentRows({ chatId, items }: { chatId: string; items: readonly ChatS
|
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
function SubagentEntry({ chatId, item, now }: { chatId: string; item: ChatSubagentItem; now: number }) {
|
|
176
|
+
const host = useChatHost();
|
|
174
177
|
const { t } = useTranslation('agent-chat');
|
|
175
178
|
const { id, keyOf } = useChatScope();
|
|
176
179
|
const refused = useSubagentSupport((s) => s.unsupported[id] === true);
|
|
177
|
-
const task =
|
|
180
|
+
const task = host.tasks.useTask(id, taskIdOf(item));
|
|
178
181
|
const { show } = useSubagentTrail(chatId);
|
|
179
182
|
const child = useChats((s) => (item.childId === undefined ? undefined : s.statusByKey[keyOf(item.childId)]?.info));
|
|
180
183
|
const word = statusWordOf(item, task);
|
|
@@ -199,6 +202,7 @@ function SubagentEntry({ chatId, item, now }: { chatId: string; item: ChatSubage
|
|
|
199
202
|
|
|
200
203
|
/* Only drawn while the flyout is open, so the clock ticks for nobody otherwise. */
|
|
201
204
|
function BackgroundTaskRows({ chatId, tasks }: { chatId: string; tasks: readonly ChatBackgroundTask[] }) {
|
|
205
|
+
const host = useChatHost();
|
|
202
206
|
const { t } = useTranslation('agent-chat');
|
|
203
207
|
const now = useNow(1000);
|
|
204
208
|
const actions = useChatActions();
|
|
@@ -206,7 +210,7 @@ function BackgroundTaskRows({ chatId, tasks }: { chatId: string; tasks: readonly
|
|
|
206
210
|
function stop(taskId: string): void {
|
|
207
211
|
void actions.stopTask(chatId, taskId).catch((e: unknown) => {
|
|
208
212
|
const description = e instanceof Error ? e.message : t('subagents.noAnswer');
|
|
209
|
-
|
|
213
|
+
host.notify({ kind: 'error', title: t('activity.stopFailed'), description });
|
|
210
214
|
});
|
|
211
215
|
}
|
|
212
216
|
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { MessageSquare, X } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import { useChatHost } from '../../host';
|
|
5
5
|
import { CHIP_IN_MESSAGE, MENTION_TONE } from './chips';
|
|
6
6
|
import { Icon, Tooltip } from '@adecore/ui';
|
|
7
7
|
|
|
8
8
|
/* A chat attached to a message, under the title it has now rather than the one it had when it was attached. */
|
|
9
9
|
export function ChatReferenceChip({ chatId, onRemove }: { chatId: string; onRemove?: () => void }) {
|
|
10
|
+
const host = useChatHost();
|
|
10
11
|
const { t } = useTranslation('agent-chat');
|
|
11
|
-
const chat =
|
|
12
|
-
.useReferableChats()
|
|
13
|
-
.find((candidate) => candidate.id === chatId);
|
|
12
|
+
const chat = host.useReferableChats().find((candidate) => candidate.id === chatId);
|
|
14
13
|
const title = chat === undefined ? t('chatReference.gone') : chat.title || t('chatReference.untitled');
|
|
15
14
|
return (
|
|
16
15
|
<span className={clsx(MENTION_TONE, CHIP_IN_MESSAGE)}>
|
|
@@ -1,119 +1,23 @@
|
|
|
1
|
-
import { Fragment, memo, useContext, useEffect, useMemo, useRef, useState, type
|
|
1
|
+
import { Fragment, memo, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { Check, Copy } from 'lucide-react';
|
|
5
5
|
import { ButtonGroup, IconButton } from '@adecore/ui';
|
|
6
|
-
import
|
|
7
|
-
import { IncrementalLines, type CodeToken, type Tokenize } from './code-lines';
|
|
6
|
+
import { IncrementalLines, type CodeToken } from './code-lines';
|
|
8
7
|
import { CodeStreamingContext } from './code-streaming';
|
|
9
8
|
import { WHOLE_FADE_CLASS } from './rehype-fade';
|
|
10
|
-
import {
|
|
9
|
+
import { useChatHost } from '../../host';
|
|
10
|
+
import { useCodeTheme } from './code-theme';
|
|
11
|
+
import { tokenStyle, useCodeHighlighter } from './highlighter';
|
|
11
12
|
|
|
12
|
-
type Highlighter = Awaited<ReturnType<typeof createHighlighter>>;
|
|
13
|
-
type GrammarState = ReturnType<Highlighter['getLastGrammarState']>;
|
|
14
|
-
// A plain language needs no grammar; shiki draws it in the theme's own foreground.
|
|
15
|
-
const PLAIN = 'text';
|
|
16
13
|
const COPIED_MS = 1500;
|
|
17
14
|
|
|
18
|
-
let highlighter: Highlighter | null = null;
|
|
19
|
-
let highlighterLoad: Promise<Highlighter> | null = null;
|
|
20
|
-
const loadedLanguages = new Set<string>([PLAIN]);
|
|
21
|
-
const languageLoads = new Map<string, Promise<void>>();
|
|
22
|
-
const loadedThemes = new Set<string>();
|
|
23
|
-
const themeLoads = new Map<string, Promise<void>>();
|
|
24
|
-
const tokenizers = new Map<string, { tokenize: Tokenize<GrammarState>; fg: string | undefined }>();
|
|
25
|
-
|
|
26
|
-
// Shiki loads on the first code block, not with the app; the web bundle covers the languages a coding agent writes.
|
|
27
|
-
function loadHighlighter(): Promise<Highlighter> {
|
|
28
|
-
highlighterLoad ??= import('shiki/bundle/web').then(async ({ createHighlighter }) => {
|
|
29
|
-
highlighter = await createHighlighter({ themes: [], langs: [] });
|
|
30
|
-
return highlighter;
|
|
31
|
-
});
|
|
32
|
-
return highlighterLoad;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function resolveLanguage(lang: string, known: Record<string, unknown> | null): string {
|
|
36
|
-
return known !== null && lang in known ? lang : PLAIN;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let bundled: Record<string, unknown> | null = null;
|
|
40
|
-
|
|
41
|
-
/* Loads the highlighter and the grammar of one language, once each. */
|
|
42
|
-
function loadLanguage(lang: string): Promise<void> {
|
|
43
|
-
let load = languageLoads.get(lang);
|
|
44
|
-
if (!load) {
|
|
45
|
-
load = Promise.all([loadHighlighter(), import('shiki/bundle/web')]).then(async ([loaded, { bundledLanguages }]) => {
|
|
46
|
-
bundled = bundledLanguages;
|
|
47
|
-
const language = resolveLanguage(lang, bundledLanguages);
|
|
48
|
-
if (!loadedLanguages.has(language)) {
|
|
49
|
-
await loaded.loadLanguage(language as BundledLanguage);
|
|
50
|
-
loadedLanguages.add(language);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
languageLoads.set(lang, load);
|
|
54
|
-
}
|
|
55
|
-
return load;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/* Loads a theme into the highlighter, once. */
|
|
59
|
-
function loadTheme(theme: string): Promise<void> {
|
|
60
|
-
let load = themeLoads.get(theme);
|
|
61
|
-
if (!load) {
|
|
62
|
-
load = loadHighlighter().then(async (loaded) => {
|
|
63
|
-
await loaded.loadTheme(shikiThemeOf(theme) as BundledTheme | ThemeRegistration);
|
|
64
|
-
loadedThemes.add(theme);
|
|
65
|
-
});
|
|
66
|
-
themeLoads.set(theme, load);
|
|
67
|
-
}
|
|
68
|
-
return load;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/* The tokenizer for a language in a theme, or null while shiki, the grammar or the theme is still on its way. */
|
|
72
|
-
function tokenizerFor(lang: string, theme: string): { tokenize: Tokenize<GrammarState>; fg: string | undefined } | null {
|
|
73
|
-
if (highlighter === null || bundled === null || !languageLoads.has(lang) || !loadedThemes.has(theme)) {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
const language = resolveLanguage(lang, bundled);
|
|
77
|
-
if (!loadedLanguages.has(language)) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
const key = `${language}:${theme}`;
|
|
81
|
-
let entry = tokenizers.get(key);
|
|
82
|
-
if (!entry) {
|
|
83
|
-
const loaded = highlighter;
|
|
84
|
-
const options = { lang: language as BundledLanguage, theme };
|
|
85
|
-
entry = {
|
|
86
|
-
tokenize: (code, state) => {
|
|
87
|
-
const result = loaded.codeToTokens(code, state ? { ...options, grammarState: state } : options);
|
|
88
|
-
return { lines: result.tokens, state: result.grammarState };
|
|
89
|
-
},
|
|
90
|
-
fg: loaded.getTheme(theme).fg
|
|
91
|
-
};
|
|
92
|
-
tokenizers.set(key, entry);
|
|
93
|
-
}
|
|
94
|
-
return entry;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function styleOf(token: CodeToken): CSSProperties | undefined {
|
|
98
|
-
const style = token.fontStyle ?? 0;
|
|
99
|
-
if (token.color === undefined && style <= 0) {
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
const decorations = [style & 4 ? 'underline' : '', style & 8 ? 'line-through' : ''].filter(Boolean).join(' ');
|
|
103
|
-
return {
|
|
104
|
-
color: token.color,
|
|
105
|
-
...(style & 1 ? { fontStyle: 'italic' } : {}),
|
|
106
|
-
...(style & 2 ? { fontWeight: 'bold' } : {}),
|
|
107
|
-
...(decorations ? { textDecoration: decorations } : {})
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
15
|
/* A line whose tokens did not change keeps its elements, so a selection in it survives the next delta. */
|
|
112
16
|
const CodeLine = memo(function CodeLine({ tokens }: { tokens: CodeToken[] }) {
|
|
113
17
|
return (
|
|
114
18
|
<span className="line">
|
|
115
19
|
{tokens.map((token, index) => (
|
|
116
|
-
<span key={index} style={
|
|
20
|
+
<span key={index} style={tokenStyle(token)}>
|
|
117
21
|
{token.content}
|
|
118
22
|
</span>
|
|
119
23
|
))}
|
|
@@ -124,17 +28,20 @@ const CodeLine = memo(function CodeLine({ tokens }: { tokens: CodeToken[] }) {
|
|
|
124
28
|
/*
|
|
125
29
|
* A fenced block, highlighted a line at a time. Until shiki and the grammar are there it holds its
|
|
126
30
|
* place invisibly, so bare code never turns colored. An open fence is tokenized as it grows, and
|
|
127
|
-
* closing it does not draw it again.
|
|
31
|
+
* closing it does not draw it again. `view` takes the place of the code, such as what the code draws,
|
|
32
|
+
* and Copy still copies the code.
|
|
128
33
|
*/
|
|
129
|
-
export function CodeBlock({ code, lang, actions }: { code: string; lang: string; actions?: ReactNode }) {
|
|
34
|
+
export function CodeBlock({ code, lang, actions, view }: { code: string; lang: string; actions?: ReactNode; view?: ReactNode }) {
|
|
130
35
|
const { t } = useTranslation('agent-chat');
|
|
131
36
|
const theme = useCodeTheme();
|
|
37
|
+
const { custom } = useChatHost().code;
|
|
38
|
+
const highlighter = useCodeHighlighter();
|
|
132
39
|
const streaming = useContext(CodeStreamingContext);
|
|
133
40
|
const [copyState, setCopyState] = useState<'idle' | 'copying' | 'copied' | 'failed'>('idle');
|
|
134
41
|
const copying = useRef(false);
|
|
135
42
|
// What loaded last, so a block whose theme changes draws again once the new one is in.
|
|
136
43
|
const [loaded, setLoaded] = useState<string | null>(null);
|
|
137
|
-
const tokenizer =
|
|
44
|
+
const tokenizer = highlighter.tokenizer(lang, theme);
|
|
138
45
|
// Only a block that had to wait fades in; one drawn highlighted from its first frame just stands there.
|
|
139
46
|
const [waited] = useState(tokenizer === null);
|
|
140
47
|
const lines = useMemo(() => (tokenizer ? new IncrementalLines(tokenizer.tokenize) : null), [tokenizer]);
|
|
@@ -170,34 +77,36 @@ export function CodeBlock({ code, lang, actions }: { code: string; lang: string;
|
|
|
170
77
|
return;
|
|
171
78
|
}
|
|
172
79
|
let cancelled = false;
|
|
173
|
-
Promise.all([loadLanguage(lang), loadTheme(theme)])
|
|
80
|
+
Promise.all([highlighter.loadLanguage(lang), highlighter.loadTheme(theme, custom)])
|
|
174
81
|
.then(() => !cancelled && setLoaded(`${lang}:${theme}`))
|
|
175
82
|
.catch(() => !cancelled && setLoaded('failed'));
|
|
176
83
|
return () => {
|
|
177
84
|
cancelled = true;
|
|
178
85
|
};
|
|
179
|
-
}, [lang, theme, tokenizer]);
|
|
86
|
+
}, [highlighter, lang, theme, custom, tokenizer]);
|
|
180
87
|
|
|
181
88
|
const pending = (tokenizer === null || tokens === null) && loaded !== 'failed';
|
|
182
89
|
return (
|
|
183
90
|
<div className="chat-code" data-copy-state={copyState}>
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
91
|
+
{view ?? (
|
|
92
|
+
<pre
|
|
93
|
+
className={clsx(pending && 'invisible', !pending && tokens !== null && waited && WHOLE_FADE_CLASS)}
|
|
94
|
+
aria-hidden={pending || undefined}
|
|
95
|
+
style={{ color: tokenizer?.fg }}
|
|
96
|
+
>
|
|
97
|
+
<code>
|
|
98
|
+
{tokens === null
|
|
99
|
+
? code
|
|
100
|
+
: tokens.map((line, index) => (
|
|
101
|
+
// Lines only ever grow at the end, so the place of a line is a stable key.
|
|
102
|
+
<Fragment key={index}>
|
|
103
|
+
<CodeLine tokens={line} />
|
|
104
|
+
{index < tokens.length - 1 && '\n'}
|
|
105
|
+
</Fragment>
|
|
106
|
+
))}
|
|
107
|
+
</code>
|
|
108
|
+
</pre>
|
|
109
|
+
)}
|
|
201
110
|
<ButtonGroup className="chat-code-actions" role="group" aria-label={t('code.actions')}>
|
|
202
111
|
<IconButton
|
|
203
112
|
icon={copyState === 'copied' ? Check : Copy}
|