@adecore/agents-react 0.20.0 → 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 (134) hide show
  1. package/dist/chat/chat-client.d.ts +1 -0
  2. package/dist/chat/logic/generated-image.d.ts +27 -0
  3. package/dist/chat/logic/generated-image.js +32 -0
  4. package/dist/chat/logic/timeline-target.d.ts +19 -1
  5. package/dist/chat/logic/timeline-target.js +49 -3
  6. package/dist/chat/logic/timeline.js +25 -17
  7. package/dist/chat/subagent-conversation.d.ts +2 -1
  8. package/dist/chat/subagent-conversation.js +7 -1
  9. package/dist/chat/timeline-flash.d.ts +3 -1
  10. package/dist/chat/timeline-flash.js +2 -2
  11. package/dist/chat/ui/CodeBlock.d.ts +3 -1
  12. package/dist/chat/ui/CodeBlock.js +38 -7
  13. package/dist/chat/ui/Composer.js +13 -3
  14. package/dist/chat/ui/ImageView.d.ts +8 -2
  15. package/dist/chat/ui/ImageView.js +12 -6
  16. package/dist/chat/ui/Markdown.d.ts +7 -1
  17. package/dist/chat/ui/Markdown.js +80 -23
  18. package/dist/chat/ui/MessageActions.js +6 -7
  19. package/dist/chat/ui/SubagentTimeline.js +7 -3
  20. package/dist/chat/ui/Timeline.js +68 -8
  21. package/dist/chat/ui/TimelineMenu.js +7 -3
  22. package/dist/chat/ui/file-links.d.ts +2 -1
  23. package/dist/chat/ui/file-links.js +8 -3
  24. package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +15 -0
  25. package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +50 -0
  26. package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +10 -0
  27. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +61 -0
  28. package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
  29. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
  30. package/dist/chat/ui/intelligent-ui/UiReply.d.ts +13 -0
  31. package/dist/chat/ui/intelligent-ui/UiReply.js +325 -0
  32. package/dist/chat/ui/intelligent-ui/block-local.d.ts +2 -0
  33. package/dist/chat/ui/intelligent-ui/block-local.js +13 -0
  34. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
  35. package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
  36. package/dist/chat/ui/intelligent-ui/node-text.d.ts +9 -0
  37. package/dist/chat/ui/intelligent-ui/node-text.js +52 -0
  38. package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
  39. package/dist/chat/ui/intelligent-ui/registry.js +50 -0
  40. package/dist/chat/ui/intelligent-ui/render-context.d.ts +50 -0
  41. package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
  42. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
  43. package/dist/chat/ui/intelligent-ui/renderers/chart.js +125 -0
  44. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
  45. package/dist/chat/ui/intelligent-ui/renderers/choices.js +41 -0
  46. package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
  47. package/dist/chat/ui/intelligent-ui/renderers/content.js +66 -0
  48. package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
  49. package/dist/chat/ui/intelligent-ui/renderers/data.js +87 -0
  50. package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +7 -0
  51. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +60 -0
  52. package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
  53. package/dist/chat/ui/intelligent-ui/renderers/links.js +86 -0
  54. package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
  55. package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
  56. package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
  57. package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
  58. package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
  59. package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
  60. package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
  61. package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
  62. package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
  63. package/dist/chat/ui/intelligent-ui/tones.js +31 -0
  64. package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
  65. package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
  66. package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
  67. package/dist/chat/ui/intelligent-ui/use-ui-links.js +107 -0
  68. package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +20 -0
  69. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +259 -0
  70. package/dist/chat/ui/reply-context.d.ts +8 -0
  71. package/dist/chat/ui/reply-context.js +1 -0
  72. package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
  73. package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
  74. package/dist/chat/ui/rows/MessageRows.js +27 -7
  75. package/dist/chat/ui/rows/Rows.js +4 -2
  76. package/dist/chat/ui/rows/SubagentRow.js +5 -0
  77. package/dist/chat/ui/rows/WorkRows.d.ts +4 -1
  78. package/dist/chat/ui/rows/WorkRows.js +4 -3
  79. package/dist/host.d.ts +25 -3
  80. package/dist/host.js +1 -0
  81. package/dist/locales/en/agent-chat.json +89 -1
  82. package/dist/locales/nl/agent-chat.json +89 -1
  83. package/dist/state/chats.js +9 -0
  84. package/dist/theme.css +38 -2
  85. package/package.json +133 -7
  86. package/src/chat/chat-client.ts +1 -0
  87. package/src/chat/logic/generated-image.ts +48 -0
  88. package/src/chat/logic/timeline-target.ts +69 -4
  89. package/src/chat/logic/timeline.ts +28 -17
  90. package/src/chat/subagent-conversation.ts +13 -2
  91. package/src/chat/timeline-flash.ts +5 -3
  92. package/src/chat/ui/CodeBlock.tsx +70 -21
  93. package/src/chat/ui/Composer.tsx +22 -5
  94. package/src/chat/ui/ImageView.tsx +51 -8
  95. package/src/chat/ui/Markdown.tsx +123 -26
  96. package/src/chat/ui/MessageActions.tsx +15 -8
  97. package/src/chat/ui/SubagentTimeline.tsx +26 -1
  98. package/src/chat/ui/Timeline.tsx +98 -22
  99. package/src/chat/ui/TimelineMenu.tsx +6 -3
  100. package/src/chat/ui/file-links.ts +9 -3
  101. package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +139 -0
  102. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +89 -0
  103. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
  104. package/src/chat/ui/intelligent-ui/UiReply.tsx +531 -0
  105. package/src/chat/ui/intelligent-ui/block-local.ts +16 -0
  106. package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
  107. package/src/chat/ui/intelligent-ui/node-text.ts +59 -0
  108. package/src/chat/ui/intelligent-ui/registry.ts +52 -0
  109. package/src/chat/ui/intelligent-ui/render-context.ts +78 -0
  110. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +321 -0
  111. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +98 -0
  112. package/src/chat/ui/intelligent-ui/renderers/content.tsx +145 -0
  113. package/src/chat/ui/intelligent-ui/renderers/data.tsx +187 -0
  114. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +121 -0
  115. package/src/chat/ui/intelligent-ui/renderers/links.tsx +157 -0
  116. package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
  117. package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
  118. package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
  119. package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
  120. package/src/chat/ui/intelligent-ui/tones.ts +37 -0
  121. package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
  122. package/src/chat/ui/intelligent-ui/use-ui-links.ts +123 -0
  123. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +282 -0
  124. package/src/chat/ui/reply-context.ts +7 -0
  125. package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
  126. package/src/chat/ui/rows/MessageRows.tsx +69 -7
  127. package/src/chat/ui/rows/Rows.tsx +4 -2
  128. package/src/chat/ui/rows/SubagentRow.tsx +5 -0
  129. package/src/chat/ui/rows/WorkRows.tsx +20 -5
  130. package/src/host.ts +46 -3
  131. package/src/locales/en/agent-chat.json +89 -1
  132. package/src/locales/nl/agent-chat.json +89 -1
  133. package/src/state/chats.ts +9 -0
  134. package/src/theme.css +38 -2
