@adecore/agents-react 0.20.1 → 0.21.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.
Files changed (116) hide show
  1. package/dist/chat/logic/generated-image.d.ts +27 -0
  2. package/dist/chat/logic/generated-image.js +32 -0
  3. package/dist/chat/logic/timeline-target.d.ts +13 -1
  4. package/dist/chat/logic/timeline-target.js +26 -0
  5. package/dist/chat/logic/timeline.js +16 -10
  6. package/dist/chat/subagent-conversation.d.ts +2 -1
  7. package/dist/chat/subagent-conversation.js +7 -1
  8. package/dist/chat/timeline-flash.d.ts +3 -1
  9. package/dist/chat/timeline-flash.js +2 -2
  10. package/dist/chat/ui/ImageView.d.ts +8 -2
  11. package/dist/chat/ui/ImageView.js +12 -6
  12. package/dist/chat/ui/SubagentTimeline.js +6 -2
  13. package/dist/chat/ui/Timeline.js +67 -7
  14. package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +15 -0
  15. package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +50 -0
  16. package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +10 -0
  17. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +61 -0
  18. package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
  19. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
  20. package/dist/chat/ui/intelligent-ui/UiReply.d.ts +13 -0
  21. package/dist/chat/ui/intelligent-ui/UiReply.js +325 -0
  22. package/dist/chat/ui/intelligent-ui/block-local.d.ts +2 -0
  23. package/dist/chat/ui/intelligent-ui/block-local.js +13 -0
  24. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
  25. package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
  26. package/dist/chat/ui/intelligent-ui/node-text.d.ts +9 -0
  27. package/dist/chat/ui/intelligent-ui/node-text.js +52 -0
  28. package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
  29. package/dist/chat/ui/intelligent-ui/registry.js +50 -0
  30. package/dist/chat/ui/intelligent-ui/render-context.d.ts +50 -0
  31. package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
  32. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
  33. package/dist/chat/ui/intelligent-ui/renderers/chart.js +125 -0
  34. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
  35. package/dist/chat/ui/intelligent-ui/renderers/choices.js +41 -0
  36. package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
  37. package/dist/chat/ui/intelligent-ui/renderers/content.js +66 -0
  38. package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
  39. package/dist/chat/ui/intelligent-ui/renderers/data.js +87 -0
  40. package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +7 -0
  41. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +60 -0
  42. package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
  43. package/dist/chat/ui/intelligent-ui/renderers/links.js +86 -0
  44. package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
  45. package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
  46. package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
  47. package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
  48. package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
  49. package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
  50. package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
  51. package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
  52. package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
  53. package/dist/chat/ui/intelligent-ui/tones.js +31 -0
  54. package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
  55. package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
  56. package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
  57. package/dist/chat/ui/intelligent-ui/use-ui-links.js +107 -0
  58. package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +20 -0
  59. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +259 -0
  60. package/dist/chat/ui/reply-context.d.ts +8 -0
  61. package/dist/chat/ui/reply-context.js +1 -0
  62. package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
  63. package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
  64. package/dist/chat/ui/rows/MessageRows.js +21 -4
  65. package/dist/chat/ui/rows/Rows.js +4 -2
  66. package/dist/chat/ui/rows/SubagentRow.js +5 -0
  67. package/dist/chat/ui/rows/WorkRows.d.ts +4 -1
  68. package/dist/chat/ui/rows/WorkRows.js +4 -3
  69. package/dist/host.d.ts +11 -1
  70. package/dist/locales/en/agent-chat.json +82 -0
  71. package/dist/locales/nl/agent-chat.json +82 -0
  72. package/dist/state/chats.js +9 -0
  73. package/dist/theme.css +19 -0
  74. package/package.json +133 -7
  75. package/src/chat/logic/generated-image.ts +48 -0
  76. package/src/chat/logic/timeline-target.ts +41 -1
  77. package/src/chat/logic/timeline.ts +19 -10
  78. package/src/chat/subagent-conversation.ts +13 -2
  79. package/src/chat/timeline-flash.ts +5 -3
  80. package/src/chat/ui/ImageView.tsx +51 -8
  81. package/src/chat/ui/SubagentTimeline.tsx +24 -1
  82. package/src/chat/ui/Timeline.tsx +97 -21
  83. package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +139 -0
  84. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +89 -0
  85. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
  86. package/src/chat/ui/intelligent-ui/UiReply.tsx +531 -0
  87. package/src/chat/ui/intelligent-ui/block-local.ts +16 -0
  88. package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
  89. package/src/chat/ui/intelligent-ui/node-text.ts +59 -0
  90. package/src/chat/ui/intelligent-ui/registry.ts +52 -0
  91. package/src/chat/ui/intelligent-ui/render-context.ts +78 -0
  92. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +321 -0
  93. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +98 -0
  94. package/src/chat/ui/intelligent-ui/renderers/content.tsx +145 -0
  95. package/src/chat/ui/intelligent-ui/renderers/data.tsx +187 -0
  96. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +121 -0
  97. package/src/chat/ui/intelligent-ui/renderers/links.tsx +157 -0
  98. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
  99. package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
  100. package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
  101. package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
  102. package/src/chat/ui/intelligent-ui/tones.ts +37 -0
  103. package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
  104. package/src/chat/ui/intelligent-ui/use-ui-links.ts +123 -0
  105. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +282 -0
  106. package/src/chat/ui/reply-context.ts +7 -0
  107. package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
  108. package/src/chat/ui/rows/MessageRows.tsx +60 -4
  109. package/src/chat/ui/rows/Rows.tsx +4 -2
  110. package/src/chat/ui/rows/SubagentRow.tsx +5 -0
  111. package/src/chat/ui/rows/WorkRows.tsx +20 -5
  112. package/src/host.ts +27 -1
  113. package/src/locales/en/agent-chat.json +82 -0
  114. package/src/locales/nl/agent-chat.json +82 -0
  115. package/src/state/chats.ts +9 -0
  116. package/src/theme.css +19 -0
