@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.
- 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 +13 -1
- package/dist/chat/logic/timeline-target.js +26 -0
- package/dist/chat/logic/timeline.js +16 -10
- 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/ImageView.d.ts +8 -2
- package/dist/chat/ui/ImageView.js +12 -6
- package/dist/chat/ui/SubagentTimeline.js +6 -2
- package/dist/chat/ui/Timeline.js +67 -7
- 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 +21 -4
- 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 +11 -1
- package/dist/locales/en/agent-chat.json +82 -0
- package/dist/locales/nl/agent-chat.json +82 -0
- package/dist/state/chats.js +9 -0
- package/dist/theme.css +19 -0
- package/package.json +133 -7
- package/src/chat/logic/generated-image.ts +48 -0
- package/src/chat/logic/timeline-target.ts +41 -1
- package/src/chat/logic/timeline.ts +19 -10
- package/src/chat/subagent-conversation.ts +13 -2
- package/src/chat/timeline-flash.ts +5 -3
- package/src/chat/ui/ImageView.tsx +51 -8
- package/src/chat/ui/SubagentTimeline.tsx +24 -1
- package/src/chat/ui/Timeline.tsx +97 -21
- 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 +60 -4
- 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 +27 -1
- package/src/locales/en/agent-chat.json +82 -0
- package/src/locales/nl/agent-chat.json +82 -0
- package/src/state/chats.ts +9 -0
- 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
|
-
//
|
|
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
|
-
|
|
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
|
|
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,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
|
}
|
package/dist/chat/ui/Timeline.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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),
|
|
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;
|