@adecore/agents-react 0.21.0 → 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.
Files changed (137) hide show
  1. package/dist/agents/status-look.js +2 -3
  2. package/dist/chat/chat-client.d.ts +1 -1
  3. package/dist/chat/chat-client.js +2 -2
  4. package/dist/chat/drafts.js +6 -12
  5. package/dist/chat/logic/ansi.js +5 -1
  6. package/dist/chat/logic/fork.js +8 -9
  7. package/dist/chat/logic/limit.js +17 -14
  8. package/dist/chat/logic/resume-compaction.js +2 -11
  9. package/dist/chat/logic/scrubber.js +9 -18
  10. package/dist/chat/logic/timeline.js +15 -10
  11. package/dist/chat/logic/tool-catalog.js +3 -4
  12. package/dist/chat/mentions.js +5 -6
  13. package/dist/chat/runtime-modes.js +2 -4
  14. package/dist/chat/stash.js +1 -3
  15. package/dist/chat/thumbnails.js +3 -4
  16. package/dist/chat/timeline-scroll.js +39 -45
  17. package/dist/chat/ui/BookmarkSubmenu.js +1 -5
  18. package/dist/chat/ui/CodeBlock.js +2 -3
  19. package/dist/chat/ui/Composer.js +57 -69
  20. package/dist/chat/ui/Markdown.js +11 -11
  21. package/dist/chat/ui/Pickers.js +5 -9
  22. package/dist/chat/ui/QuoteButton.js +2 -3
  23. package/dist/chat/ui/RunSettings.js +8 -2
  24. package/dist/chat/ui/Scrubber.js +2 -3
  25. package/dist/chat/ui/SubagentControls.js +1 -3
  26. package/dist/chat/ui/Timeline.js +15 -18
  27. package/dist/chat/ui/TimelineMenu.js +3 -9
  28. package/dist/chat/ui/UnifiedDiff.js +3 -5
  29. package/dist/chat/ui/VisualFrame.js +3 -4
  30. package/dist/chat/ui/composer/keys.js +19 -11
  31. package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +14 -1
  32. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +47 -5
  33. package/dist/chat/ui/intelligent-ui/UiReply.d.ts +3 -1
  34. package/dist/chat/ui/intelligent-ui/UiReply.js +34 -30
  35. package/dist/chat/ui/intelligent-ui/block-local.d.ts +9 -0
  36. package/dist/chat/ui/intelligent-ui/block-local.js +30 -1
  37. package/dist/chat/ui/intelligent-ui/node-text.d.ts +6 -0
  38. package/dist/chat/ui/intelligent-ui/node-text.js +54 -0
  39. package/dist/chat/ui/intelligent-ui/registry.js +2 -1
  40. package/dist/chat/ui/intelligent-ui/render-context.d.ts +4 -1
  41. package/dist/chat/ui/intelligent-ui/renderers/chart.js +5 -7
  42. package/dist/chat/ui/intelligent-ui/renderers/choices.js +6 -2
  43. package/dist/chat/ui/intelligent-ui/renderers/content.js +6 -13
  44. package/dist/chat/ui/intelligent-ui/renderers/data.js +8 -15
  45. package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +1 -0
  46. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +16 -4
  47. package/dist/chat/ui/intelligent-ui/renderers/links.js +2 -1
  48. package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +4 -4
  49. package/dist/chat/ui/intelligent-ui/use-ui-links.js +29 -10
  50. package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +2 -1
  51. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +34 -21
  52. package/dist/chat/ui/rows/MessageRows.js +3 -4
  53. package/dist/chat/ui/rows/WorkRows.d.ts +4 -3
  54. package/dist/chat/ui/rows/WorkRows.js +19 -14
  55. package/dist/chat/visual-viewport.d.ts +1 -1
  56. package/dist/chat/visual-viewport.js +9 -8
  57. package/dist/locales/en/agent-chat.json +23 -4
  58. package/dist/locales/nl/agent-chat.json +23 -4
  59. package/dist/port-transport.js +4 -8
  60. package/dist/state/chats.js +15 -15
  61. package/dist/state/usage.js +44 -49
  62. package/dist/storage.js +7 -7
  63. package/dist/theme.css +6 -14
  64. package/dist/usage/LimitsList.js +1 -2
  65. package/dist/usage/UsageBreakdown.js +1 -3
  66. package/dist/usage/UsageDialog.js +2 -4
  67. package/dist/usage/format.js +6 -6
  68. package/dist/usage/money.js +1 -5
  69. package/dist/usage/summary.js +2 -4
  70. package/package.json +24 -29
  71. package/src/agents/status-look.ts +2 -3
  72. package/src/chat/chat-client.ts +2 -3
  73. package/src/chat/drafts.ts +6 -14
  74. package/src/chat/logic/ansi.ts +5 -1
  75. package/src/chat/logic/fork.ts +8 -9
  76. package/src/chat/logic/limit.ts +17 -16
  77. package/src/chat/logic/resume-compaction.ts +2 -11
  78. package/src/chat/logic/scrubber.ts +13 -17
  79. package/src/chat/logic/timeline.ts +22 -11
  80. package/src/chat/logic/tool-catalog.ts +7 -6
  81. package/src/chat/mentions.ts +5 -6
  82. package/src/chat/runtime-modes.ts +2 -4
  83. package/src/chat/stash.ts +1 -3
  84. package/src/chat/thumbnails.ts +3 -4
  85. package/src/chat/timeline-scroll.ts +43 -52
  86. package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
  87. package/src/chat/ui/CodeBlock.tsx +2 -3
  88. package/src/chat/ui/Composer.tsx +66 -86
  89. package/src/chat/ui/Markdown.tsx +13 -17
  90. package/src/chat/ui/Pickers.tsx +5 -9
  91. package/src/chat/ui/QuoteButton.tsx +2 -3
  92. package/src/chat/ui/RunSettings.tsx +9 -2
  93. package/src/chat/ui/Scrubber.tsx +2 -3
  94. package/src/chat/ui/SubagentControls.tsx +1 -3
  95. package/src/chat/ui/SubagentTimeline.tsx +1 -2
  96. package/src/chat/ui/Timeline.tsx +18 -20
  97. package/src/chat/ui/TimelineMenu.tsx +3 -9
  98. package/src/chat/ui/UnifiedDiff.tsx +3 -5
  99. package/src/chat/ui/VisualFrame.tsx +3 -4
  100. package/src/chat/ui/composer/keys.ts +21 -11
  101. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +135 -5
  102. package/src/chat/ui/intelligent-ui/UiReply.tsx +48 -38
  103. package/src/chat/ui/intelligent-ui/block-local.ts +36 -1
  104. package/src/chat/ui/intelligent-ui/node-text.ts +60 -0
  105. package/src/chat/ui/intelligent-ui/registry.ts +2 -1
  106. package/src/chat/ui/intelligent-ui/render-context.ts +8 -2
  107. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +5 -7
  108. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +6 -2
  109. package/src/chat/ui/intelligent-ui/renderers/content.tsx +9 -14
  110. package/src/chat/ui/intelligent-ui/renderers/data.tsx +13 -19
  111. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +28 -5
  112. package/src/chat/ui/intelligent-ui/renderers/links.tsx +2 -1
  113. package/src/chat/ui/intelligent-ui/use-ui-links.ts +31 -11
  114. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +48 -22
  115. package/src/chat/ui/rehype-chips.ts +1 -1
  116. package/src/chat/ui/rehype-fade.ts +1 -1
  117. package/src/chat/ui/remark-html-as-text.ts +1 -1
  118. package/src/chat/ui/rows/MessageRows.tsx +4 -5
  119. package/src/chat/ui/rows/WorkRows.tsx +86 -69
  120. package/src/chat/visual-viewport.ts +10 -8
  121. package/src/host.ts +1 -3
  122. package/src/locales/en/agent-chat.json +23 -4
  123. package/src/locales/nl/agent-chat.json +23 -4
  124. package/src/port-transport.ts +4 -8
  125. package/src/state/chats.ts +16 -15
  126. package/src/state/usage.ts +49 -60
  127. package/src/storage.ts +7 -7
  128. package/src/theme.css +6 -14
  129. package/src/usage/LimitsList.tsx +2 -4
  130. package/src/usage/UsageBreakdown.tsx +2 -5
  131. package/src/usage/UsageDialog.tsx +2 -4
  132. package/src/usage/format.ts +6 -6
  133. package/src/usage/money.ts +1 -5
  134. package/src/usage/summary.ts +2 -4
  135. package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +0 -15
  136. package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +0 -50
  137. package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +0 -139