@@ -0,0 +1,27 @@
1
+ import { type ChatAttachment, type ChatToolItem } from '@adecore/agent-contracts';
2
+ export declare const IMAGE_GENERATION_TOOL = "ImageGeneration";
3
+ export type GeneratedImageFailure = {
4
+ kind: 'provider';
5
+ message: string | null;
6
+ } | {
7
+ kind: 'empty';
8
+ } | {
9
+ kind: 'not-image';
10
+ } | {
11
+ kind: 'too-large';
12
+ limit: number;
13
+ };
14
+ export type GeneratedImageView = {
15
+ state: 'generating';
16
+ } | {
17
+ state: 'failed';
18
+ failure: GeneratedImageFailure;
19
+ } | {
20
+ state: 'ready';
21
+ attachment: ChatAttachment;
22
+ prompt: string | null;
23
+ transparent: boolean;
24
+ };
25
+ export declare function isImageGeneration(tool: ChatToolItem): boolean;
26
+ export declare function generatedImageView(tool: ChatToolItem): GeneratedImageView;
27
+ export declare function attachmentAspect(attachment: ChatAttachment): number | undefined;
@@ -0,0 +1,32 @@
1
+ import { CHAT_ATTACHMENT_MAX_BYTES, GeneratedImageInputSchema } from '@adecore/agent-contracts';
2
+ export const IMAGE_GENERATION_TOOL = 'ImageGeneration';
3
+ export function isImageGeneration(tool) {
4
+ return tool.name === IMAGE_GENERATION_TOOL;
5
+ }
6
+ /* What the row of an image generation draws, from the tool call alone. */
7
+ export function generatedImageView(tool) {
8
+ if (tool.state === 'running') {
9
+ return { state: 'generating' };
10
+ }
11
+ const parsed = GeneratedImageInputSchema.safeParse(tool.input);
12
+ const input = parsed.success ? parsed.data : {};
13
+ const output = tool.output?.trim() || null;
14
+ if (tool.state === 'error' || input.attachment === undefined) {
15
+ return { state: 'failed', failure: output === null && tool.state !== 'error' ? { kind: 'empty' } : { kind: 'provider', message: output } };
16
+ }
17
+ const attachment = input.attachment;
18
+ if (attachment.size === 0) {
19
+ return { state: 'failed', failure: { kind: 'empty' } };
20
+ }
21
+ if (!attachment.mime.startsWith('image/')) {
22
+ return { state: 'failed', failure: { kind: 'not-image' } };
23
+ }
24
+ if (attachment.size > CHAT_ATTACHMENT_MAX_BYTES) {
25
+ return { state: 'failed', failure: { kind: 'too-large', limit: CHAT_ATTACHMENT_MAX_BYTES } };
26
+ }
27
+ return { state: 'ready', attachment, prompt: input.revisedPrompt?.trim() || null, transparent: input.transparentBackground === true };
28
+ }
29
+ /* Width over height, when the attachment carries both. */
30
+ export function attachmentAspect(attachment) {
31
+ return attachment.width !== undefined && attachment.height !== undefined ? attachment.width / attachment.height : undefined;
32
+ }
@@ -1,4 +1,4 @@
1
- import type { ChatItem } from '@adecore/agent-contracts';
1
+ import type { ChatItem, ChatUiChoiceOrigin } from '@adecore/agent-contracts';
2
2
  import type { TimelineRow } from './timeline.js';
3
3
  import type { FileRef } from '../../host.js';
