@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.
- package/dist/chat/chat-client.d.ts +1 -0
- package/dist/chat/logic/generated-image.d.ts +27 -0
- package/dist/chat/logic/generated-image.js +32 -0
- package/dist/chat/logic/timeline-target.d.ts +19 -1
- package/dist/chat/logic/timeline-target.js +49 -3
- package/dist/chat/logic/timeline.js +25 -17
- package/dist/chat/subagent-conversation.d.ts +2 -1
- package/dist/chat/subagent-conversation.js +7 -1
- package/dist/chat/timeline-flash.d.ts +3 -1
- package/dist/chat/timeline-flash.js +2 -2
- package/dist/chat/ui/CodeBlock.d.ts +3 -1
- package/dist/chat/ui/CodeBlock.js +38 -7
- package/dist/chat/ui/Composer.js +13 -3
- package/dist/chat/ui/ImageView.d.ts +8 -2
- package/dist/chat/ui/ImageView.js +12 -6
- package/dist/chat/ui/Markdown.d.ts +7 -1
- package/dist/chat/ui/Markdown.js +80 -23
- package/dist/chat/ui/MessageActions.js +6 -7
- package/dist/chat/ui/SubagentTimeline.js +7 -3
- package/dist/chat/ui/Timeline.js +68 -8
- package/dist/chat/ui/TimelineMenu.js +7 -3
- package/dist/chat/ui/file-links.d.ts +2 -1
- package/dist/chat/ui/file-links.js +8 -3
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +15 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +50 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +61 -0
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +11 -0
- package/dist/chat/ui/intelligent-ui/UiReply.d.ts +13 -0
- package/dist/chat/ui/intelligent-ui/UiReply.js +325 -0
- package/dist/chat/ui/intelligent-ui/block-local.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/block-local.js +13 -0
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +16 -0
- package/dist/chat/ui/intelligent-ui/chart-data.js +46 -0
- package/dist/chat/ui/intelligent-ui/node-text.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/node-text.js +52 -0
- package/dist/chat/ui/intelligent-ui/registry.d.ts +2 -0
- package/dist/chat/ui/intelligent-ui/registry.js +50 -0
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +50 -0
- package/dist/chat/ui/intelligent-ui/render-context.js +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +3 -0
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +125 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +4 -0
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +41 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.d.ts +14 -0
- package/dist/chat/ui/intelligent-ui/renderers/content.js +66 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/data.js +87 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +60 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.d.ts +10 -0
- package/dist/chat/ui/intelligent-ui/renderers/links.js +86 -0
- package/dist/chat/ui/intelligent-ui/renderers/structure.d.ts +6 -0
- package/dist/chat/ui/intelligent-ui/renderers/structure.js +32 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.d.ts +8 -0
- package/dist/chat/ui/intelligent-ui/renderers/text.js +61 -0
- package/dist/chat/ui/intelligent-ui/reply-parts.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/reply-parts.js +18 -0
- package/dist/chat/ui/intelligent-ui/table-data.d.ts +31 -0
- package/dist/chat/ui/intelligent-ui/table-data.js +79 -0
- package/dist/chat/ui/intelligent-ui/tones.d.ts +7 -0
- package/dist/chat/ui/intelligent-ui/tones.js +31 -0
- package/dist/chat/ui/intelligent-ui/use-later.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-later.js +16 -0
- package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +9 -0
- package/dist/chat/ui/intelligent-ui/use-ui-links.js +107 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +20 -0
- package/dist/chat/ui/intelligent-ui/use-ui-queries.js +259 -0
- package/dist/chat/ui/reply-context.d.ts +8 -0
- package/dist/chat/ui/reply-context.js +1 -0
- package/dist/chat/ui/rows/GeneratedImageRow.d.ts +5 -0
- package/dist/chat/ui/rows/GeneratedImageRow.js +121 -0
- package/dist/chat/ui/rows/MessageRows.js +27 -7
- package/dist/chat/ui/rows/Rows.js +4 -2
- package/dist/chat/ui/rows/SubagentRow.js +5 -0
- package/dist/chat/ui/rows/WorkRows.d.ts +4 -1
- package/dist/chat/ui/rows/WorkRows.js +4 -3
- package/dist/host.d.ts +25 -3
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +89 -1
- package/dist/locales/nl/agent-chat.json +89 -1
- package/dist/state/chats.js +9 -0
- package/dist/theme.css +38 -2
- package/package.json +133 -7
- package/src/chat/chat-client.ts +1 -0
- package/src/chat/logic/generated-image.ts +48 -0
- package/src/chat/logic/timeline-target.ts +69 -4
- package/src/chat/logic/timeline.ts +28 -17
- package/src/chat/subagent-conversation.ts +13 -2
- package/src/chat/timeline-flash.ts +5 -3
- package/src/chat/ui/CodeBlock.tsx +70 -21
- package/src/chat/ui/Composer.tsx +22 -5
- package/src/chat/ui/ImageView.tsx +51 -8
- package/src/chat/ui/Markdown.tsx +123 -26
- package/src/chat/ui/MessageActions.tsx +15 -8
- package/src/chat/ui/SubagentTimeline.tsx +26 -1
- package/src/chat/ui/Timeline.tsx +98 -22
- package/src/chat/ui/TimelineMenu.tsx +6 -3
- package/src/chat/ui/file-links.ts +9 -3
- package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +139 -0
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +89 -0
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +21 -0
- package/src/chat/ui/intelligent-ui/UiReply.tsx +531 -0
- package/src/chat/ui/intelligent-ui/block-local.ts +16 -0
- package/src/chat/ui/intelligent-ui/chart-data.ts +67 -0
- package/src/chat/ui/intelligent-ui/node-text.ts +59 -0
- package/src/chat/ui/intelligent-ui/registry.ts +52 -0
- package/src/chat/ui/intelligent-ui/render-context.ts +78 -0
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +321 -0
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +98 -0
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +145 -0
- package/src/chat/ui/intelligent-ui/renderers/data.tsx +187 -0
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +121 -0
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +157 -0
- package/src/chat/ui/intelligent-ui/renderers/structure.tsx +84 -0
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +124 -0
- package/src/chat/ui/intelligent-ui/reply-parts.ts +22 -0
- package/src/chat/ui/intelligent-ui/table-data.ts +104 -0
- package/src/chat/ui/intelligent-ui/tones.ts +37 -0
- package/src/chat/ui/intelligent-ui/use-later.ts +17 -0
- package/src/chat/ui/intelligent-ui/use-ui-links.ts +123 -0
- package/src/chat/ui/intelligent-ui/use-ui-queries.ts +282 -0
- package/src/chat/ui/reply-context.ts +7 -0
- package/src/chat/ui/rows/GeneratedImageRow.tsx +244 -0
- package/src/chat/ui/rows/MessageRows.tsx +69 -7
- package/src/chat/ui/rows/Rows.tsx +4 -2
- package/src/chat/ui/rows/SubagentRow.tsx +5 -0
- package/src/chat/ui/rows/WorkRows.tsx +20 -5
- package/src/host.ts +46 -3
- package/src/locales/en/agent-chat.json +89 -1
- package/src/locales/nl/agent-chat.json +89 -1
- package/src/state/chats.ts +9 -0
- package/src/theme.css +38 -2
|
@@ -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 =
|
|
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
|
|
32
|
-
line
|
|
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
|
|
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(...
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|
|
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
|
-
|
|
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,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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
}
|
package/dist/chat/ui/Composer.js
CHANGED
|
@@ -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
|
|
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
|
|
3
|
+
export interface ImageThumbProps {
|
|
3
4
|
source: ResourceUrl;
|
|
4
5
|
alt: string;
|
|
5
6
|
className?: string;
|
|
6
|
-
|
|
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 [
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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 {};
|