@adecore/agents-react 0.21.1 → 0.21.2
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/status-look.js +2 -3
- package/dist/chat/chat-client.d.ts +1 -1
- package/dist/chat/chat-client.js +2 -2
- package/dist/chat/drafts.js +6 -12
- package/dist/chat/logic/ansi.js +5 -1
- package/dist/chat/logic/fork.js +8 -9
- package/dist/chat/logic/limit.js +17 -14
- package/dist/chat/logic/resume-compaction.js +2 -11
- package/dist/chat/logic/scrubber.js +9 -18
- package/dist/chat/logic/timeline.js +15 -10
- package/dist/chat/logic/tool-catalog.js +3 -4
- package/dist/chat/mentions.js +5 -6
- package/dist/chat/runtime-modes.js +2 -4
- package/dist/chat/stash.js +1 -3
- package/dist/chat/thumbnails.js +3 -4
- package/dist/chat/timeline-scroll.js +39 -45
- package/dist/chat/ui/BookmarkSubmenu.js +1 -5
- package/dist/chat/ui/CodeBlock.js +2 -3
- package/dist/chat/ui/Composer.js +57 -69
- package/dist/chat/ui/Markdown.js +11 -11
- package/dist/chat/ui/Pickers.js +5 -9
- package/dist/chat/ui/QuoteButton.js +2 -3
- package/dist/chat/ui/RunSettings.js +8 -2
- package/dist/chat/ui/Scrubber.js +2 -3
- package/dist/chat/ui/SubagentControls.js +1 -3
- package/dist/chat/ui/Timeline.js +15 -18
- package/dist/chat/ui/TimelineMenu.js +3 -9
- package/dist/chat/ui/UnifiedDiff.js +3 -5
- package/dist/chat/ui/VisualFrame.js +3 -4
- package/dist/chat/ui/composer/keys.js +19 -11
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +1 -1
- package/dist/chat/ui/rows/MessageRows.js +2 -3
- package/dist/chat/ui/rows/WorkRows.d.ts +4 -3
- package/dist/chat/ui/rows/WorkRows.js +19 -14
- package/dist/chat/visual-viewport.d.ts +1 -1
- package/dist/chat/visual-viewport.js +9 -8
- package/dist/port-transport.js +4 -8
- package/dist/state/chats.js +10 -14
- package/dist/state/usage.js +44 -49
- package/dist/storage.js +7 -7
- package/dist/theme.css +6 -14
- package/dist/usage/LimitsList.js +1 -2
- package/dist/usage/UsageBreakdown.js +1 -3
- package/dist/usage/UsageDialog.js +2 -4
- package/dist/usage/format.js +6 -6
- package/dist/usage/money.js +1 -5
- package/dist/usage/summary.js +2 -4
- package/package.json +4 -4
- package/src/agents/status-look.ts +2 -3
- package/src/chat/chat-client.ts +2 -3
- package/src/chat/drafts.ts +6 -14
- package/src/chat/logic/ansi.ts +5 -1
- package/src/chat/logic/fork.ts +8 -9
- package/src/chat/logic/limit.ts +17 -16
- package/src/chat/logic/resume-compaction.ts +2 -11
- package/src/chat/logic/scrubber.ts +13 -17
- package/src/chat/logic/timeline.ts +22 -11
- package/src/chat/logic/tool-catalog.ts +7 -6
- package/src/chat/mentions.ts +5 -6
- package/src/chat/runtime-modes.ts +2 -4
- package/src/chat/stash.ts +1 -3
- package/src/chat/thumbnails.ts +3 -4
- package/src/chat/timeline-scroll.ts +43 -52
- package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
- package/src/chat/ui/CodeBlock.tsx +2 -3
- package/src/chat/ui/Composer.tsx +66 -86
- package/src/chat/ui/Markdown.tsx +13 -17
- package/src/chat/ui/Pickers.tsx +5 -9
- package/src/chat/ui/QuoteButton.tsx +2 -3
- package/src/chat/ui/RunSettings.tsx +9 -2
- package/src/chat/ui/Scrubber.tsx +2 -3
- package/src/chat/ui/SubagentControls.tsx +1 -3
- package/src/chat/ui/SubagentTimeline.tsx +1 -2
- package/src/chat/ui/Timeline.tsx +18 -20
- package/src/chat/ui/TimelineMenu.tsx +3 -9
- package/src/chat/ui/UnifiedDiff.tsx +3 -5
- package/src/chat/ui/VisualFrame.tsx +3 -4
- package/src/chat/ui/composer/keys.ts +21 -11
- package/src/chat/ui/intelligent-ui/render-context.ts +2 -2
- package/src/chat/ui/rehype-chips.ts +1 -1
- package/src/chat/ui/rehype-fade.ts +1 -1
- package/src/chat/ui/remark-html-as-text.ts +1 -1
- package/src/chat/ui/rows/MessageRows.tsx +3 -4
- package/src/chat/ui/rows/WorkRows.tsx +86 -69
- package/src/chat/visual-viewport.ts +10 -8
- package/src/host.ts +1 -3
- package/src/port-transport.ts +4 -8
- package/src/state/chats.ts +11 -14
- package/src/state/usage.ts +49 -60
- package/src/storage.ts +7 -7
- package/src/theme.css +6 -14
- package/src/usage/LimitsList.tsx +2 -4
- package/src/usage/UsageBreakdown.tsx +2 -5
- package/src/usage/UsageDialog.tsx +2 -4
- package/src/usage/format.ts +6 -6
- package/src/usage/money.ts +1 -5
- package/src/usage/summary.ts +2 -4
|
@@ -28,10 +28,9 @@ function personActsIn(frame: HTMLIFrameElement): boolean {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/*
|
|
31
|
-
* One visual's page in a frame
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* box keeps one height while the page loads and when it fails, so nothing under it moves.
|
|
31
|
+
* One visual's page in a frame that loads the app's sandbox host page; the bridge hands it the page and
|
|
32
|
+
* the theme. The frame never gets `allow-same-origin`, which keeps the page on an opaque origin even
|
|
33
|
+
* when the host page is served on the app's own. Its box keeps one height while it loads or fails.
|
|
35
34
|
*/
|
|
36
35
|
export function VisualFrame({ chatId, visual, fill = false, className }: VisualFrameProps) {
|
|
37
36
|
const { t } = useTranslation('agent-chat');
|
|
@@ -60,25 +60,35 @@ export function enterAction(keys: EnterKeys, { inOpenFence, list, column }: Ente
|
|
|
60
60
|
return list.empty ? 'leave-list' : 'continue-list';
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/*
|
|
64
|
-
|
|
63
|
+
/* The innermost fenced code block around `pos`, if any. */
|
|
64
|
+
function enclosingFence(tree: Tree, pos: number): SyntaxNode | null {
|
|
65
65
|
for (let node: SyntaxNode | null = tree.resolveInner(pos, -1); node !== null; node = node.parent) {
|
|
66
66
|
if (node.name === 'FencedCode') {
|
|
67
|
-
return
|
|
67
|
+
return node;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
return
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* A fence nobody has closed yet has its opening mark only. */
|
|
74
|
+
function isOpenFence(fence: SyntaxNode): boolean {
|
|
75
|
+
return fence.getChildren('CodeMark').length < 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Whether `pos` sits inside a fenced code block that has an opening fence and no closing one yet. */
|
|
79
|
+
export function inOpenFence(tree: Tree, pos: number): boolean {
|
|
80
|
+
const fence = enclosingFence(tree, pos);
|
|
81
|
+
return fence !== null && pos > fence.from && isOpenFence(fence);
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
/* Whether `pos` sits in the body of a fenced code block, open or closed: below its opening line and before its end. */
|
|
74
85
|
export function inFenceBody(tree: Tree, text: string, pos: number): boolean {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return openingEnd !== -1 && pos > openingEnd && (pos < node.to || node.getChildren('CodeMark').length < 2);
|
|
79
|
-
}
|
|
86
|
+
const fence = enclosingFence(tree, pos);
|
|
87
|
+
if (fence === null) {
|
|
88
|
+
return false;
|
|
80
89
|
}
|
|
81
|
-
|
|
90
|
+
const openingEnd = text.indexOf('\n', fence.from);
|
|
91
|
+
return openingEnd !== -1 && pos > openingEnd && (pos < fence.to || isOpenFence(fence));
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
/* The spaces Tab inserts at `column`, up to the next stop of four, the way a code editor lines up. */
|
|
@@ -97,7 +107,7 @@ export function inCode(tree: Tree, text: string, pos: number): boolean {
|
|
|
97
107
|
let blockFrom: number | null = null;
|
|
98
108
|
for (let node: SyntaxNode | null = tree.resolveInner(pos, -1); node !== null; node = node.parent) {
|
|
99
109
|
if (node.name === 'FencedCode') {
|
|
100
|
-
return pos > node.from && (pos < node.to || node
|
|
110
|
+
return pos > node.from && (pos < node.to || isOpenFence(node));
|
|
101
111
|
}
|
|
102
112
|
if (node.name === 'InlineCode' && pos > node.from && pos < node.to) {
|
|
103
113
|
return true;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createContext, type ReactNode } from 'react';
|
|
2
2
|
import type { UiComponentName, UiProps, UiViewNode } from '@adecore/intelligent-ui';
|
|
3
|
-
|
|
4
|
-
export type { UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
5
3
|
import type { UiLinkTarget } from '@adecore/intelligent-ui/links';
|
|
6
4
|
|
|
5
|
+
export type { UiLinkTarget };
|
|
6
|
+
|
|
7
7
|
/* How the host lets a link be drawn: a chip it can open, or plain text it cannot. */
|
|
8
8
|
export interface UiLink {
|
|
9
9
|
state: 'chip' | 'plain';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chipText, tokenizeChips, type ChipSegment } from '../mentions';
|
|
2
2
|
|
|
3
|
-
/* Just the part of hast this plugin touches; the package's own types are not a dependency
|
|
3
|
+
/* Just the part of hast this plugin touches; the package's own types are not a dependency here. */
|
|
4
4
|
interface HastText {
|
|
5
5
|
type: 'text';
|
|
6
6
|
value: string;
|
|
@@ -391,9 +391,8 @@ function OpenChatButton({ chatId }: { chatId: string }) {
|
|
|
391
391
|
}
|
|
392
392
|
|
|
393
393
|
/*
|
|
394
|
-
* The header of a turn the agent started itself, in place of
|
|
395
|
-
*
|
|
396
|
-
* "Show result". When the CLI named the sub-agent it woke up about, the header opens that row.
|
|
394
|
+
* The header of a turn the agent started itself, in place of a message of the person. What the
|
|
395
|
+
* sub-agent came back with lives on its own row, which the header opens when the CLI named it.
|
|
397
396
|
*/
|
|
398
397
|
export function AgentTurnRow({ label, onOpen }: { label: string; onOpen?: () => void }) {
|
|
399
398
|
const line = 'mb-0.5 flex h-7 w-full items-center gap-2 text-left text-xs text-text-muted';
|
|
@@ -430,7 +429,7 @@ export function CompactionRow({ preTokens }: { preTokens: number | null }) {
|
|
|
430
429
|
);
|
|
431
430
|
}
|
|
432
431
|
|
|
433
|
-
const DECISION_ICON: Record<Exclude<ChatApprovalItem['decision'], 'pending'>,
|
|
432
|
+
const DECISION_ICON: Record<Exclude<ChatApprovalItem['decision'], 'pending'>, ReactNode> = {
|
|
434
433
|
allow: <Icon icon={Check} size={12} />,
|
|
435
434
|
'allow-always': <Icon icon={Check} size={12} />,
|
|
436
435
|
deny: <Icon icon={X} size={12} />,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Suspense, useEffect, useState } from 'react';
|
|
1
|
+
import { Suspense, useEffect, useState, type Dispatch, type ReactNode, type SetStateAction } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import i18next from 'i18next';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
@@ -40,14 +40,14 @@ export function ToggleLine({
|
|
|
40
40
|
trailing,
|
|
41
41
|
className
|
|
42
42
|
}: {
|
|
43
|
-
icon:
|
|
43
|
+
icon: ReactNode;
|
|
44
44
|
label: string;
|
|
45
45
|
detail?: string;
|
|
46
46
|
open: boolean;
|
|
47
47
|
onToggle(): void;
|
|
48
48
|
failed?: boolean;
|
|
49
49
|
live?: boolean;
|
|
50
|
-
trailing?:
|
|
50
|
+
trailing?: ReactNode;
|
|
51
51
|
className?: string;
|
|
52
52
|
}) {
|
|
53
53
|
return (
|
|
@@ -75,24 +75,60 @@ export function ToggleLine({
|
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/* The diff renderers load lazily, so a diff waits on them under a line that says so. */
|
|
79
|
+
function DiffSuspense({ children }: { children: ReactNode }) {
|
|
79
80
|
const { t } = useTranslation('agent-chat');
|
|
81
|
+
return <Suspense fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>{children}</Suspense>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* A file in a changed files card, which opens its diff under it. */
|
|
85
|
+
function ChangedFileLine({
|
|
86
|
+
path,
|
|
87
|
+
open,
|
|
88
|
+
onToggle,
|
|
89
|
+
trailing,
|
|
90
|
+
children
|
|
91
|
+
}: {
|
|
92
|
+
path: string;
|
|
93
|
+
open: boolean;
|
|
94
|
+
onToggle(): void;
|
|
95
|
+
trailing: ReactNode;
|
|
96
|
+
children: ReactNode;
|
|
97
|
+
}) {
|
|
98
|
+
return (
|
|
99
|
+
<div className="border-t border-border">
|
|
100
|
+
<button
|
|
101
|
+
className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-text-muted hover:bg-surface-hover"
|
|
102
|
+
data-file-path={path}
|
|
103
|
+
onClick={onToggle}
|
|
104
|
+
>
|
|
105
|
+
<Icon icon={ChevronRight} size={12} className={clsx('shrink-0 text-text-faint transition-transform', open && 'rotate-90')} />
|
|
106
|
+
<span className="min-w-0 truncate font-mono text-text">{path}</span>
|
|
107
|
+
<span className="grow" />
|
|
108
|
+
{trailing}
|
|
109
|
+
</button>
|
|
110
|
+
{open && <div className="border-t border-border">{children}</div>}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ToolBody({ tool }: { tool: ChatToolItem }) {
|
|
80
116
|
const patches = unifiedChanges(tool);
|
|
81
117
|
const changes = patches.length > 0 ? [] : fileChanges(tool.name, tool.input);
|
|
82
118
|
return (
|
|
83
119
|
<div className="mt-1.5 mb-2 ml-6 overflow-hidden rounded-md border border-border bg-surface-raised">
|
|
84
120
|
{patches.length > 0 ? (
|
|
85
|
-
<
|
|
121
|
+
<DiffSuspense>
|
|
86
122
|
{patches.map((change, index) => (
|
|
87
123
|
<UnifiedDiff key={index} change={change} />
|
|
88
124
|
))}
|
|
89
|
-
</
|
|
125
|
+
</DiffSuspense>
|
|
90
126
|
) : changes.length > 0 ? (
|
|
91
|
-
<
|
|
127
|
+
<DiffSuspense>
|
|
92
128
|
{changes.map((change, index) => (
|
|
93
129
|
<EditDiff key={index} change={change} />
|
|
94
130
|
))}
|
|
95
|
-
</
|
|
131
|
+
</DiffSuspense>
|
|
96
132
|
) : (
|
|
97
133
|
<pre className="max-h-64 overflow-auto px-3 py-2 font-mono text-code text-text-muted select-text">
|
|
98
134
|
{clip(JSON.stringify(tool.input, null, 2) ?? '')}
|
|
@@ -113,9 +149,10 @@ function ToolBody({ tool }: { tool: ChatToolItem }) {
|
|
|
113
149
|
);
|
|
114
150
|
}
|
|
115
151
|
|
|
116
|
-
/*
|
|
117
|
-
|
|
118
|
-
|
|
152
|
+
/*
|
|
153
|
+
* One settled tool call: a line, and its input and output behind it. An image the call looked at is
|
|
154
|
+
* drawn under the line. `icon`, `label` and `failed` let a row that knows the call better name it.
|
|
155
|
+
*/
|
|
119
156
|
export function WorkRow({
|
|
120
157
|
tool,
|
|
121
158
|
nested,
|
|
@@ -127,7 +164,7 @@ export function WorkRow({
|
|
|
127
164
|
tool: ChatToolItem;
|
|
128
165
|
nested?: boolean;
|
|
129
166
|
detail?: string;
|
|
130
|
-
icon?:
|
|
167
|
+
icon?: ReactNode;
|
|
131
168
|
label?: string;
|
|
132
169
|
failed?: boolean;
|
|
133
170
|
}) {
|
|
@@ -235,14 +272,13 @@ function omittedLabel(reason: 'binary' | 'too-large'): string {
|
|
|
235
272
|
|
|
236
273
|
/* One file of a turn's checkpoint diff: its patch, or the reason there is none. */
|
|
237
274
|
function CheckpointFileBody({ file }: { file: ChatCheckpointFile }) {
|
|
238
|
-
const { t } = useTranslation('agent-chat');
|
|
239
275
|
if (file.omitted) {
|
|
240
276
|
return <div className="px-3 py-2 text-xs text-text-faint">{omittedLabel(file.omitted)}</div>;
|
|
241
277
|
}
|
|
242
278
|
return (
|
|
243
|
-
<
|
|
279
|
+
<DiffSuspense>
|
|
244
280
|
<UnifiedDiff change={file} />
|
|
245
|
-
</
|
|
281
|
+
</DiffSuspense>
|
|
246
282
|
);
|
|
247
283
|
}
|
|
248
284
|
|
|
@@ -261,11 +297,11 @@ function ChangedFilesCard({
|
|
|
261
297
|
deleted?: number;
|
|
262
298
|
/* A jump from outside the chat that asked for this card; each new one opens it. */
|
|
263
299
|
revealNonce?: number;
|
|
264
|
-
children:
|
|
300
|
+
children: ReactNode;
|
|
265
301
|
}) {
|
|
266
302
|
const { t } = useTranslation('agent-chat');
|
|
267
303
|
const [expanded, setExpanded] = useState(false);
|
|
268
|
-
// A jump
|
|
304
|
+
// A jump opens the card, also one that mounted it by scrolling it on screen; the flash is gone by the next visit.
|
|
269
305
|
const [seenReveal, setSeenReveal] = useState(0);
|
|
270
306
|
if (revealNonce > 0 && revealNonce !== seenReveal) {
|
|
271
307
|
setSeenReveal(revealNonce);
|
|
@@ -345,28 +381,20 @@ export function ChangedFilesRow({
|
|
|
345
381
|
deleted={files.reduce((sum, file) => sum + file.deleted, 0)}
|
|
346
382
|
>
|
|
347
383
|
{files.map((file) => (
|
|
348
|
-
<
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
<span className="text-chat-removed tabular-nums">-{file.deleted}</span>
|
|
363
|
-
</button>
|
|
364
|
-
{open[file.path] && (
|
|
365
|
-
<div className="border-t border-border">
|
|
366
|
-
<CheckpointFileBody file={file} />
|
|
367
|
-
</div>
|
|
368
|
-
)}
|
|
369
|
-
</div>
|
|
384
|
+
<ChangedFileLine
|
|
385
|
+
key={file.path}
|
|
386
|
+
path={file.path}
|
|
387
|
+
open={open[file.path] === true}
|
|
388
|
+
onToggle={() => setOpen((o) => ({ ...o, [file.path]: !o[file.path] }))}
|
|
389
|
+
trailing={
|
|
390
|
+
<>
|
|
391
|
+
<span className="text-chat-added tabular-nums">+{file.added}</span>
|
|
392
|
+
<span className="text-chat-removed tabular-nums">-{file.deleted}</span>
|
|
393
|
+
</>
|
|
394
|
+
}
|
|
395
|
+
>
|
|
396
|
+
<CheckpointFileBody file={file} />
|
|
397
|
+
</ChangedFileLine>
|
|
370
398
|
))}
|
|
371
399
|
</ChangedFilesCard>
|
|
372
400
|
);
|
|
@@ -384,7 +412,7 @@ function ProviderChangedFiles({
|
|
|
384
412
|
tools: ChatToolItem[];
|
|
385
413
|
open: Record<string, boolean>;
|
|
386
414
|
revealNonce: number;
|
|
387
|
-
setOpen:
|
|
415
|
+
setOpen: Dispatch<SetStateAction<Record<string, boolean>>>;
|
|
388
416
|
}) {
|
|
389
417
|
const { t } = useTranslation('agent-chat');
|
|
390
418
|
const byPath = new Map<string, { edits: FileChange[]; patches: ChatFileChange[] }>();
|
|
@@ -411,35 +439,24 @@ function ProviderChangedFiles({
|
|
|
411
439
|
}
|
|
412
440
|
return (
|
|
413
441
|
<ChangedFilesCard count={byPath.size} revealNonce={revealNonce}>
|
|
414
|
-
{[...byPath].map(([path, entry]) =>
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
<UnifiedDiff key={`patch-${index}`} change={change} />
|
|
433
|
-
))}
|
|
434
|
-
{entry.edits.map((change, index) => (
|
|
435
|
-
<EditDiff key={`edit-${index}`} change={change} />
|
|
436
|
-
))}
|
|
437
|
-
</Suspense>
|
|
438
|
-
</div>
|
|
439
|
-
)}
|
|
440
|
-
</div>
|
|
441
|
-
);
|
|
442
|
-
})}
|
|
442
|
+
{[...byPath].map(([path, entry]) => (
|
|
443
|
+
<ChangedFileLine
|
|
444
|
+
key={path}
|
|
445
|
+
path={path}
|
|
446
|
+
open={open[path] === true}
|
|
447
|
+
onToggle={() => setOpen((o) => ({ ...o, [path]: !o[path] }))}
|
|
448
|
+
trailing={<span className="text-text-faint">{t('work.edits', { count: entry.edits.length + entry.patches.length })}</span>}
|
|
449
|
+
>
|
|
450
|
+
<DiffSuspense>
|
|
451
|
+
{entry.patches.map((change, index) => (
|
|
452
|
+
<UnifiedDiff key={`patch-${index}`} change={change} />
|
|
453
|
+
))}
|
|
454
|
+
{entry.edits.map((change, index) => (
|
|
455
|
+
<EditDiff key={`edit-${index}`} change={change} />
|
|
456
|
+
))}
|
|
457
|
+
</DiffSuspense>
|
|
458
|
+
</ChangedFileLine>
|
|
459
|
+
))}
|
|
443
460
|
</ChangedFilesCard>
|
|
444
461
|
);
|
|
445
462
|
}
|
|
@@ -2,7 +2,7 @@ import type { VisualScrollRequest } from '@adecore/agent-contracts/visual';
|
|
|
2
2
|
import { visualViewportGeometry } from './logic/visual-viewport';
|
|
3
3
|
import type { VisualBridge } from './visual-bridge';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/* Keeps one small frame in a full-height timeline row, without rendering React on scroll. */
|
|
6
6
|
export class VisualViewportController {
|
|
7
7
|
private readonly box: HTMLElement;
|
|
8
8
|
private readonly viewport: HTMLElement;
|
|
@@ -40,11 +40,7 @@ export class VisualViewportController {
|
|
|
40
40
|
if (this.disposed) {
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
-
const
|
|
44
|
-
const box = this.box.getBoundingClientRect();
|
|
45
|
-
// A chat on a canvas can be scaled; messages inside its frame use unscaled CSS pixels.
|
|
46
|
-
const scale = outer.height / this.scroller.offsetHeight || 1;
|
|
47
|
-
const top = (box.top - outer.top) / scale - this.scroller.clientTop;
|
|
43
|
+
const top = this.offsetInScroller(this.scroller.getBoundingClientRect(), this.box.getBoundingClientRect());
|
|
48
44
|
const available = this.scroller.clientHeight;
|
|
49
45
|
const geometry = visualViewportGeometry(this.box.offsetHeight, available, top);
|
|
50
46
|
this.viewport.style.height = `${geometry.height}px`;
|
|
@@ -67,8 +63,7 @@ export class VisualViewportController {
|
|
|
67
63
|
return;
|
|
68
64
|
}
|
|
69
65
|
this.pendingDelta = 0;
|
|
70
|
-
const
|
|
71
|
-
const start = this.scroller.scrollTop + (box.top - outer.top) / scale - this.scroller.clientTop;
|
|
66
|
+
const start = this.scroller.scrollTop + this.offsetInScroller(outer, box);
|
|
72
67
|
const top = 'to' in request ? start + request.to : request.edge === 'start' ? 0 : this.scroller.scrollHeight;
|
|
73
68
|
this.scroller.scrollTo({ top, behavior: 'instant' });
|
|
74
69
|
this.refresh();
|
|
@@ -83,6 +78,13 @@ export class VisualViewportController {
|
|
|
83
78
|
this.bridge.setViewport(null);
|
|
84
79
|
}
|
|
85
80
|
|
|
81
|
+
/* The box's top below the scroller's visible top, in the scroller's own pixels. */
|
|
82
|
+
private offsetInScroller(outer: DOMRect, box: DOMRect): number {
|
|
83
|
+
// A chat on a canvas can be scaled; messages inside its frame use unscaled CSS pixels.
|
|
84
|
+
const scale = outer.height / this.scroller.offsetHeight || 1;
|
|
85
|
+
return (box.top - outer.top) / scale - this.scroller.clientTop;
|
|
86
|
+
}
|
|
87
|
+
|
|
86
88
|
private schedule(): void {
|
|
87
89
|
if (this.frame || this.disposed) {
|
|
88
90
|
return;
|
package/src/host.ts
CHANGED
|
@@ -134,7 +134,7 @@ export interface TimelineFind {
|
|
|
134
134
|
/* The dictation control beside the composer's editor; it takes its text from the editor itself. */
|
|
135
135
|
export interface ComposerDictationProps {
|
|
136
136
|
targetRef: RefObject<HTMLElement | null>;
|
|
137
|
-
/*
|
|
137
|
+
/* In the composer's row of round controls. */
|
|
138
138
|
buttonContainer: HTMLElement | null | undefined;
|
|
139
139
|
disabled: boolean;
|
|
140
140
|
editor(): EditorView | null;
|
|
@@ -239,7 +239,6 @@ export interface ChatHost {
|
|
|
239
239
|
/* The image a tool read, drawn under its row; null draws none. */
|
|
240
240
|
ReadImage: ComponentType<{ path: string }> | null;
|
|
241
241
|
code: {
|
|
242
|
-
/* Whether the app is light or dark right now. */
|
|
243
242
|
useMode(): 'light' | 'dark';
|
|
244
243
|
/* The Shiki theme ids code is drawn in, one per mode. */
|
|
245
244
|
useThemes(): { light: string; dark: string };
|
|
@@ -300,7 +299,6 @@ export interface ChatHost {
|
|
|
300
299
|
};
|
|
301
300
|
/* Offers a fork of the chat after one of its turns; null offers none. */
|
|
302
301
|
fork: ((chatId: string, turnId: string) => void) | null;
|
|
303
|
-
/* Opens the app's settings on a section: `providers`, or `agents`. */
|
|
304
302
|
openSettings(section: 'providers' | 'agents'): void;
|
|
305
303
|
/* Whether the host lets a chat go on by itself once a limit it stopped on lifts. */
|
|
306
304
|
useResumeAtReset(scopeId: string): boolean;
|
package/src/port-transport.ts
CHANGED
|
@@ -110,15 +110,11 @@ export function portTransport(port: FramePort): PortTransport {
|
|
|
110
110
|
return promise;
|
|
111
111
|
},
|
|
112
112
|
on<E extends AgentEventType>(event: E, handler: (payload: ChatEventMap[E]) => void): () => void {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
eventHandlers.set(event, handlers);
|
|
117
|
-
}
|
|
118
|
-
const set = handlers;
|
|
119
|
-
set.add(handler);
|
|
113
|
+
const handlers = eventHandlers.get(event) ?? new Set();
|
|
114
|
+
eventHandlers.set(event, handlers);
|
|
115
|
+
handlers.add(handler);
|
|
120
116
|
return () => {
|
|
121
|
-
|
|
117
|
+
handlers.delete(handler);
|
|
122
118
|
};
|
|
123
119
|
},
|
|
124
120
|
subscribeStatus(handler: (status: ChatTransportStatus) => void): () => void {
|
package/src/state/chats.ts
CHANGED
|
@@ -194,6 +194,15 @@ export function applyEvent(state: ChatState, event: ChatEvent): ChatState {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
/* Fields of a held chat that are not part of its thread; a chat this client does not hold stays as it is. */
|
|
198
|
+
function withFields(store: ChatsStore, key: string, fields: Pick<ChatState, 'bookmarks' | 'visuals'>): Partial<ChatsStore> {
|
|
199
|
+
const current = store.byKey[key];
|
|
200
|
+
if (!current) {
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
return { byKey: { ...store.byKey, [key]: { ...current, ...fields } } };
|
|
204
|
+
}
|
|
205
|
+
|
|
197
206
|
export const useChats = create<ChatsStore>((set) => ({
|
|
198
207
|
byKey: {},
|
|
199
208
|
statusByKey: {},
|
|
@@ -247,22 +256,10 @@ export const useChats = create<ChatsStore>((set) => ({
|
|
|
247
256
|
});
|
|
248
257
|
},
|
|
249
258
|
bookmarks(key, bookmarks) {
|
|
250
|
-
set((s) => {
|
|
251
|
-
const current = s.byKey[key];
|
|
252
|
-
if (!current) {
|
|
253
|
-
return {};
|
|
254
|
-
}
|
|
255
|
-
return { byKey: { ...s.byKey, [key]: { ...current, bookmarks } } };
|
|
256
|
-
});
|
|
259
|
+
set((s) => withFields(s, key, { bookmarks }));
|
|
257
260
|
},
|
|
258
261
|
visuals(key, visuals) {
|
|
259
|
-
set((s) => {
|
|
260
|
-
const current = s.byKey[key];
|
|
261
|
-
if (!current) {
|
|
262
|
-
return {};
|
|
263
|
-
}
|
|
264
|
-
return { byKey: { ...s.byKey, [key]: { ...current, visuals } } };
|
|
265
|
-
});
|
|
262
|
+
set((s) => withFields(s, key, { visuals }));
|
|
266
263
|
},
|
|
267
264
|
forget(key) {
|
|
268
265
|
set((s) => {
|