4
4
  export interface TimelineTarget {
@@ -16,3 +16,15 @@ export interface TimelineTarget {
16
16
  export declare const EMPTY_TARGET: TimelineTarget;
17
17
  export declare function withCurrentText(rows: TimelineRow[], items: Record<string, ChatItem> | undefined): TimelineRow[];
18
18
  export declare function readTimelineTarget(element: HTMLElement, scroller: HTMLElement | null, rows: TimelineRow[]): TimelineTarget;
19
+ export type UiReplyTarget = Pick<ChatUiChoiceOrigin, 'itemId' | 'blockId' | 'revision'>;
20
+ export type UiReplyJump = {
21
+ kind: 'missing' | 'earlier';
22
+ } | {
23
+ kind: 'turn';
24
+ turnId: string;
25
+ } | {
26
+ kind: 'row';
27
+ index: number;
28
+ };
29
+ export declare function uiReplyJump(target: UiReplyTarget, rows: readonly TimelineRow[], items: Record<string, ChatItem> | undefined, expandedTurns: ReadonlySet<string>, hasEarlier: boolean): UiReplyJump;
30
+ export declare function revealUiReplyBlock(scroller: HTMLElement, target: UiReplyTarget): boolean;
@@ -52,3 +52,29 @@ function positiveInteger(value) {
52
52
  const number = Number(value);
53
53
  return Number.isSafeInteger(number) && number > 0 ? number : null;
54
54
  }
55
+ export function uiReplyJump(target, rows, items, expandedTurns, hasEarlier) {
56
+ const item = items?.[target.itemId];
57
+ if (!item) {
58
+ return { kind: hasEarlier ? 'earlier' : 'missing' };
59
+ }
60
+ if (item.kind !== 'assistant' || !item.ui?.some((block) => block.id === target.blockId && block.revision === target.revision)) {
61
+ return { kind: 'missing' };
62
+ }
63
+ const index = rows.findIndex((row) => row.kind === 'assistant' && row.id === target.itemId);
64
+ if (index !== -1) {
65
+ return { kind: 'row', index };
66
+ }
67
+ return item.turnId && !expandedTurns.has(item.turnId) ? { kind: 'turn', turnId: item.turnId } : { kind: 'missing' };
68
+ }
69
+ export function revealUiReplyBlock(scroller, target) {
70
+ // Compare attributes instead of interpolating provider ids into CSS selectors.
71
+ const row = Array.from(scroller.querySelectorAll('[data-item-id]')).find((element) => element.dataset.itemId === target.itemId);
72
+ const block = row &&
73
+ Array.from(row.querySelectorAll('[data-ui-block]')).find((element) => element.dataset.uiBlock === target.blockId && element.dataset.uiRevision === target.revision);
74
+ if (!block) {
75
+ return false;
76
+ }
77
+ scroller.scrollTop += block.getBoundingClientRect().top - scroller.getBoundingClientRect().top;
78
+ block.focus({ preventScroll: true });
79
+ return true;
80
+ }
@@ -1,6 +1,7 @@
1
1
  import i18next from 'i18next';
2
2
  import { abortedByMachine } from '@adecore/agent-contracts';
3
3
  import { formatElapsedShort } from '@adecore/ui/format';
4
+ import { isImageGeneration } from './generated-image.js';
4
5
  import { handbackReportOf } from './handback.js';
5
6
  import { toolEntry } from './tool-catalog.js';
6
7
  import { hasFileChanges, isFileChange } from './tools.js';
@@ -11,7 +12,7 @@ import { hasFileChanges, isFileChange } from './tools.js';
11
12
  */
12
13
  const BLOCK_KINDS = new Set(['assistant', 'report', 'thinking', 'changed-files', 'compaction', 'app-card', 'visual']);
13
14
  export function isBlock(row) {
14
- return BLOCK_KINDS.has(row.kind);
15
+ return BLOCK_KINDS.has(row.kind) || isImageRow(row);
15
16
  }
16
17
  /* "Read 4 files", "Ran 2 commands", or "12 tool calls" when the run mixes kinds. */
17
18
  export function summarizeGroup(tools) {
@@ -171,6 +172,9 @@ function flushTools(buffer, rows, options) {
171
172
  rows.push({ kind: 'work-live', id: tool.id, tool });
172
173
  }
173
174
  }
175
+ function isImageRow(row) {
176
+ return (row.kind === 'work' || row.kind === 'work-live') && isImageGeneration(row.tool);
177
+ }
174
178
  /* Rows for a run of items, in order, with tool runs folded and a subagent's work under its own row. */
175
179
  function rowsForItems(items, options, children) {
176
180
  const rows = [];
@@ -185,6 +189,11 @@ function rowsForItems(items, options, children) {
185
189
  rows.push({ kind: 'workflow', id: item.id, tool: item, workflow: item.workflow });
186
190
  continue;
187
191
  }
192
+ if (item.kind === 'tool' && isImageGeneration(item)) {
193
+ flushTools(tools, rows, options);
194
+ rows.push({ kind: item.state === 'running' ? 'work-live' : 'work', id: item.id, tool: item });
195
+ continue;
196
+ }
188
197
  if (item.kind === 'tool' && report === null) {
189
198
  tools.push(item);
190
199
  continue;
@@ -250,7 +259,7 @@ function lastAssistantRow(rows) {
250
259
  if (row.kind === 'assistant') {
251
260
  return row;
252
261
  }
253
- if (row.kind !== 'note') {
262
+ if (row.kind !== 'note' && !isImageRow(row)) {
254
263
  return null;
255
264
  }
256
265
  }
@@ -310,12 +319,12 @@ export function deriveTimelineRows(items, options) {
310
319
  // The closing answer stays visible; the work after the last input folds behind the label.
311
320
  const finalAssistant = lastAssistantRow(work);
312
321
  const before = finalAssistant ? work.slice(0, work.indexOf(finalAssistant)) : work;
313
- // A workflow runs on past the turn that launched it, so its row stays in sight until it ends.
314
- const standing = before.filter((row) => row.kind === 'workflow' && row.tool.state === 'running');
322
+ // Images remain a turn's visible result, and a workflow may still be running after its turn ends.
323
+ const standing = before.filter((row) => isImageRow(row) || (row.kind === 'workflow' && row.tool.state === 'running'));
315
324
  const folded = before.filter((row) => !standing.includes(row));
316
325
  const expanded = options.expandedTurns.has(turnId);
317
326
  if (folded.length > 0) {
318
- const tools = rest.filter((item) => item.kind === 'tool' && parentOf(item) === null && handbackReportOf(item) === null);
327
+ const tools = rest.filter((item) => item.kind === 'tool' && parentOf(item) === null && handbackReportOf(item) === null && !isImageGeneration(item));
319
328
  rows.push({
320
329
  kind: 'turn-fold',
321
330
  id: `fold-${turnId}`,
@@ -325,17 +334,14 @@ export function deriveTimelineRows(items, options) {
325
334
  hiddenCount: folded.length,
326
335
  expanded
327
336
  });
328
- if (expanded) {
329
- rows.push(...folded);
330
- }
331
337
  }
332
- rows.push(...standing);
338
+ rows.push(...(expanded ? before : standing));
333
339
  const changed = changedFilesRow(turn, conversation);
334
340
  if (changed) {
335
341
  rows.push(changed);
336
342
  }
337
343
  if (finalAssistant) {
338
- rows.push(finalAssistant);
344
+ rows.push(...work.slice(work.indexOf(finalAssistant)));
339
345
  }
340
346
  if (options.forkedTurns?.has(turnId) === true) {
341
347
  rows.push({ kind: 'forks', id: `forks-${turnId}`, turnId });
@@ -1,9 +1,10 @@
1
- import type { ChatItem, ChatSubagentResult } from '@adecore/agent-contracts';
1
+ import type { ChatApprovalItem, ChatItem, ChatQuestionItem, ChatSubagentResult } from '@adecore/agent-contracts';
2
2
  import { type ChatTransport } from '../transport.js';
3
3
  export declare const SUBAGENT_PAGE = 60;
4
4
  export interface SubagentConversationState {
5
5
  status: 'loading' | 'ready' | 'failed';
6
6
  items: ChatItem[];
7
+ pending: Array<ChatApprovalItem | ChatQuestionItem>;
7
8
  source: ChatSubagentResult['source'] | null;
8
9
  context: NonNullable<ChatSubagentResult['context']> | null;
9
10
  cursor: string | null;
@@ -5,6 +5,7 @@ export const SUBAGENT_PAGE = 60;
5
5
  export const INITIAL_CONVERSATION = {
6
6
  status: 'loading',
7
7
  items: [],
8
+ pending: [],
8
9
  source: null,
9
10
  context: null,
10
11
  cursor: null,
@@ -35,6 +36,9 @@ export function mergeNewest(current, currentCursor, page, pageCursor) {
35
36
  }
36
37
  return { items, cursor: current.length === 0 ? pageCursor : currentCursor };
37
38
  }
39
+ function pendingOf(items) {
40
+ return items.filter((item) => (item.kind === 'approval' && item.decision === 'pending') || (item.kind === 'question' && item.state === 'pending'));
41
+ }
38
42
  function messageOf(error) {
39
43
  return error instanceof Error ? error.message : i18next.t('agent-chat:subagents.noAnswer');
40
44
  }
@@ -98,7 +102,8 @@ export class SubagentConversation {
98
102
  this.set({ loadingEarlier: true });
99
103
  try {
100
104
  const page = await this.ask({ cursor, limit: SUBAGENT_PAGE });
101
- this.set({ items: [...page.items, ...this.state.items], cursor: page.history.cursor, live: page.live, loadingEarlier: false });
105
+ const items = [...page.items, ...this.state.items];
106
+ this.set({ items, pending: page.pending ?? pendingOf(items), cursor: page.history.cursor, live: page.live, loadingEarlier: false });
102
107
  }
103
108
  catch (error) {
104
109
  if (errorCode(error) === 'history-expired') {
@@ -133,6 +138,7 @@ export class SubagentConversation {
133
138
  this.set({
134
139
  status: 'ready',
135
140
  items: merged.items,
141
+ pending: page.pending ?? pendingOf(merged.items),
136
142
  cursor: merged.cursor,
137
143
  live: page.live,
138
144
  source: page.source,
@@ -1,12 +1,14 @@
1
+ import type { UiReplyTarget } from './logic/timeline-target.js';
1
2
  export interface TimelineFlash {
2
3
  key: string;
3
4
  rowId: string;
4
5
  opens: boolean;
6
+ block?: Pick<UiReplyTarget, 'blockId' | 'revision'>;
5
7
  nonce: number;
6
8
  }
7
9
  interface TimelineFlashStore {
8
10
  target: TimelineFlash | null;
9
- flash(key: string, rowId: string, opens: boolean): void;
11
+ flash(key: string, rowId: string, opens: boolean, block?: TimelineFlash['block']): void;
10
12
  }
11
13
  export declare const useTimelineFlash: import("zustand").UseBoundStore<import("zustand").StoreApi<TimelineFlashStore>>;
12
14
  export {};
@@ -3,9 +3,9 @@ import { create } from 'zustand';
3
3
  const FLASH_MS = 2000;
4
4
  export const useTimelineFlash = create((set, get) => ({
5
5
  target: null,
6
- flash(key, rowId, opens) {
6
+ flash(key, rowId, opens, block) {
7
7
  const nonce = (get().target?.nonce ?? 0) + 1;
8
- set({ target: { key, rowId, opens, nonce } });
8
+ set({ target: { key, rowId, opens, nonce, ...(block ? { block } : {}) } });
9
9
  const timer = setTimeout(() => {
10
10
  if (get().target?.nonce === nonce) {
11
11
  set({ target: null });
@@ -1,6 +1,12 @@
1
+ import { type ReactNode } from 'react';
1
2
  import type { ResourceUrl } from '../../host.js';
2
- export declare function ImageThumb({ source, alt, className }: {
3
+ export interface ImageThumbProps {
3
4
  source: ResourceUrl;
4
5
  alt: string;
5
6
  className?: string;
6
- }): import("react").JSX.Element;
7
+ aspect?: number;
8
+ actions?: ReactNode;
9
+ open?: boolean;
10
+ onOpenChange?(open: boolean): void;
11
+ }
12
+ export declare function ImageThumb({ source, alt, className, aspect, actions, open: openProp, onOpenChange }: ImageThumbProps): import("react").JSX.Element;
@@ -24,7 +24,7 @@ function zoomed(view, factor, pointX, pointY) {
24
24
  return { scale, x: pointX - (pointX - view.x) * ratio, y: pointY - (pointY - view.y) * ratio };
25
25
  }
26
26
  /* An image on its own, large: the wheel and the buttons zoom, dragging pans, Escape closes. */
27
- function Lightbox({ src, alt, open, onOpenChange }) {
27
+ function Lightbox({ src, alt, open, onOpenChange, actions }) {
28
28
  const { t } = useTranslation('agent-chat');
29
29
  const [view, setView] = useState(START);
30
30
  // The alt of an image an agent read is its path; the header shows the file and keeps the path on a tooltip.
@@ -44,7 +44,7 @@ function Lightbox({ src, alt, open, onOpenChange }) {
44
44
  return { x: event.clientX - rect.left - rect.width / 2, y: event.clientY - rect.top - rect.height / 2 };
45
45
  };
46
46
  const zoomButton = (factor) => setView((current) => zoomed(current, factor, 0, 0));
47
- return (_jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Popup, { backdropClassName: "lightbox-backdrop", className: "flex min-w-72 flex-col", children: [_jsx(Dialog.Title, { className: "sr-only", children: alt }), _jsxs("div", { className: "flex h-11 shrink-0 items-center gap-2 border-b border-border pr-2 pl-3", children: [name === alt ? title : _jsx(Tooltip, { label: alt, children: title }), _jsx("span", { className: "grow" }), _jsxs("span", { className: "tabular-nums text-xs text-text-faint", children: [Math.round(view.scale * 100), "%"] }), _jsxs(ButtonGroup, { render: _jsx("span", {}), children: [_jsx(IconButton, { icon: Minus, size: "sm", label: t('image.zoomOut'), disabled: view.scale <= MIN_SCALE, onClick: () => zoomButton(1 / BUTTON_STEP) }), _jsx(IconButton, { icon: Plus, size: "sm", label: t('image.zoomIn'), disabled: view.scale >= MAX_SCALE, onClick: () => zoomButton(BUTTON_STEP) }), _jsx(IconButton, { icon: RotateCcw, size: "sm", label: t('image.reset'), disabled: view.scale === MIN_SCALE, onClick: () => setView(START) })] }), _jsx(CloseButton, { label: t('common.action.close'), kbd: "esc", className: "ml-1", dialog: true })] }), _jsx("div", { ref: frameRef, className: clsx('lightbox-frame grid place-items-center overflow-hidden bg-surface-sunken', view.scale > MIN_SCALE && 'cursor-grab'), onWheel: (e) => {
47
+ return (_jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Popup, { backdropClassName: "lightbox-backdrop", className: "flex min-w-72 flex-col", children: [_jsx(Dialog.Title, { className: "sr-only", children: alt }), _jsxs("div", { className: "flex h-11 shrink-0 items-center gap-2 border-b border-border pr-2 pl-3", children: [name === alt ? title : _jsx(Tooltip, { label: alt, children: title }), _jsx("span", { className: "grow" }), _jsxs("span", { className: "tabular-nums text-xs text-text-faint", children: [Math.round(view.scale * 100), "%"] }), _jsxs(ButtonGroup, { render: _jsx("span", {}), children: [_jsx(IconButton, { icon: Minus, size: "sm", label: t('image.zoomOut'), disabled: view.scale <= MIN_SCALE, onClick: () => zoomButton(1 / BUTTON_STEP) }), _jsx(IconButton, { icon: Plus, size: "sm", label: t('image.zoomIn'), disabled: view.scale >= MAX_SCALE, onClick: () => zoomButton(BUTTON_STEP) }), _jsx(IconButton, { icon: RotateCcw, size: "sm", label: t('image.reset'), disabled: view.scale === MIN_SCALE, onClick: () => setView(START) })] }), actions, _jsx(CloseButton, { label: t('common.action.close'), kbd: "esc", className: "ml-1", dialog: true })] }), _jsx("div", { ref: frameRef, className: clsx('lightbox-frame grid place-items-center overflow-hidden bg-surface-sunken', view.scale > MIN_SCALE && 'cursor-grab'), onWheel: (e) => {
48
48
  const point = pointIn(e);
49
49
  setView((current) => zoomed(current, WHEEL_STEP ** -e.deltaY, point.x, point.y));
50
50
  }, onDoubleClick: (e) => {
@@ -71,11 +71,17 @@ function Lightbox({ src, alt, open, onOpenChange }) {
71
71
  * A thumbnail that opens the same image large. The button is the picture, so there is nothing to aim at.
72
72
  * The image is bytes where the host keeps them; until they are here a quiet square stands in for it.
73
73
  */
74
- export function ImageThumb({ source, alt, className }) {
75
- const [open, setOpen] = useState(false);
74
+ export function ImageThumb({ source, alt, className, aspect, actions, open: openProp, onOpenChange }) {
75
+ const [ownOpen, setOwnOpen] = useState(false);
76
+ const open = openProp ?? ownOpen;
77
+ const setOpen = (next) => {
78
+ setOwnOpen(next);
79
+ onOpenChange?.(next);
80
+ };
76
81
  const { url: src, failure } = source;
82
+ const ratio = aspect === undefined ? undefined : { aspectRatio: aspect };
77
83
  if (src === null) {
78
- return (_jsx("span", { className: "grid size-16 place-items-center rounded-lg border border-border bg-surface-sunken text-text-faint", children: failure !== null && (_jsx(Tooltip, { label: failure, children: _jsx(Icon, { icon: ImageOff, size: 14 }) })) }));
84
+ return (_jsx("span", { className: clsx('grid place-items-center rounded-lg border border-border bg-surface-sunken text-text-faint', aspect === undefined ? 'size-16' : 'w-full'), style: ratio, children: failure !== null && (_jsx(Tooltip, { label: failure, children: _jsx(Icon, { icon: ImageOff, size: 14 }) })) }));
79
85
  }
80
- return (_jsxs(_Fragment, { children: [_jsx("button", { className: "block overflow-hidden rounded-lg border border-border", onClick: () => setOpen(true), children: _jsx("img", { src: src, alt: alt, className: className }) }), _jsx(Lightbox, { src: src, alt: alt, open: open, onOpenChange: setOpen })] }));
86
+ return (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: clsx('block overflow-hidden rounded-lg border border-border', aspect !== undefined && 'w-full'), style: ratio, onClick: () => setOpen(true), children: _jsx("img", { src: src, alt: alt, className: clsx(aspect !== undefined && 'size-full object-contain', className) }) }), _jsx(Lightbox, { src: src, alt: alt, open: open, onOpenChange: setOpen, actions: actions })] }));
81
87
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { Bot, LoaderCircle } from 'lucide-react';
5
5
  import { deriveTimelineRows } from '../logic/timeline.js';
@@ -10,6 +10,8 @@ import { EMPTY_TARGET, readTimelineTarget } from '../logic/timeline-target.js';
10
10
  import { openBelow, useSubagentTrail } from '../subagent-view.js';
11
11
  import { Row } from './rows/Rows.js';
12
12
  import { ReplyContext } from './reply-context.js';
13
+ import { PromptComposer } from './PromptComposer.js';
14
+ import { useProviders } from '../../state/providers.js';
13
15
  import { SubagentInfo } from './SubagentInfo.js';
14
16
  import { TimelineMenuPopup } from './TimelineMenu.js';
15
17
  import { FOLLOW_THRESHOLD_PX, rowRhythm } from './rows/row-rhythm.js';
@@ -26,8 +28,10 @@ const NO_TURNS = new Set();
26
28
  export function SubagentTimeline({ chatId, toolUseId }) {
27
29
  const { t } = useTranslation('agent-chat');
28
30
  const scope = useChatScope();
31
+ const connected = useSyncExternalStore(useCallback((changed) => scope.transport.subscribeStatus(changed), [scope]), useCallback(() => scope.transport.status === 'open', [scope]), () => false);
29
32
  const { trail, show } = useSubagentTrail(chatId);
30
33
  const [state, setState] = useState(INITIAL_CONVERSATION);
34
+ const denyReason = useProviders((row) => row.providers.find((provider) => provider.kind === state.context?.provider)?.capabilities.denyReason === true);
31
35
  const controller = useRef(null);
32
36
  const scrollRef = useRef(null);
33
37
  const threadRef = useRef(null);
@@ -103,5 +107,5 @@ export function SubagentTimeline({ chatId, toolUseId }) {
103
107
  }, children: _jsxs(ContextMenu.Root, { children: [_jsxs(ContextMenu.Trigger, { ref: threadRef, className: "chat-column-content", onContextMenu: (event) => setTarget(readTimelineTarget(event.target, threadRef.current, rows)), children: [state.loadingEarlier && (_jsxs("div", { className: "pointer-events-none sticky top-0 z-10 flex h-0 justify-center", role: "status", children: [_jsx(Icon, { icon: LoaderCircle, size: 16, className: "animate-spin text-text-faint" }), _jsx("span", { className: "sr-only", children: t('timeline.loadingEarlier') })] })), rows.length === 0 && !state.live && _jsx(EmptyState, { icon: Bot, children: t('rows.subagent.nothingYet') }), rows.map((row, index) => {
104
108
  const previous = index > 0 ? rows[index - 1] : null;
105
109
  return (_jsx("div", { "data-item-id": row.id, className: rowRhythm(row, previous), children: _jsx(Row, { row: row, chatId: chatId, toggleGroup: groups.toggle, toggleTurn: () => undefined, toggleSubagent: subagents.toggle, openSubagent: () => undefined, openConversation: openChild }) }, row.id));
106
- }), state.live && _jsx("div", { className: "chat-live-text pt-1 text-xs", children: t('subagents.working') })] }), _jsx(TimelineMenuPopup, { target: target, thread: threadRef })] }) }) }) }))] }));
110
+ }), state.live && _jsx("div", { className: "chat-live-text pt-1 text-xs", children: t('subagents.working') })] }), _jsx(TimelineMenuPopup, { target: target, thread: threadRef })] }) }) }) })), state.context?.chatId !== undefined && state.pending.length > 0 && (_jsx("div", { className: "chat-column-content shrink-0", children: _jsx(PromptComposer, { chatId: state.context.chatId, pending: state.pending, focused: false, disabled: !connected, hasDraft: false, denyReason: denyReason, children: null }, state.context.chatId) }))] }));
107
111
  }
@@ -12,8 +12,9 @@ import { openFromMain, useSubagentTrail } from '../subagent-view.js';
12
12
  import { registerItemJumper, registerMessageStepper, registerTimeline, registerTurnJumper, setTimelineAtEnd, wantsEarlier } from '../timeline-scroll.js';
13
13
  import { useTimelineFlash } from '../timeline-flash.js';
14
14
  import { SCRUBBER_MIN_TICKS, STRIP_INSET_PX, STRIP_WIDTH_PX, messagesInView, stepMessage, threadPaddingLeft, STRIP_CLEARANCE_PX, tickOfRow, ticksOf, ticksWithHits } from '../logic/scrubber.js';
15
- import { EMPTY_TARGET, readTimelineTarget, withCurrentText } from '../logic/timeline-target.js';
15
+ import { EMPTY_TARGET, readTimelineTarget, revealUiReplyBlock, uiReplyJump, withCurrentText } from '../logic/timeline-target.js';
16
16
  import { forkRefusal } from '../logic/fork.js';
17
+ import { UiReplyNavigationContext } from './reply-context.js';
17
18
  import { FindRevealContext } from './find-reveal.js';
18
19
  import { BookmarkMarker } from './BookmarkMarker.js';
19
20
  import { MessageActions } from './MessageActions.js';
@@ -142,9 +143,19 @@ export function Timeline({ chatId, composer, overlay }) {
142
143
  const ticks = useMemo(() => ticksOf(rows, marks), [rows, marks]);
143
144
  // A message a jump is on its way to; it may first have to open the turn its reply folded into.
144
145
  const [seeking, setSeeking] = useState(null);
146
+ const [landing, setLanding] = useState(null);
145
147
  // A turn a jump is on its way to, which waits for the page its messages are on to come in.
146
148
  const [revealing, setRevealing] = useState(null);
147
149
  const flash = useTimelineFlash((s) => s.target);
150
+ const revealBlock = useCallback((block) => {
151
+ setLanding(null);
152
+ setSeeking({ itemId: block.itemId, block, key: keyOf(chatId) });
153
+ }, [keyOf, chatId]);
154
+ const uiNavigation = useMemo(() => ({
155
+ chatId,
156
+ reveal: revealBlock,
157
+ flash: flash?.key === keyOf(chatId) && flash.block ? { itemId: flash.rowId, ...flash.block, nonce: flash.nonce } : null
158
+ }), [revealBlock, flash, keyOf, chatId]);
148
159
  // Whether the thread's text clears the strip depends on the width of the view, which only the thread's own frame tells.
149
160
  useEffect(() => {
150
161
  const element = frameRef.current;
@@ -299,7 +310,8 @@ export function Timeline({ chatId, composer, overlay }) {
299
310
  return;
300
311
  }
301
312
  if (tick.bookmark !== null) {
302
- setSeeking(tick.bookmark.itemId);
313
+ setLanding(null);
314
+ setSeeking({ itemId: tick.bookmark.itemId, key: keyOf(chatId) });
303
315
  return;
304
316
  }
305
317
  followRef.current = false;
@@ -309,14 +321,36 @@ export function Timeline({ chatId, composer, overlay }) {
309
321
  if (seeking === null) {
310
322
  return;
311
323
  }
312
- const index = rows.findIndex((row) => row.id === seeking);
324
+ if (seeking.key !== keyOf(chatId)) {
325
+ setSeeking(null);
326
+ return;
327
+ }
328
+ if (seeking.block) {
329
+ const jump = uiReplyJump(seeking.block, rows, items, turns.ids, cursor !== null);
330
+ if (jump.kind === 'earlier') {
331
+ loadEarlier();
332
+ }
333
+ else if (jump.kind === 'turn') {
334
+ turns.add(jump.turnId);
335
+ }
336
+ else {
337
+ setSeeking(null);
338
+ if (jump.kind === 'row') {
339
+ followRef.current = false;
340
+ virtualizer.scrollToIndex(jump.index, { align: 'start' });
341
+ setLanding({ target: seeking.block, key: seeking.key });
342
+ }
343
+ }
344
+ return;
345
+ }
346
+ const index = rows.findIndex((row) => row.id === seeking.itemId);
313
347
  if (index !== -1) {
314
348
  setSeeking(null);
315
349
  followRef.current = false;
316
350
  virtualizer.scrollToIndex(index, { align: 'start' });
317
351
  return;
318
352
  }
319
- const item = items?.[seeking];
353
+ const item = items?.[seeking.itemId];
320
354
  if (item === undefined && cursor !== null) {
321
355
  loadEarlier();
322
356
  return;
@@ -327,8 +361,34 @@ export function Timeline({ chatId, composer, overlay }) {
327
361
  return;
328
362
  }
329
363
  setSeeking(null);
330
- }, [seeking, rows, items, turns, virtualizer, cursor, loadEarlier]);
331
- useEffect(() => registerItemJumper(keyOf(chatId), setSeeking), [keyOf, chatId]);
364
+ }, [seeking, rows, items, turns, virtualizer, cursor, loadEarlier, keyOf, chatId]);
365
+ useEffect(() => registerItemJumper(keyOf(chatId), (itemId) => {
366
+ setLanding(null);
367
+ setSeeking({ itemId, key: keyOf(chatId) });
368
+ }), [keyOf, chatId]);
369
+ useLayoutEffect(() => {
370
+ if (!landing || landing.key !== keyOf(chatId)) {
371
+ return;
372
+ }
373
+ let frame = 0;
374
+ let attempts = 0;
375
+ const reveal = () => {
376
+ const scroller = scrollRef.current;
377
+ if (scroller && revealUiReplyBlock(scroller, landing.target)) {
378
+ useTimelineFlash.getState().flash(keyOf(chatId), landing.target.itemId, false, landing.target);
379
+ setLanding(null);
380
+ }
381
+ else if (++attempts < 30) {
382
+ // Virtual rows mount after scrollToIndex updates the visible range.
383
+ frame = requestAnimationFrame(reveal);
384
+ }
385
+ else {
386
+ setLanding(null);
387
+ }
388
+ };
389
+ frame = requestAnimationFrame(reveal);
390
+ return () => cancelAnimationFrame(frame);
391
+ }, [landing, keyOf, chatId]);
332
392
  useLayoutEffect(() => {
333
393
  if (revealing === null) {
334
394
  return;
@@ -424,7 +484,7 @@ export function Timeline({ chatId, composer, overlay }) {
424
484
  const row = rows[virtualRow.index];
425
485
  const previous = virtualRow.index > 0 ? rows[virtualRow.index - 1] : null;
426
486
  const header = replyHeader(row, previous);
427
- return (_jsxs(RowContainer, { row: row, previous: previous, index: virtualRow.index, top: virtualRow.start, ref: virtualizer.measureElement, children: [flash !== null && flash.key === keyOf(chatId) && flash.rowId === row.id && (_jsx("span", { className: "chat-flash pointer-events-none absolute inset-0 rounded-lg", "aria-hidden": true }, flash.nonce)), header !== null && (_jsx("div", { className: "chat-column-content w-full", children: _jsx(ReplyHeader, { chatId: chatId, at: header.at }) })), _jsx(FindRevealContext.Provider, { value: chatFind.reveal, children: _jsx(MarkableRow, { row: row, chatId: chatId, bookmark: marks.get(row.id) ?? null, children: _jsx(Row, { row: row, chatId: chatId, toggleGroup: groups.toggle, toggleTurn: turns.toggle, toggleSubagent: subagents.toggle, openSubagent: openSubagent, openConversation: openConversation }) }) })] }, row.id));
487
+ return (_jsxs(RowContainer, { row: row, previous: previous, index: virtualRow.index, top: virtualRow.start, ref: virtualizer.measureElement, children: [flash !== null && flash.key === keyOf(chatId) && flash.rowId === row.id && !flash.block && (_jsx("span", { className: "chat-flash pointer-events-none absolute inset-0 rounded-lg", "aria-hidden": true }, flash.nonce)), header !== null && (_jsx("div", { className: "chat-column-content w-full", children: _jsx(ReplyHeader, { chatId: chatId, at: header.at }) })), _jsx(UiReplyNavigationContext.Provider, { value: uiNavigation, children: _jsx(FindRevealContext.Provider, { value: chatFind.reveal, children: _jsx(MarkableRow, { row: row, chatId: chatId, bookmark: marks.get(row.id) ?? null, children: _jsx(Row, { row: row, chatId: chatId, toggleGroup: groups.toggle, toggleTurn: turns.toggle, toggleSubagent: subagents.toggle, openSubagent: openSubagent, openConversation: openConversation }) }) }) })] }, row.id));
428
488
  }) })) }), _jsx(QuoteButton, { thread: threadRef, frame: contentRef, taker: quoteTakerRef }), _jsx("div", { ref: composerRef,
429
489
  // The card is translucent so the thread shows through it, but the pb-3 strip under it must stay opaque.
430
490
  className: clsx('relative z-10 shrink-0', composer && 'px-3 pb-3 pt-3 bg-[linear-gradient(to_bottom,transparent_calc(100%-12px),var(--surface)_calc(100%-12px))]'), style: { position: stickyComposer ? 'sticky' : 'relative', bottom: 0 }, children: _jsx(QuoteTakerContext.Provider, { value: registerQuoteTaker, children: composer }) })] }) }), _jsx(TimelineMenuPopup, { target: target, thread: threadRef, chatId: onMainAgent ? chatId : null })] }), drawsVisuals && _jsx(VisualDialogs, { chatId: chatId }), loadingEarlier && !overlay && (_jsxs("div", { className: "pointer-events-none absolute inset-x-0 top-3 z-10 flex justify-center", role: "status", children: [_jsx(Icon, { icon: LoaderCircle, size: 16, className: "animate-spin text-text-faint" }), _jsx("span", { className: "sr-only", children: t('timeline.loadingEarlier') })] })), showsScrubber && !overlay && (_jsx("div", { className: "absolute top-4", style: { left: STRIP_INSET_PX, width: STRIP_WIDTH_PX, bottom: coveredHeight }, children: _jsx(ErrorBoundary, { label: t('timeline.stripFailed'), resetKeys: [ticks.length], children: _jsx(Scrubber, { ticks: ticks, firstInView: inView?.first ?? null, lastInView: inView?.last ?? null, onPick: pick, chat: cardChat, found: found, foundCurrent: foundCurrent }) }) })), overlay && (_jsx("div", { className: "absolute inset-0 z-5 flex flex-col bg-surface", style: { paddingBottom: coveredHeight }, children: overlay }))] }) }));