@@ -10,6 +10,7 @@ interface ChatOpenOptions {
10
10
  runtimeMode?: RuntimeMode;
11
11
  }
12
12
  export interface ChatSendExtras {
13
+ delivery?: 'steer' | 'queue';
13
14
  mentions?: string[];
14
15
  skills?: string[];
15
16
  chats?: string[];
@@ -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,12 +1,30 @@
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
+ import type { FileRef } from '../../host.js';
3
4
  export interface TimelineTarget {
4
5
  selection: string;
5
6
  row: TimelineRow | null;
6
7
  code: string | null;
7
8
  path: string | null;
8
9
  line: number | null;
10
+ file?: {
11
+ ref: FileRef;
12
+ cwd: string | null;
13
+ scopeId: string | null;
14
+ } | null;
9
15
  }
10
16
  export declare const EMPTY_TARGET: TimelineTarget;
11
17
  export declare function withCurrentText(rows: TimelineRow[], items: Record<string, ChatItem> | undefined): TimelineRow[];
12
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;
@@ -23,12 +23,58 @@ export function withCurrentText(rows, items) {
23
23
  export function readTimelineTarget(element, scroller, rows) {
24
24
  const id = element.closest('[data-item-id]')?.dataset.itemId;
25
25
  const file = element.closest('[data-file-path]');
26
- const line = Number(file?.dataset.fileLine);
26
+ const line = positiveInteger(file?.dataset.fileLine);
27
+ const column = positiveInteger(file?.dataset.fileColumn);
28
+ const endLine = positiveInteger(file?.dataset.fileEndLine);
29
+ const path = file?.dataset.filePath ?? null;
27
30
  return {
28
31
  selection: selectionWithin(scroller),
29
32
  row: rows.find((row) => row.id === id) ?? null,
30
33
  code: element.closest('pre')?.textContent ?? null,
31
- path: file?.dataset.filePath ?? null,
32
- line: Number.isFinite(line) && line > 0 ? line : null
34
+ path,
35
+ line,
36
+ file: path === null
37
+ ? null
38
+ : {
39
+ ref: {
40
+ path,
41
+ ...(line === null ? {} : { line }),
42
+ ...(column === null ? {} : { column }),
43
+ ...(line === null || endLine === null || endLine < line ? {} : { endLine }),
44
+ directory: file?.dataset.fileDirectory === 'true'
45
+ },
46
+ cwd: file?.closest('[data-file-cwd]')?.dataset.fileCwd || null,
47
+ scopeId: file?.closest('[data-file-scope-id]')?.dataset.fileScopeId || null
48
+ }
33
49
  };
34
50
  }
51
+ function positiveInteger(value) {
52
+ const number = Number(value);
53
+ return Number.isSafeInteger(number) && number > 0 ? number : null;
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
  }
@@ -289,31 +298,33 @@ export function deriveTimelineRows(items, options) {
289
298
  }
290
299
  const turnId = chunk.turnId;
291
300
  const turn = chunk.items.find((item) => item.kind === 'turn');
292
- const user = chunk.items.filter((item) => item.kind === 'user');
293
- const rest = chunk.items.filter((item) => item.kind !== 'user' && item.kind !== 'turn');
301
+ const conversation = chunk.items.filter((item) => item.kind !== 'turn');
294
302
  if (turn?.origin === 'agent') {
295
303
  // The agent started this one itself, so there is no message of the person to show above it.
296
304
  rows.push({ kind: 'turn-start', id: `start-${turnId}`, turn, label: agentTurnLabel(turn) });
297
305
  }
298
- rows.push(...rowsForItems(user, options, children));
299
306
  const active = turnId === options.activeTurnId || turn?.state === 'running';
300
- const work = rowsForItems(rest, options, children);
301
307
  if (!turn || active) {
302
- rows.push(...work);
308
+ rows.push(...rowsForItems(conversation, options, children));
303
309
  if (active) {
304
310
  rows.push({ kind: 'working', id: `working-${turnId}`, startedAt: turn?.createdAt ?? Date.now() });
305
311
  }
306
312
  continue;
307
313
  }
308
- // The closing answer stays visible; everything before it folds behind the label.
314
+ // Steering splits the conversation inside a turn. Earlier responses keep their place above it.
315
+ const lastUser = conversation.findLastIndex((item) => item.kind === 'user');
316
+ rows.push(...rowsForItems(conversation.slice(0, lastUser + 1), options, children));
317
+ const rest = conversation.slice(lastUser + 1);
318
+ const work = rowsForItems(rest, options, children);
319
+ // The closing answer stays visible; the work after the last input folds behind the label.
309
320
  const finalAssistant = lastAssistantRow(work);
310
321
  const before = finalAssistant ? work.slice(0, work.indexOf(finalAssistant)) : work;
311
- // A workflow runs on past the turn that launched it, so its row stays in sight until it ends.
312
- 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'));
313
324
  const folded = before.filter((row) => !standing.includes(row));
314
325
  const expanded = options.expandedTurns.has(turnId);
315
326
  if (folded.length > 0) {
316
- 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));
317
328
  rows.push({
318
329
  kind: 'turn-fold',
319
330
  id: `fold-${turnId}`,
@@ -323,17 +334,14 @@ export function deriveTimelineRows(items, options) {
323
334
  hiddenCount: folded.length,
324
335
  expanded
325
336
  });
326
- if (expanded) {
327
- rows.push(...folded);
328
- }
329
337
  }
330
- rows.push(...standing);
331
- const changed = changedFilesRow(turn, rest);
338
+ rows.push(...(expanded ? before : standing));
339
+ const changed = changedFilesRow(turn, conversation);
332
340
  if (changed) {
333
341
  rows.push(changed);
334
342
  }
335
343
  if (finalAssistant) {
336
- rows.push(finalAssistant);
344
+ rows.push(...work.slice(work.indexOf(finalAssistant)));
337
345
  }
338
346
  if (options.forkedTurns?.has(turnId) === true) {
339
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,4 +1,6 @@
1
- export declare function CodeBlock({ code, lang }: {
1
+ import { type ReactNode } from 'react';
2
+ export declare function CodeBlock({ code, lang, actions }: {
2
3
  code: string;
3
4
  lang: string;
5
+ actions?: ReactNode;
4
6
  }): import("react").JSX.Element;
@@ -1,12 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, memo, useContext, useEffect, useMemo, useState } from 'react';
2
+ import { Fragment, memo, useContext, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Check, Copy } from 'lucide-react';
6
+ import { ButtonGroup, IconButton } from '@adecore/ui';
4
7
  import { IncrementalLines } from './code-lines.js';
5
8
  import { CodeStreamingContext } from './code-streaming.js';
6
9
  import { WHOLE_FADE_CLASS } from './rehype-fade.js';
7
10
  import { shikiThemeOf, useCodeTheme } from './code-theme.js';
8
11
  // A plain language needs no grammar; shiki draws it in the theme's own foreground.
9
12
  const PLAIN = 'text';
13
+ const COPIED_MS = 1500;
10
14
  let highlighter = null;
11
15
  let highlighterLoad = null;
12
16
  const loadedLanguages = new Set([PLAIN]);
@@ -102,9 +106,12 @@ const CodeLine = memo(function CodeLine({ tokens }) {
102
106
  * fence that is still open is tokenized as it grows, and the same component carries on once it
103
107
  * closes, so closing it does not draw it again.
104
108
  */
105
- export function CodeBlock({ code, lang }) {
109
+ export function CodeBlock({ code, lang, actions }) {
110
+ const { t } = useTranslation('agent-chat');
106
111
  const theme = useCodeTheme();
107
112
  const streaming = useContext(CodeStreamingContext);
113
+ const [copyState, setCopyState] = useState('idle');
114
+ const copying = useRef(false);
108
115
  // What loaded last, so a block whose theme changes draws again once the new one is in.
109
116
  const [loaded, setLoaded] = useState(null);
110
117
  const tokenizer = tokenizerFor(lang, theme);
@@ -112,6 +119,31 @@ export function CodeBlock({ code, lang }) {
112
119
  const [waited] = useState(tokenizer === null);
113
120
  const lines = useMemo(() => (tokenizer ? new IncrementalLines(tokenizer.tokenize) : null), [tokenizer]);
114
121
  const tokens = useMemo(() => lines?.update(code, !streaming) ?? null, [lines, code, streaming]);
122
+ useEffect(() => {
123
+ if (copyState !== 'copied') {
124
+ return;
125
+ }
126
+ const timer = setTimeout(() => setCopyState('idle'), COPIED_MS);
127
+ return () => clearTimeout(timer);
128
+ }, [copyState]);
129
+ async function copy() {
130
+ if (copying.current) {
131
+ return;
132
+ }
133
+ copying.current = true;
134
+ setCopyState('copying');
135
+ try {
136
+ // The best-effort clipboard helper cannot distinguish a refusal from success.
137
+ await navigator.clipboard.writeText(code);
138
+ setCopyState('copied');
139
+ }
140
+ catch {
141
+ setCopyState('failed');
142
+ }
143
+ finally {
144
+ copying.current = false;
145
+ }
146
+ }
115
147
  useEffect(() => {
116
148
  if (tokenizer !== null) {
117
149
  return;
@@ -124,9 +156,8 @@ export function CodeBlock({ code, lang }) {
124
156
  cancelled = true;
125
157
  };
126
158
  }, [lang, theme, tokenizer]);
127
- if (tokenizer === null || tokens === null) {
128
- const failed = loaded === 'failed';
129
- return (_jsx("div", { className: clsx('chat-code', !failed && 'invisible'), "aria-hidden": failed ? undefined : true, children: _jsx("pre", { children: _jsx("code", { children: code }) }) }));
130
- }
131
- return (_jsx("div", { className: clsx('chat-code', waited && WHOLE_FADE_CLASS), children: _jsx("pre", { style: { color: tokenizer.fg }, children: _jsx("code", { children: tokens.map((line, index) => (_jsxs(Fragment, { children: [_jsx(CodeLine, { tokens: line }), index < tokens.length - 1 && '\n'] }, index))) }) }) }));
159
+ const pending = (tokenizer === null || tokens === null) && loaded !== 'failed';
160
+ return (_jsxs("div", { className: "chat-code", "data-copy-state": copyState, children: [_jsx("pre", { className: clsx(pending && 'invisible', !pending && tokens !== null && waited && WHOLE_FADE_CLASS), "aria-hidden": pending || undefined, style: { color: tokenizer?.fg }, children: _jsx("code", { children: tokens === null
161
+ ? code
162
+ : tokens.map((line, index) => (_jsxs(Fragment, { children: [_jsx(CodeLine, { tokens: line }), index < tokens.length - 1 && '\n'] }, index))) }) }), _jsxs(ButtonGroup, { className: "chat-code-actions", role: "group", "aria-label": t('code.actions'), children: [_jsx(IconButton, { icon: copyState === 'copied' ? Check : Copy, size: "sm", label: copyState === 'copied' ? t('timeline.actions.copied') : t('timeline.menu.copyCode'), busy: copyState === 'copying', "aria-busy": copyState === 'copying', "aria-disabled": copyState === 'copying', onClick: () => void copy() }), actions] }), _jsx("span", { className: "sr-only", role: "status", children: copyState === 'copied' ? t('timeline.actions.copied') : '' }), copyState === 'failed' && (_jsx("p", { className: "chat-code-error", role: "alert", children: t('code.copyFailed') }))] }));
132
163
  }
@@ -95,6 +95,8 @@ function splitPath(path) {
95
95
  // Keep the editor mounted while a pending request takes over, so its selection and draft survive.
96
96
  export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabled, readOnly = false, providerFixed, onSend, onRetarget }) {
97
97
  const { t } = useTranslation('agent-chat');
98
+ const sendDelivery = chatHost().useSendDelivery();
99
+ const canSteer = info.provider === 'claude' || info.provider === 'codex';
98
100
  const [draft, setDraft] = useState(() => readDraft(chatId));
99
101
  const [historyIndex, setHistoryIndex] = useState(null);
100
102
  const [menuIndex, setMenuIndex] = useState(0);
@@ -474,7 +476,8 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
474
476
  const removeAttachment = (index) => {
475
477
  setDraft((current) => ({ ...current, attachments: current.attachments.filter((_, i) => i !== index) }));
476
478
  };
477
- const submit = () => {
479
+ const submit = (alternate = false) => {
480
+ const delivery = canSteer && (alternate ? sendDelivery === 'queue' : sendDelivery === 'steer') ? 'steer' : 'queue';
478
481
  const trimmed = text.trim();
479
482
  if (isEmptyDraft(draft) || !writable || guard.tooLong) {
480
483
  return;
@@ -502,6 +505,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
502
505
  return;
503
506
  }
504
507
  onSend(withQuote(quote, trimmed), {
508
+ delivery,
505
509
  mentions: presentMentions(trimmed, draft.mentions),
506
510
  skills: presentSkills(trimmed, draft.skills),
507
511
  chats: draft.chats,
@@ -739,7 +743,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
739
743
  view.dispatch({ changes: { from: line.from, to: line.to }, selection: { anchor: line.from }, userEvent: 'delete' });
740
744
  return true;
741
745
  }
742
- submit();
746
+ submit(e.altKey);
743
747
  return true;
744
748
  }
745
749
  const { from, to } = view.state.selection.main;
@@ -852,5 +856,11 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
852
856
  addFiles(Array.from(e.target.files ?? []));
853
857
  e.target.value = '';
854
858
  inputRef.current?.focus();
855
- } }), _jsx(RunSettings, { disabled: configurationPending || !writable, providers: pickable, provider: info.provider, selection: info.selection, model: model, runtimeMode: info.runtimeMode, effectiveRuntimeMode: info.effectiveRuntimeMode, permissionMode: info.permissionMode, open: modelPickerOpen, onOpenChange: setModelPickerOpen, onModel: chooseModel, onOption: (id, value) => configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } }), onMode: (runtimeMode) => configure({ runtimeMode }), usage: info.usage, compactDisabled: busy || !writable, onCompact: compact, account: accountChoice === null ? null : { choice: accountChoice, started, onPick: chooseAccount } }), _jsx(StashPicker, { onRestore: restoreStashed }), Slot !== null && (_jsx(ErrorBoundary, { label: t('composer.slotFailed'), resetKeys: [chatId], compact: true, className: "relative rounded-lg", children: _jsx(Slot, { scopeId: scope.id, chatId: chatId, disabled: !writable, insert: insertIntoDraft }) })), _jsx("span", { className: "grow" }), _jsxs("div", { className: "flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover", children: [_jsx("div", { ref: setDictationToolbar, className: "flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" }), busy && (_jsx(Tooltip, { label: composerStopLabel(), name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text", onClick: (event) => stop(event.shiftKey), children: _jsx(Icon, { icon: Square, size: 16 }) }) })), (!busy || !isEmptyDraft(draft)) && (_jsx(Tooltip, { label: busy ? t('composer.queueButton') : t('composer.send'), kbd: KEY_SHORTCUTS.modEnter, name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text hover:brightness-90 disabled:opacity-50 disabled:hover:brightness-100", disabled: isEmptyDraft(draft) || !writable || guard.tooLong, onClick: submit, children: _jsx(Icon, { icon: ArrowUp, size: 16 }) }) }))] })] })] }) }), _jsx(PromptDialog, { open: confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: forceClearThread, onOpenChange: () => setConfirmClear(false) })] }));
859
+ } }), _jsx(RunSettings, { disabled: configurationPending || !writable, providers: pickable, provider: info.provider, selection: info.selection, model: model, runtimeMode: info.runtimeMode, effectiveRuntimeMode: info.effectiveRuntimeMode, permissionMode: info.permissionMode, open: modelPickerOpen, onOpenChange: setModelPickerOpen, onModel: chooseModel, onOption: (id, value) => configure({ selection: { ...info.selection, options: { ...info.selection.options, [id]: value } } }), onMode: (runtimeMode) => configure({ runtimeMode }), usage: info.usage, compactDisabled: busy || !writable, onCompact: compact, account: accountChoice === null ? null : { choice: accountChoice, started, onPick: chooseAccount } }), _jsx(StashPicker, { onRestore: restoreStashed }), Slot !== null && (_jsx(ErrorBoundary, { label: t('composer.slotFailed'), resetKeys: [chatId], compact: true, className: "relative rounded-lg", children: _jsx(Slot, { scopeId: scope.id, chatId: chatId, disabled: !writable, insert: insertIntoDraft }) })), _jsx("span", { className: "grow" }), _jsxs("div", { className: "flex h-9 shrink-0 items-center gap-0.5 rounded-full bg-surface-hover", children: [_jsx("div", { ref: setDictationToolbar, className: "flex items-center pl-1 empty:hidden [&_.icon-btn]:rounded-full" }), busy && (_jsx(Tooltip, { label: composerStopLabel(), name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-status-error text-accent-text", onClick: (event) => stop(event.shiftKey), children: _jsx(Icon, { icon: Square, size: 16 }) }) })), (!busy || !isEmptyDraft(draft)) && (_jsx(Tooltip, { label: busy
860
+ ? t(canSteer
861
+ ? sendDelivery === 'steer'
862
+ ? 'composer.steerButton'
863
+ : 'composer.queueWithSteerButton'
864
+ : 'composer.queueButton')
865
+ : t('composer.send'), kbd: KEY_SHORTCUTS.modEnter, name: true, children: _jsx("button", { className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent text-accent-text hover:brightness-90 disabled:opacity-50 disabled:hover:brightness-100", disabled: isEmptyDraft(draft) || !writable || guard.tooLong, onClick: (event) => submit(event.altKey), children: _jsx(Icon, { icon: ArrowUp, size: 16 }) }) }))] })] })] }) }), _jsx(PromptDialog, { open: confirmClear, title: t('composer.clear.title'), description: t('composer.clear.description'), confirmLabel: t('composer.clear.confirm'), danger: true, fallbackMessage: t('composer.notice.clearFailed'), onConfirm: forceClearThread, onOpenChange: () => setConfirmClear(false) })] }));
856
866
  }
@@ -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,8 +1,13 @@
1
1
  import { type Components, type Options } from 'react-markdown';
2
- export declare const ReplyMarkdown: import("react").MemoExoticComponent<({ text, streaming, arriving }: {
2
+ import { type ShellCodeBlockContext } from '../../host.js';
3
+ type ShellCodeBlockReply = Pick<ShellCodeBlockContext, 'scopeId' | 'chatId' | 'itemId'> & {
4
+ streaming: boolean;
5
+ };
6
+ export declare const ReplyMarkdown: import("react").MemoExoticComponent<({ text, streaming, arriving, reply }: {
3
7
  text: string;
4
8
  streaming: boolean;
5
9
  arriving?: boolean;
10
+ reply?: ShellCodeBlockReply;
6
11
  }) => import("react").JSX.Element>;
7
12
  export declare const MessageMarkdown: import("react").MemoExoticComponent<({ text, mentions, skills }: {
8
13
  text: string;
@@ -16,3 +21,4 @@ export declare const Markdown: import("react").MemoExoticComponent<({ text, brea
16
21
  rehypePlugins?: Options['rehypePlugins'];
17
22
  componentOverrides?: Components;
18
23
  }) => import("react").JSX.Element>;
24
+ export {};