@@ -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: React.ReactNode;
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?: React.ReactNode;
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
- function ToolBody({ tool }: { tool: ChatToolItem }) {
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
- <Suspense fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
121
+ <DiffSuspense>
86
122
  {patches.map((change, index) => (
87
123
  <UnifiedDiff key={index} change={change} />
88
124
  ))}
89
- </Suspense>
125
+ </DiffSuspense>
90
126
  ) : changes.length > 0 ? (
91
- <Suspense fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
127
+ <DiffSuspense>
92
128
  {changes.map((change, index) => (
93
129
  <EditDiff key={index} change={change} />
94
130
  ))}
95
- </Suspense>
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
- /* One settled tool call: a line, and its input and output behind it. An image the call looked at
117
- is drawn under the line, because a picture says more about that read than its path does.
118
- `icon`, `label` and `failed` let a row that knows the call better name it in its own words. */
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?: React.ReactNode;
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
- <Suspense fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
279
+ <DiffSuspense>
244
280
  <UnifiedDiff change={file} />
245
- </Suspense>
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: React.ReactNode;
300
+ children: ReactNode;
265
301
  }) {
266
302
  const { t } = useTranslation('agent-chat');
267
303
  const [expanded, setExpanded] = useState(false);
268
- // A jump from outside opens the card, also when the jump put the row on screen and so mounted it; the flash is gone by the time the row is scrolled to again.
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
- <div key={file.path} className="border-t border-border">
349
- <button
350
- className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-text-muted hover:bg-surface-hover"
351
- data-file-path={file.path}
352
- onClick={() => setOpen((o) => ({ ...o, [file.path]: !o[file.path] }))}
353
- >
354
- <Icon
355
- icon={ChevronRight}
356
- size={12}
357
- className={clsx('shrink-0 text-text-faint transition-transform', open[file.path] && 'rotate-90')}
358
- />
359
- <span className="min-w-0 truncate font-mono text-text">{file.path}</span>
360
- <span className="grow" />
361
- <span className="text-chat-added tabular-nums">+{file.added}</span>
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: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
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
- const count = entry.edits.length + entry.patches.length;
416
- return (
417
- <div key={path} className="border-t border-border">
418
- <button
419
- className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-text-muted hover:bg-surface-hover"
420
- data-file-path={path}
421
- onClick={() => setOpen((o) => ({ ...o, [path]: !o[path] }))}
422
- >
423
- <Icon icon={ChevronRight} size={12} className={clsx('shrink-0 text-text-faint transition-transform', open[path] && 'rotate-90')} />
424
- <span className="min-w-0 truncate font-mono text-text">{path}</span>
425
- <span className="grow" />
426
- <span className="text-text-faint">{t('work.edits', { count })}</span>
427
- </button>
428
- {open[path] && (
429
- <div className="border-t border-border">
430
- <Suspense fallback={<div className="px-3 py-2 text-xs text-text-faint">{t('work.loadingDiff')}</div>}>
431
- {entry.patches.map((change, index) => (
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
- /** Keeps one small frame in a full-height timeline row, without rendering React on scroll. */
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 outer = this.scroller.getBoundingClientRect();
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 scale = outer.height / this.scroller.offsetHeight || 1;
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
- /* Where the button goes, in the composer's row of round controls. */
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;
@@ -498,6 +498,7 @@
498
498
  "from": "from {{previous}}"
499
499
  },
500
500
  "table": {
501
+ "label": "Table of {{columns}}",
501
502
  "showAll": "Show all {{total}} rows"
502
503
  },
503
504
  "chart": {
@@ -514,8 +515,10 @@
514
515
  "outsideProject": "Outside the project, shown as text",
515
516
  "options": "Options",
516
517
  "choices": "Choices",
518
+ "checklist": "Checklist",
517
519
  "choice": {
518
520
  "waiting": "Available when the reply is done",
521
+ "reading": "Available once the live data is read",
519
522
  "unavailable": "Not available",
520
523
  "sent": "Sent",
521
524
  "queued": "Queued",
@@ -525,14 +528,11 @@
525
528
  "answered": "Answered with “{{label}}”",
526
529
  "chose": "Chose “{{label}}”",
527
530
  "choseOlder": "Chose “{{label}}” on the reply of {{time}}",
528
- "sendContext": "Send",
529
- "copyText": "Copy text",
530
531
  "fixes_one": "{{count}} fix",
531
532
  "fixes_other": "{{count}} fixes",
532
533
  "shownAsText": "Shown as text",
533
534
  "unknownComponent": "This version cannot draw {{component}}",
534
535
  "renderFailed": "Could not draw this part",
535
- "tooLarge": "The block is too large to draw",
536
536
  "unreadable": "The block could not be read",
537
537
  "live": {
538
538
  "label": "Live",
@@ -540,7 +540,26 @@
540
540
  "sources": "Reads {{sources}}",
541
541
  "failed": "Could not read {{source}}",
542
542
  "refused": "Not allowed to read {{source}}",
543
- "notRead": "Not read"
543
+ "notRead": "Not read",
544
+ "reasons": {
545
+ "access-unavailable": "The access of the chat that wrote this is no longer available.",
546
+ "block-stale": "This block changed since it was drawn.",
547
+ "busy": "Too many live blocks are reading right now.",
548
+ "link-unchecked": "This link has to be checked again.",
549
+ "link-unsupported": "This is not a link the machine can open.",
550
+ "links-unsupported": "The machine does not open links from blocks.",
551
+ "no-unique-match": "The source found no single match for that name.",
552
+ "outside-project": "This is outside the project the agent wrote this in.",
553
+ "query-undeclared": "This block does not read that source.",
554
+ "refresh-limit": "This source reads at most once every ten seconds.",
555
+ "result-too-large": "What the source returned is too large.",
556
+ "snapshot-too-large": "The first readings were too large to keep.",
557
+ "source-unregistered": "The machine does not know this source.",
558
+ "stale-read": "Read the source again before you choose.",
559
+ "target-unavailable": "What this points to is no longer available.",
560
+ "timed-out": "The source took too long to answer.",
561
+ "unreadable": "The source could not be read."
562
+ }
544
563
  }
545
564
  }
546
565
  }
@@ -498,6 +498,7 @@
498
498
  "from": "vanaf {{previous}}"
499
499
  },
500
500
  "table": {
501
+ "label": "Tabel van {{columns}}",
501
502
  "showAll": "Alle {{total}} rijen tonen"
502
503
  },
503
504
  "chart": {
@@ -514,8 +515,10 @@
514
515
  "outsideProject": "Buiten het project, als tekst getoond",
515
516
  "options": "Opties",
516
517
  "choices": "Keuzes",
518
+ "checklist": "Checklijst",
517
519
  "choice": {
518
520
  "waiting": "Beschikbaar zodra het antwoord klaar is",
521
+ "reading": "Beschikbaar zodra de live gegevens gelezen zijn",
519
522
  "unavailable": "Niet beschikbaar",
520
523
  "sent": "Verstuurd",
521
524
  "queued": "In de wachtrij",
@@ -525,14 +528,11 @@
525
528
  "answered": "Beantwoord met “{{label}}”",
526
529
  "chose": "Gekozen: “{{label}}”",
527
530
  "choseOlder": "Gekozen: “{{label}}”, bij het antwoord van {{time}}",
528
- "sendContext": "Versturen",
529
- "copyText": "Tekst kopiëren",
530
531
  "fixes_one": "{{count}} correctie",
531
532
  "fixes_other": "{{count}} correcties",
532
533
  "shownAsText": "Als tekst getoond",
533
534
  "unknownComponent": "Deze versie kan {{component}} niet tekenen",
534
535
  "renderFailed": "Dit deel kon niet getekend worden",
535
- "tooLarge": "Het blok is te groot om te tekenen",
536
536
  "unreadable": "Het blok kon niet gelezen worden",
537
537
  "live": {
538
538
  "label": "Live",
@@ -540,7 +540,26 @@
540
540
  "sources": "Leest {{sources}}",
541
541
  "failed": "{{source}} lezen lukte niet",
542
542
  "refused": "Geen toegang tot {{source}}",
543
- "notRead": "Niet gelezen"
543
+ "notRead": "Niet gelezen",
544
+ "reasons": {
545
+ "access-unavailable": "De toegang van de chat die dit schreef is niet meer beschikbaar.",
546
+ "block-stale": "Dit blok is veranderd sinds het getekend werd.",
547
+ "busy": "Er lezen nu te veel live blokken tegelijk.",
548
+ "link-unchecked": "Deze link moet opnieuw gecontroleerd worden.",
549
+ "link-unsupported": "Dit is geen link die de machine kan openen.",
550
+ "links-unsupported": "De machine opent geen links uit blokken.",
551
+ "no-unique-match": "De bron vond voor die naam niet precies één overeenkomst.",
552
+ "outside-project": "Dit valt buiten het project waarin de agent dit schreef.",
553
+ "query-undeclared": "Dit blok leest die bron niet.",
554
+ "refresh-limit": "Deze bron leest hooguit eens per tien seconden.",
555
+ "result-too-large": "Wat de bron teruggaf is te groot.",
556
+ "snapshot-too-large": "De eerste metingen waren te groot om te bewaren.",
557
+ "source-unregistered": "De machine kent deze bron niet.",
558
+ "stale-read": "Lees de bron opnieuw voordat je kiest.",
559
+ "target-unavailable": "Waar dit naar verwijst is niet meer beschikbaar.",
560
+ "timed-out": "De bron deed er te lang over om te antwoorden.",
561
+ "unreadable": "De bron kon niet gelezen worden."
562
+ }
544
563
  }
545
564
  }
546
565
  }
@@ -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
- let handlers = eventHandlers.get(event);
114
- if (!handlers) {
115
- handlers = new Set();
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
- set.delete(handler);
117
+ handlers.delete(handler);
122
118
  };
123
119
  },
124
120
  subscribeStatus(handler: (status: ChatTransportStatus) => void): () => void {
@@ -160,7 +160,11 @@ export function applyEvent(state: ChatState, event: ChatEvent): ChatState {
160
160
  case 'delta': {
161
161
  const item = state.items[event.itemId];
162
162
  if (event.uiQueries && item?.kind === 'assistant') {
163
- return { ...state, items: { ...state.items, [event.itemId]: { ...item, uiQueries: event.uiQueries } } };
163
+ // The text first, as the daemon's thread applies it, then the metadata on what it grew into.
164
+ const { uiQueries, ...text } = event;
165
+ const grown = event.text === '' ? state : applyEvent(state, text);
166
+ const reply = grown.items[event.itemId] ?? item;
167
+ return { ...grown, items: { ...grown.items, [event.itemId]: { ...reply, uiQueries } as ChatItem } };
164
168
  }
165
169
  if (event.ui !== undefined && event.text === '') {
166
170
  if (item?.kind === 'assistant' && item.streaming && event.textLength === item.text.length) {
@@ -190,6 +194,15 @@ export function applyEvent(state: ChatState, event: ChatEvent): ChatState {
190
194
  }
191
195
  }
192
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
+
193
206
  export const useChats = create<ChatsStore>((set) => ({
194
207
  byKey: {},
195
208
  statusByKey: {},
@@ -243,22 +256,10 @@ export const useChats = create<ChatsStore>((set) => ({
243
256
  });
244
257
  },
245
258
  bookmarks(key, bookmarks) {
246
- set((s) => {
247
- const current = s.byKey[key];
248
- if (!current) {
249
- return {};
250
- }
251
- return { byKey: { ...s.byKey, [key]: { ...current, bookmarks } } };
252
- });
259
+ set((s) => withFields(s, key, { bookmarks }));
253
260
  },
254
261
  visuals(key, visuals) {
255
- set((s) => {
256
- const current = s.byKey[key];
257
- if (!current) {
258
- return {};
259
- }
260
- return { byKey: { ...s.byKey, [key]: { ...current, visuals } } };
261
- });
262
+ set((s) => withFields(s, key, { visuals }));
262
263
  },
263
264
  forget(key) {
264
265
  set((s) => {