@@ -0,0 +1,15 @@
1
+ import type { UiLiveStatus } from './render-context.js';
2
+ export interface UiBlockFix {
3
+ code: string;
4
+ message: string;
5
+ }
6
+ export interface UiBlockFooterProps {
7
+ live?: UiLiveStatus | null;
8
+ answered?: {
9
+ label: string;
10
+ at: number;
11
+ } | null;
12
+ fixes?: readonly UiBlockFix[];
13
+ shownAsText?: string | null;
14
+ }
15
+ export declare function UiBlockFooter({ live, answered, fixes, shownAsText }: UiBlockFooterProps): import("react").JSX.Element | null;
@@ -0,0 +1,50 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Lock, Radio, TriangleAlert, Wrench } from 'lucide-react';
5
+ import { Icon, Spinner, Tooltip, useTickingText } from '@adecore/ui';
6
+ import { formatAgo, formatMoment } from '@adecore/ui/format';
7
+ import { useLater } from './use-later.js';
8
+ const NO_FIXES = [];
9
+ const READING_SPINNER_MS = 1000;
10
+ /* The one sign of a live block: what it reads, and when it last read it. A reading spins only after a second. */
11
+ function LivePart({ live }) {
12
+ const { t } = useTranslation('agent-chat');
13
+ const slow = useLater(live.state === 'reading', READING_SPINNER_MS);
14
+ const { readAt } = live;
15
+ const fresh = live.state === 'fresh' || live.state === 'reading';
16
+ const ago = useTickingText(() => {
17
+ if (readAt === null) {
18
+ return '';
19
+ }
20
+ const time = formatAgo(Date.now() - readAt);
21
+ return fresh ? t('blocks.live.read', { time }) : time;
22
+ });
23
+ const source = live.source ?? live.sources[0] ?? '';
24
+ const icon = slow ? (_jsx(Spinner, { size: 12 })) : live.state === 'failed' ? (_jsx(Icon, { icon: TriangleAlert, size: 12, className: "text-status-needs-you" })) : live.state === 'refused' ? (_jsx(Icon, { icon: Lock, size: 12 })) : (_jsx(Icon, { icon: Radio, size: 12, className: "text-status-idle" }));
25
+ const words = live.state === 'failed'
26
+ ? t('blocks.live.failed', { source })
27
+ : live.state === 'refused'
28
+ ? t('blocks.live.refused', { source })
29
+ : t('blocks.live.label');
30
+ const hint = live.state === 'failed' && live.reason !== undefined
31
+ ? live.reason
32
+ : live.sources.length > 0
33
+ ? t('blocks.live.sources', { sources: live.sources.join(', ') })
34
+ : null;
35
+ const body = (_jsxs("span", { tabIndex: hint === null ? undefined : 0, className: "flex items-center gap-1.5", children: [icon, _jsx("span", { children: words }), readAt !== null && live.state !== 'refused' && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(readAt).toISOString(), children: _jsx("span", { ref: ago, className: "tabular-nums" }) })] })), readAt === null && fresh && (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: t('blocks.live.notRead') })] }))] }));
36
+ return hint === null ? body : _jsx(Tooltip, { label: hint, children: body });
37
+ }
38
+ /*
39
+ * The line under a block, only when it has something to say: live, answered, repaired or drawn as
40
+ * text, in that order. A hairline of the soft border runs the full width of the card over it.
41
+ */
42
+ export function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAsText = null }) {
43
+ const { t } = useTranslation('agent-chat');
44
+ const [fixesOpen, setFixesOpen] = useState(false);
45
+ const listId = useId();
46
+ if (live === null && answered === null && fixes.length === 0 && shownAsText === null) {
47
+ return null;
48
+ }
49
+ return (_jsxs("div", { className: "-mx-2.25 mt-2.25 -mb-2.25 flex flex-col gap-1.5 border-t border-border-soft px-4.25 py-2 text-xs text-text-faint", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-1", children: [live !== null && _jsx(LivePart, { live: live }), answered !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.answered', { label: answered.label }) }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("time", { dateTime: new Date(answered.at).toISOString(), children: formatMoment(answered.at) })] })), fixes.length > 0 && (_jsxs("button", { type: "button", "aria-expanded": fixesOpen, "aria-controls": listId, className: "flex items-center gap-1.5 rounded-sm hover:text-text-muted", onClick: () => setFixesOpen(!fixesOpen), children: [_jsx(Icon, { icon: Wrench, size: 12 }), t('blocks.fixes', { count: fixes.length })] })), shownAsText !== null && (_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { children: t('blocks.shownAsText') }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx("span", { children: shownAsText })] }))] }), fixes.length > 0 && (_jsx("ul", { id: listId, hidden: !fixesOpen, className: "flex flex-col gap-0.5 text-text-muted", children: fixes.map((fix, index) => (_jsxs("li", { className: "flex gap-2", children: [_jsx("span", { className: "shrink-0 font-mono", children: fix.code }), _jsx("span", { className: "min-w-0", children: fix.message })] }, index))) }))] }));
50
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { UiViewNode } from '@adecore/intelligent-ui';
3
+ import { type UiBlockFooterProps } from './UiBlockFooter.js';
4
+ export interface UiBlockFrameProps extends UiBlockFooterProps {
5
+ nodes: readonly UiViewNode[];
6
+ phase: 'streaming' | 'final';
7
+ children: ReactNode;
8
+ className?: string;
9
+ }
10
+ export declare function UiBlockFrame({ nodes, phase, children, className, ...footer }: UiBlockFrameProps): import("react").JSX.Element;