@adecore/agents-react 0.21.1 → 0.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/status-look.js +2 -3
- package/dist/chat/chat-client.d.ts +1 -1
- package/dist/chat/chat-client.js +2 -2
- package/dist/chat/drafts.js +6 -12
- package/dist/chat/logic/ansi.js +5 -1
- package/dist/chat/logic/fork.js +8 -9
- package/dist/chat/logic/limit.js +17 -14
- package/dist/chat/logic/resume-compaction.js +2 -11
- package/dist/chat/logic/scrubber.js +9 -18
- package/dist/chat/logic/timeline.js +15 -10
- package/dist/chat/logic/tool-catalog.js +3 -4
- package/dist/chat/mentions.js +5 -6
- package/dist/chat/runtime-modes.js +2 -4
- package/dist/chat/stash.js +1 -3
- package/dist/chat/thumbnails.js +3 -4
- package/dist/chat/timeline-scroll.js +39 -45
- package/dist/chat/ui/BookmarkSubmenu.js +1 -5
- package/dist/chat/ui/CodeBlock.js +2 -3
- package/dist/chat/ui/Composer.js +57 -69
- package/dist/chat/ui/Markdown.js +11 -11
- package/dist/chat/ui/Pickers.js +5 -9
- package/dist/chat/ui/QuoteButton.js +2 -3
- package/dist/chat/ui/RunSettings.js +8 -2
- package/dist/chat/ui/Scrubber.js +2 -3
- package/dist/chat/ui/SubagentControls.js +1 -3
- package/dist/chat/ui/Timeline.js +15 -18
- package/dist/chat/ui/TimelineMenu.js +3 -9
- package/dist/chat/ui/UnifiedDiff.js +3 -5
- package/dist/chat/ui/VisualFrame.js +3 -4
- package/dist/chat/ui/composer/keys.js +19 -11
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +1 -1
- package/dist/chat/ui/rows/MessageRows.js +2 -3
- package/dist/chat/ui/rows/WorkRows.d.ts +4 -3
- package/dist/chat/ui/rows/WorkRows.js +19 -14
- package/dist/chat/visual-viewport.d.ts +1 -1
- package/dist/chat/visual-viewport.js +9 -8
- package/dist/port-transport.js +4 -8
- package/dist/state/chats.js +10 -14
- package/dist/state/usage.js +44 -49
- package/dist/storage.js +7 -7
- package/dist/theme.css +6 -14
- package/dist/usage/LimitsList.js +1 -2
- package/dist/usage/UsageBreakdown.js +1 -3
- package/dist/usage/UsageDialog.js +2 -4
- package/dist/usage/format.js +6 -6
- package/dist/usage/money.js +1 -5
- package/dist/usage/summary.js +2 -4
- package/package.json +4 -4
- package/src/agents/status-look.ts +2 -3
- package/src/chat/chat-client.ts +2 -3
- package/src/chat/drafts.ts +6 -14
- package/src/chat/logic/ansi.ts +5 -1
- package/src/chat/logic/fork.ts +8 -9
- package/src/chat/logic/limit.ts +17 -16
- package/src/chat/logic/resume-compaction.ts +2 -11
- package/src/chat/logic/scrubber.ts +13 -17
- package/src/chat/logic/timeline.ts +22 -11
- package/src/chat/logic/tool-catalog.ts +7 -6
- package/src/chat/mentions.ts +5 -6
- package/src/chat/runtime-modes.ts +2 -4
- package/src/chat/stash.ts +1 -3
- package/src/chat/thumbnails.ts +3 -4
- package/src/chat/timeline-scroll.ts +43 -52
- package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
- package/src/chat/ui/CodeBlock.tsx +2 -3
- package/src/chat/ui/Composer.tsx +66 -86
- package/src/chat/ui/Markdown.tsx +13 -17
- package/src/chat/ui/Pickers.tsx +5 -9
- package/src/chat/ui/QuoteButton.tsx +2 -3
- package/src/chat/ui/RunSettings.tsx +9 -2
- package/src/chat/ui/Scrubber.tsx +2 -3
- package/src/chat/ui/SubagentControls.tsx +1 -3
- package/src/chat/ui/SubagentTimeline.tsx +1 -2
- package/src/chat/ui/Timeline.tsx +18 -20
- package/src/chat/ui/TimelineMenu.tsx +3 -9
- package/src/chat/ui/UnifiedDiff.tsx +3 -5
- package/src/chat/ui/VisualFrame.tsx +3 -4
- package/src/chat/ui/composer/keys.ts +21 -11
- package/src/chat/ui/intelligent-ui/render-context.ts +2 -2
- package/src/chat/ui/rehype-chips.ts +1 -1
- package/src/chat/ui/rehype-fade.ts +1 -1
- package/src/chat/ui/remark-html-as-text.ts +1 -1
- package/src/chat/ui/rows/MessageRows.tsx +3 -4
- package/src/chat/ui/rows/WorkRows.tsx +86 -69
- package/src/chat/visual-viewport.ts +10 -8
- package/src/host.ts +1 -3
- package/src/port-transport.ts +4 -8
- package/src/state/chats.ts +11 -14
- package/src/state/usage.ts +49 -60
- package/src/storage.ts +7 -7
- package/src/theme.css +6 -14
- package/src/usage/LimitsList.tsx +2 -4
- package/src/usage/UsageBreakdown.tsx +2 -5
- package/src/usage/UsageDialog.tsx +2 -4
- package/src/usage/format.ts +6 -6
- package/src/usage/money.ts +1 -5
- package/src/usage/summary.ts +2 -4
|
@@ -10,9 +10,8 @@ export interface StatusLook {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/*
|
|
13
|
-
* One state, one mark
|
|
14
|
-
*
|
|
15
|
-
* motion with every other animation (`styles.css`).
|
|
13
|
+
* One state, one mark, wherever work is drawn: a sub-agent entry, a task, a summary or a toast. A
|
|
14
|
+
* spinner stops under reduced motion with every other animation.
|
|
16
15
|
*/
|
|
17
16
|
const LOOKS: Record<StatusWord, StatusLook> = {
|
|
18
17
|
running: { icon: LoaderCircle, tone: 'text-status-running', spins: true },
|
package/src/chat/chat-client.ts
CHANGED
|
@@ -63,6 +63,7 @@ export class ChatClient {
|
|
|
63
63
|
private readonly mounted = new MountedRegistry<Mounted>();
|
|
64
64
|
private readonly unsubscribe: Array<() => void> = [];
|
|
65
65
|
private readonly earlier = new Map<string, Promise<void>>();
|
|
66
|
+
private readonly inspections = new Map<string, Promise<ChatAttachResult>>();
|
|
66
67
|
private preferences: ChatPreferencesPayload | null = null;
|
|
67
68
|
|
|
68
69
|
constructor(transport: ChatTransport, sink: ChatSink, providers: ProviderSink | null = null) {
|
|
@@ -89,8 +90,6 @@ export class ChatClient {
|
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
private readonly inspections = new Map<string, Promise<ChatAttachResult>>();
|
|
93
|
-
|
|
94
93
|
inspect(chatId: string): Promise<ChatAttachResult> {
|
|
95
94
|
const pending = this.inspections.get(chatId);
|
|
96
95
|
if (pending) {
|
|
@@ -347,7 +346,7 @@ export class ChatClient {
|
|
|
347
346
|
this.sendPreferences();
|
|
348
347
|
void this.loadProviders();
|
|
349
348
|
void this.loadStatuses();
|
|
350
|
-
void this.mounted.reattachAll((chatId) => this.attach(chatId)
|
|
349
|
+
void this.mounted.reattachAll((chatId) => this.attach(chatId));
|
|
351
350
|
return;
|
|
352
351
|
}
|
|
353
352
|
this.mounted.detachAll();
|
package/src/chat/drafts.ts
CHANGED
|
@@ -28,14 +28,6 @@ const storage = persistedJson<Record<string, DraftRecord>>(
|
|
|
28
28
|
() => chatStorageLegacyKeys('drafts')
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
-
function readAll(): Record<string, DraftRecord> {
|
|
32
|
-
return storage.read();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function store(drafts: Record<string, DraftRecord>): boolean {
|
|
36
|
-
return storage.write(drafts);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
31
|
export function isEmptyDraft(draft: ChatDraft): boolean {
|
|
40
32
|
return draft.text.trim() === '' && draft.attachments.length === 0 && draft.quote === '';
|
|
41
33
|
}
|
|
@@ -46,7 +38,7 @@ export function isEmptyDraft(draft: ChatDraft): boolean {
|
|
|
46
38
|
*/
|
|
47
39
|
export const useDrafts = createChatStore<{ ids: string[] }>(
|
|
48
40
|
() => ({ ids: [] }),
|
|
49
|
-
(store) => store.setState({ ids: Object.keys(
|
|
41
|
+
(store) => store.setState({ ids: Object.keys(storage.read()) })
|
|
50
42
|
);
|
|
51
43
|
|
|
52
44
|
export function useHasDraft(chatId: string): boolean {
|
|
@@ -63,7 +55,7 @@ function trackDraft(chatId: string, held: boolean): void {
|
|
|
63
55
|
|
|
64
56
|
/* An unsent prompt per chat node, kept across reloads so a half-written message is never lost. */
|
|
65
57
|
export function readDraft(chatId: string): ChatDraft {
|
|
66
|
-
const record =
|
|
58
|
+
const record = storage.read()[chatId];
|
|
67
59
|
return record
|
|
68
60
|
? {
|
|
69
61
|
text: record.text,
|
|
@@ -77,11 +69,11 @@ export function readDraft(chatId: string): ChatDraft {
|
|
|
77
69
|
}
|
|
78
70
|
|
|
79
71
|
export function writeDraft(chatId: string, draft: ChatDraft): void {
|
|
80
|
-
const drafts =
|
|
72
|
+
const drafts = storage.read();
|
|
81
73
|
trackDraft(chatId, !isEmptyDraft(draft));
|
|
82
74
|
if (isEmptyDraft(draft)) {
|
|
83
75
|
delete drafts[chatId];
|
|
84
|
-
|
|
76
|
+
storage.write(drafts);
|
|
85
77
|
return;
|
|
86
78
|
}
|
|
87
79
|
drafts[chatId] = {
|
|
@@ -92,12 +84,12 @@ export function writeDraft(chatId: string, draft: ChatDraft): void {
|
|
|
92
84
|
attachments: draft.attachments,
|
|
93
85
|
quote: draft.quote
|
|
94
86
|
};
|
|
95
|
-
if (
|
|
87
|
+
if (storage.write(drafts)) {
|
|
96
88
|
return;
|
|
97
89
|
}
|
|
98
90
|
// A file can outgrow the storage quota; the text is the part worth keeping then.
|
|
99
91
|
drafts[chatId] = { text: draft.text, mentions: draft.mentions, skills: draft.skills, chats: draft.chats, quote: draft.quote };
|
|
100
|
-
|
|
92
|
+
storage.write(drafts);
|
|
101
93
|
}
|
|
102
94
|
|
|
103
95
|
/* Text handed to a draft from outside the composer goes under what was already typed, never over it. */
|
package/src/chat/logic/ansi.ts
CHANGED
|
@@ -164,7 +164,7 @@ function styleOf(state: AnsiState): CSSProperties {
|
|
|
164
164
|
return style;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
function styledTokens(text: string): AnsiToken[] {
|
|
168
168
|
const tokens: AnsiToken[] = [];
|
|
169
169
|
let state: AnsiState = {};
|
|
170
170
|
let position = 0;
|
|
@@ -180,7 +180,11 @@ export function parseAnsi(text: string, options: { limit?: number; tailLines?: n
|
|
|
180
180
|
if (position < text.length) {
|
|
181
181
|
tokens.push({ content: text.slice(position), style: styleOf(state) });
|
|
182
182
|
}
|
|
183
|
+
return tokens;
|
|
184
|
+
}
|
|
183
185
|
|
|
186
|
+
export function parseAnsi(text: string, options: { limit?: number; tailLines?: number } = {}): { tokens: AnsiToken[]; omitted: number } {
|
|
187
|
+
const tokens = styledTokens(text);
|
|
184
188
|
const plain = tokens.map((token) => token.content).join('');
|
|
185
189
|
let start = 0;
|
|
186
190
|
let end = plain.length;
|
package/src/chat/logic/fork.ts
CHANGED
|
@@ -172,20 +172,19 @@ export function forkPayload(input: {
|
|
|
172
172
|
worktree?: ForkWorktreeChoice | null;
|
|
173
173
|
cli?: { original: ForkCliChoice; chosen: ForkCliChoice };
|
|
174
174
|
}): ChatForkPayload {
|
|
175
|
+
const { cli, worktree } = input;
|
|
176
|
+
const provider = cli !== undefined && cli.chosen.provider !== cli.original.provider ? cli.chosen.provider : null;
|
|
177
|
+
const selection = cli !== undefined && (provider !== null || cli.chosen.selection.model !== cli.original.selection.model) ? cli.chosen.selection : null;
|
|
178
|
+
const account = cli?.chosen.account !== undefined && (provider !== null || cli.chosen.account !== cli.original.account) ? cli.chosen.account : null;
|
|
175
179
|
return {
|
|
176
180
|
chatId: input.chatId,
|
|
177
181
|
turnId: input.turnId,
|
|
178
182
|
title: input.title,
|
|
179
183
|
...(input.shape === 'view' ? { asView: true } : {}),
|
|
180
|
-
...(
|
|
181
|
-
...(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
...(input.cli?.chosen.account !== undefined &&
|
|
185
|
-
(input.cli.chosen.provider !== input.cli.original.provider || input.cli.chosen.account !== input.cli.original.account)
|
|
186
|
-
? { account: input.cli.chosen.account }
|
|
187
|
-
: {}),
|
|
188
|
-
...(input.worktree ? { worktree: { branch: input.worktree.branch }, ...(input.worktree.filesAfterTurn ? { filesAfterTurn: true } : {}) } : {})
|
|
184
|
+
...(provider === null ? {} : { provider }),
|
|
185
|
+
...(selection === null ? {} : { selection }),
|
|
186
|
+
...(account === null ? {} : { account }),
|
|
187
|
+
...(worktree ? { worktree: { branch: worktree.branch }, ...(worktree.filesAfterTurn ? { filesAfterTurn: true } : {}) } : {})
|
|
189
188
|
};
|
|
190
189
|
}
|
|
191
190
|
|
package/src/chat/logic/limit.ts
CHANGED
|
@@ -27,20 +27,21 @@ export function limitView(info: Pick<ChatInfo, 'limit' | 'resumeAt' | 'activeTur
|
|
|
27
27
|
detail: resumes === null ? i18next.t('agent-chat:limit.overload.sendAgain') : i18next.t('agent-chat:limit.overload.retries', { time: resumes })
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
resumes
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
30
|
+
const title = i18next.t('agent-chat:limit.usage.title');
|
|
31
|
+
if (resumes !== null) {
|
|
32
|
+
return {
|
|
33
|
+
pill: i18next.t('agent-chat:limit.usage.pillResumes', { time: resumes }),
|
|
34
|
+
title,
|
|
35
|
+
detail: i18next.t('agent-chat:limit.usage.resumes', { time: resumes })
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (limit.resetsAt !== undefined) {
|
|
39
|
+
const resets = formatMoment(limit.resetsAt, now);
|
|
40
|
+
return {
|
|
41
|
+
pill: i18next.t('agent-chat:limit.usage.pillUntil', { time: resets }),
|
|
42
|
+
title,
|
|
43
|
+
detail: i18next.t('agent-chat:limit.usage.resets', { time: resets })
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { pill: i18next.t('agent-chat:limit.usage.pill'), title, detail: null };
|
|
46
47
|
}
|
|
@@ -16,19 +16,10 @@ export interface ResumeCompactionOffer {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function lastTurn(items: readonly ChatItem[]): Extract<ChatItem, { kind: 'turn' }> | null {
|
|
19
|
-
|
|
20
|
-
const item = items[i]!;
|
|
21
|
-
if (item.kind === 'turn') {
|
|
22
|
-
return item;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return null;
|
|
19
|
+
return items.findLast((item): item is Extract<ChatItem, { kind: 'turn' }> => item.kind === 'turn') ?? null;
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
/*
|
|
29
|
-
* Whether to offer compacting before the next message. The caller passes `now`, so this stays pure
|
|
30
|
-
* and the test needs no clock.
|
|
31
|
-
*/
|
|
22
|
+
/* Whether to offer compacting before the next message. */
|
|
32
23
|
export function resumeCompactionOffer({
|
|
33
24
|
info,
|
|
34
25
|
compaction,
|
|
@@ -127,19 +127,10 @@ export function tickOfRow(ticks: readonly ScrubberTick[], rowIndex: number): num
|
|
|
127
127
|
if (ticks.length === 0) {
|
|
128
128
|
return null;
|
|
129
129
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const middle = (low + high) >> 1;
|
|
135
|
-
if (ticks[middle]!.rowIndex <= rowIndex) {
|
|
136
|
-
found = middle;
|
|
137
|
-
low = middle + 1;
|
|
138
|
-
} else {
|
|
139
|
-
high = middle - 1;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return found;
|
|
130
|
+
return Math.max(
|
|
131
|
+
0,
|
|
132
|
+
lastIndexAtOrBelow(ticks.length, (index) => ticks[index]!.rowIndex, rowIndex)
|
|
133
|
+
);
|
|
143
134
|
}
|
|
144
135
|
|
|
145
136
|
/* Per tick, whether any of the rows has a find hit in its stretch. */
|
|
@@ -199,14 +190,14 @@ export function tickWidth(kind: TickKind, distanceFromPointer: number | null): n
|
|
|
199
190
|
return kind === 'wake' ? WIDTH_WAKE_PX : WIDTH_PERSON_PX;
|
|
200
191
|
}
|
|
201
192
|
|
|
202
|
-
/* The last index whose
|
|
203
|
-
function
|
|
193
|
+
/* The last index whose value is at most `limit`, or -1. The values only grow. */
|
|
194
|
+
function lastIndexAtOrBelow(length: number, valueAt: (index: number) => number, limit: number): number {
|
|
204
195
|
let low = 0;
|
|
205
|
-
let high =
|
|
196
|
+
let high = length - 1;
|
|
206
197
|
let found = -1;
|
|
207
198
|
while (low <= high) {
|
|
208
199
|
const middle = (low + high) >> 1;
|
|
209
|
-
if (
|
|
200
|
+
if (valueAt(middle) <= limit) {
|
|
210
201
|
found = middle;
|
|
211
202
|
low = middle + 1;
|
|
212
203
|
} else {
|
|
@@ -216,6 +207,11 @@ function lastAtOrAbove(starts: readonly number[], line: number): number {
|
|
|
216
207
|
return found;
|
|
217
208
|
}
|
|
218
209
|
|
|
210
|
+
/* The last index whose start lies at or above a line, or -1. */
|
|
211
|
+
function lastAtOrAbove(starts: readonly number[], line: number): number {
|
|
212
|
+
return lastIndexAtOrBelow(starts.length, (index) => starts[index]!, line);
|
|
213
|
+
}
|
|
214
|
+
|
|
219
215
|
export interface ReadingPosition {
|
|
220
216
|
/* Where each message's row starts in the thread, in the virtualizer's measurements. */
|
|
221
217
|
starts: readonly number[];
|
|
@@ -88,10 +88,9 @@ export function summarizeGroup(tools: ChatToolItem[]): string {
|
|
|
88
88
|
if (only !== null && toolEntry(only)?.grouped === true) {
|
|
89
89
|
return i18next.t(`agent-chat:group.tools.${only}`, { count: tools.length });
|
|
90
90
|
}
|
|
91
|
-
|
|
92
|
-
files rather than the calls: three edits to one file are one file edited. */
|
|
91
|
+
// Counts the files rather than the calls: three edits to one file are one file edited.
|
|
93
92
|
if (tools.every((tool) => isFileChange(tool.name))) {
|
|
94
|
-
const paths = new Set(tools.map(
|
|
93
|
+
const paths = new Set(tools.map(editedPath));
|
|
95
94
|
return i18next.t('agent-chat:group.edited', { count: paths.size });
|
|
96
95
|
}
|
|
97
96
|
if (only !== null) {
|
|
@@ -100,6 +99,10 @@ export function summarizeGroup(tools: ChatToolItem[]): string {
|
|
|
100
99
|
return i18next.t('agent-chat:group.toolCalls', { count: tools.length });
|
|
101
100
|
}
|
|
102
101
|
|
|
102
|
+
function editedPath(tool: ChatToolItem): string {
|
|
103
|
+
return (tool.input as { file_path?: string })?.file_path ?? tool.id;
|
|
104
|
+
}
|
|
105
|
+
|
|
103
106
|
/*
|
|
104
107
|
* What a folded turn did, a sentence per kind of call in the order it first came: "Read 11 files",
|
|
105
108
|
* "Searched 2 patterns". Every file change reads as one sentence, and calls without a sentence of
|
|
@@ -112,7 +115,7 @@ export function summarizeTurn(tools: readonly ChatToolItem[]): string[] {
|
|
|
112
115
|
let other = 0;
|
|
113
116
|
for (const tool of tools) {
|
|
114
117
|
if (isFileChange(tool.name)) {
|
|
115
|
-
edited.add((tool
|
|
118
|
+
edited.add(editedPath(tool));
|
|
116
119
|
counts.set('agent-chat:group.edited', edited.size);
|
|
117
120
|
} else if (toolEntry(tool.name)?.grouped === true) {
|
|
118
121
|
const key = `agent-chat:group.tools.${tool.name}`;
|
|
@@ -345,12 +348,14 @@ function lastAssistantRow(rows: TimelineRow[]): TimelineRow | null {
|
|
|
345
348
|
return null;
|
|
346
349
|
}
|
|
347
350
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
interface TurnChunk {
|
|
352
|
+
turnId: string | null;
|
|
353
|
+
items: ChatItem[];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* Items grouped by turn, in order of first appearance; runs of turnless items keep their place. */
|
|
357
|
+
function chunksByTurn(items: ChatItem[]): TurnChunk[] {
|
|
358
|
+
const chunks: TurnChunk[] = [];
|
|
354
359
|
const byTurn = new Map<string, ChatItem[]>();
|
|
355
360
|
for (const item of items) {
|
|
356
361
|
if (item.turnId === null) {
|
|
@@ -370,8 +375,14 @@ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions):
|
|
|
370
375
|
}
|
|
371
376
|
bucket.push(item);
|
|
372
377
|
}
|
|
378
|
+
return chunks;
|
|
379
|
+
}
|
|
373
380
|
|
|
374
|
-
|
|
381
|
+
/* The whole thread as rows; items without a turn (older records) are shown as they are. */
|
|
382
|
+
export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions): TimelineRow[] {
|
|
383
|
+
const rows: TimelineRow[] = [];
|
|
384
|
+
const children = groupChildren(items);
|
|
385
|
+
for (const chunk of chunksByTurn(items)) {
|
|
375
386
|
if (chunk.turnId === null) {
|
|
376
387
|
rows.push(...rowsForItems(chunk.items, options, children));
|
|
377
388
|
continue;
|
|
@@ -6,8 +6,10 @@ export interface ToolEntry {
|
|
|
6
6
|
icon: LucideIcon;
|
|
7
7
|
/* The input keys that hold the one line saying what the call is about, best first. */
|
|
8
8
|
summary: readonly string[];
|
|
9
|
-
/*
|
|
10
|
-
|
|
9
|
+
/*
|
|
10
|
+
* Whether a run of calls of this name reads as a sentence of its own (`agent-chat:group.tools.<name>`).
|
|
11
|
+
* One that changes files without a sentence falls to `agent-chat:group.edited`, which counts files.
|
|
12
|
+
*/
|
|
11
13
|
grouped: boolean;
|
|
12
14
|
/* Whether the call writes to a file, which is what a mixed run of calls is named after. */
|
|
13
15
|
changesFiles: boolean;
|
|
@@ -27,10 +29,9 @@ function entry(icon: LucideIcon, summary: readonly string[], rest: Partial<ToolE
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* wrench and to the first string of its input.
|
|
32
|
+
* Every tool the thread has a word for, in one table so a new name is never missing from a lookup. A
|
|
33
|
+
* name that is not here still draws, reads and groups; it falls back to the wrench and to the first
|
|
34
|
+
* string of its input.
|
|
34
35
|
*/
|
|
35
36
|
export const TOOL_CATALOG: Record<string, ToolEntry> = {
|
|
36
37
|
Read: entry(Eye, ['file_path'], { readsImage: true }),
|
package/src/chat/mentions.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
1
|
+
/*
|
|
2
|
+
* What a file dragged out of an app's own list of files carries: the paths, space separated, relative
|
|
3
|
+
* to the folder. The composer takes it next to the image files it already accepts.
|
|
4
|
+
*/
|
|
3
5
|
export const MENTION_DRAG_TYPE = 'application/x-adecore-mention';
|
|
4
6
|
export const LEGACY_MENTION_DRAG_TYPE = 'application/x-ruimte-mention';
|
|
5
7
|
export const MENTION_DRAG_TYPES = [MENTION_DRAG_TYPE, LEGACY_MENTION_DRAG_TYPE] as const;
|
|
@@ -145,10 +147,7 @@ export function presentSkills(text: string, chosen: string[]): string[] {
|
|
|
145
147
|
return present(text, '$', chosen);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
/*
|
|
149
|
-
* The characters a segment stands for, sigil included. Joined, the segments spell the text they
|
|
150
|
-
* were cut from.
|
|
151
|
-
*/
|
|
150
|
+
/* The characters a segment stands for, sigil included; joined, the segments spell the text they were cut from. */
|
|
152
151
|
export function chipText(segment: ChipSegment): string {
|
|
153
152
|
if (segment.kind === 'mention') {
|
|
154
153
|
return `@${segment.path}`;
|
|
@@ -2,10 +2,8 @@ import i18next from 'i18next';
|
|
|
2
2
|
import { RuntimeModeSchema, type RuntimeMode } from '@adecore/agent-contracts';
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
|
-
* The runtime modes in the order they are offered
|
|
6
|
-
*
|
|
7
|
-
* rank from narrowest to widest. Only the ids live here: the words are in `modes.<id>` of the `chat`
|
|
8
|
-
* namespace, read when a surface draws one rather than when this module loads.
|
|
5
|
+
* The runtime modes in the order they are offered: the schema's, which the host reads as the rank
|
|
6
|
+
* from narrowest to widest. Their words are read when a surface draws one, not when this module loads.
|
|
9
7
|
*/
|
|
10
8
|
export const RUNTIME_MODES: readonly RuntimeMode[] = RuntimeModeSchema.options;
|
|
11
9
|
|
package/src/chat/stash.ts
CHANGED
|
@@ -94,9 +94,7 @@ export function stashedFrom(draft: ChatDraft, id: string, now: number): StashedP
|
|
|
94
94
|
|
|
95
95
|
const storage = persistedJson<StashedPrompt[]>(storageKey, parseStash, [], () => chatStorageLegacyKeys('stash'));
|
|
96
96
|
|
|
97
|
-
/*
|
|
98
|
-
* Prompts put aside for later can move to another chat, so the shelf is shared across chats.
|
|
99
|
-
*/
|
|
97
|
+
/* Prompts put aside for later can move to another chat, so the shelf is shared across chats. */
|
|
100
98
|
export const useStash = createChatStore<{ prompts: StashedPrompt[] }>(
|
|
101
99
|
() => ({ prompts: [] }),
|
|
102
100
|
(store) => store.setState({ prompts: storage.read() })
|
package/src/chat/thumbnails.ts
CHANGED
|
@@ -45,10 +45,9 @@ function dataUrlOf(blob: Blob): Promise<string> {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/*
|
|
48
|
-
* A still of the middle of an image, decoded once
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* without `OffscreenCanvas`) falls back to the image itself, which is what it was before.
|
|
48
|
+
* A still of the middle of an image, decoded once: a browser decodes a full data URL again on every
|
|
49
|
+
* paint, so a tall screenshot made typing stutter. An animated image gives its first frame; what
|
|
50
|
+
* cannot be decoded this way (an SVG, a browser without `OffscreenCanvas`) falls back to the image.
|
|
52
51
|
*/
|
|
53
52
|
async function makeThumbnail(upload: ChatAttachmentUpload): Promise<string> {
|
|
54
53
|
try {
|
|
@@ -10,6 +10,10 @@ type Scroller = {
|
|
|
10
10
|
|
|
11
11
|
const scrollers = new Map<string, Scroller>();
|
|
12
12
|
|
|
13
|
+
/* The timeline owns following; the composer subscribes here to show its jump-to-end button. */
|
|
14
|
+
const ends = new Map<string, boolean>();
|
|
15
|
+
const listeners = new Set<() => void>();
|
|
16
|
+
|
|
13
17
|
/* The timeline hands its scroller over so the composer, which never sees it, can page through it. */
|
|
14
18
|
export function registerTimeline(
|
|
15
19
|
chatId: string,
|
|
@@ -29,10 +33,6 @@ export function registerTimeline(
|
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
/* The timeline owns following; the composer subscribes here to show its jump-to-end button. */
|
|
33
|
-
const ends = new Map<string, boolean>();
|
|
34
|
-
const listeners = new Set<() => void>();
|
|
35
|
-
|
|
36
36
|
export function setTimelineAtEnd(chatId: string, atEnd: boolean): void {
|
|
37
37
|
if (ends.get(chatId) === atEnd) {
|
|
38
38
|
return;
|
|
@@ -97,72 +97,63 @@ export function stepTimelineMessage(key: string, direction: -1 | 1): boolean {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
100
|
+
* Jumps asked from outside a thread (the chat's menu, a card that names a turn), keyed with the
|
|
101
|
+
* scope's `keyOf` like the steppers. A thread that is not on screen yet gets its jump when it
|
|
102
|
+
* registers, since the asker may have to show the chat first.
|
|
103
103
|
*/
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
function waitingJumps<T>(): { register(key: string, jump: (target: T) => void): () => void; jump(key: string, target: T): boolean } {
|
|
105
|
+
const jumpers = new Map<string, (target: T) => void>();
|
|
106
|
+
const waiting = new Map<string, T>();
|
|
107
|
+
return {
|
|
108
|
+
register(key, jump) {
|
|
109
|
+
jumpers.set(key, jump);
|
|
110
|
+
if (waiting.has(key)) {
|
|
111
|
+
const target = waiting.get(key)!;
|
|
112
|
+
waiting.delete(key);
|
|
113
|
+
jump(target);
|
|
114
|
+
}
|
|
115
|
+
return () => {
|
|
116
|
+
if (jumpers.get(key) === jump) {
|
|
117
|
+
jumpers.delete(key);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
jump(key, target) {
|
|
122
|
+
const jump = jumpers.get(key);
|
|
123
|
+
if (jump === undefined) {
|
|
124
|
+
waiting.set(key, target);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
jump(target);
|
|
128
|
+
return true;
|
|
117
129
|
}
|
|
118
130
|
};
|
|
119
131
|
}
|
|
120
132
|
|
|
133
|
+
const itemJumps = waitingJumps<string>();
|
|
134
|
+
|
|
135
|
+
export function registerItemJumper(key: string, jump: (itemId: string) => void): () => void {
|
|
136
|
+
return itemJumps.register(key, jump);
|
|
137
|
+
}
|
|
138
|
+
|
|
121
139
|
/* False when that chat has no thread on screen; the jump then waits for the next one that registers. */
|
|
122
140
|
export function jumpToTimelineItem(key: string, itemId: string): boolean {
|
|
123
|
-
|
|
124
|
-
if (jump === undefined) {
|
|
125
|
-
waitingJumps.set(key, itemId);
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
jump(itemId);
|
|
129
|
-
return true;
|
|
141
|
+
return itemJumps.jump(key, itemId);
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
/*
|
|
133
|
-
* A jump to the turn of a chat, for a card that says which turn wrote something: the message that opened
|
|
134
|
-
* it, or the files it changed. Kept like the jump to an item, so a chat that is not on screen yet gets it
|
|
135
|
-
* when its thread registers.
|
|
136
|
-
*/
|
|
144
|
+
/* Which part of a turn a jump lands on: the message that opened it, or the files it changed. */
|
|
137
145
|
export type TurnTarget = 'prompt' | 'changes';
|
|
138
146
|
|
|
139
147
|
type TurnJump = (turnId: string, target: TurnTarget) => void;
|
|
140
|
-
const
|
|
141
|
-
const waitingTurnJumps = new Map<string, { turnId: string; target: TurnTarget }>();
|
|
148
|
+
const turnJumps = waitingJumps<{ turnId: string; target: TurnTarget }>();
|
|
142
149
|
|
|
143
150
|
export function registerTurnJumper(key: string, jump: TurnJump): () => void {
|
|
144
|
-
|
|
145
|
-
const waiting = waitingTurnJumps.get(key);
|
|
146
|
-
if (waiting !== undefined) {
|
|
147
|
-
waitingTurnJumps.delete(key);
|
|
148
|
-
jump(waiting.turnId, waiting.target);
|
|
149
|
-
}
|
|
150
|
-
return () => {
|
|
151
|
-
if (turnJumpers.get(key) === jump) {
|
|
152
|
-
turnJumpers.delete(key);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
151
|
+
return turnJumps.register(key, ({ turnId, target }) => jump(turnId, target));
|
|
155
152
|
}
|
|
156
153
|
|
|
157
154
|
/* False when that chat has no thread on screen; the jump then waits for the next one that registers. */
|
|
158
155
|
export function jumpToTimelineTurn(key: string, turnId: string, target: TurnTarget): boolean {
|
|
159
|
-
|
|
160
|
-
if (jump === undefined) {
|
|
161
|
-
waitingTurnJumps.set(key, { turnId, target });
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
jump(turnId, target);
|
|
165
|
-
return true;
|
|
156
|
+
return turnJumps.jump(key, { turnId, target });
|
|
166
157
|
}
|
|
167
158
|
|
|
168
159
|
/* How near the top, in screens, the page before is asked for, so it is there before a reader reaches the edge. */
|
|
@@ -9,11 +9,7 @@ import { useChatScope } from '../../scope';
|
|
|
9
9
|
import { useChatRow } from '../../state/chats';
|
|
10
10
|
import { Icon, Menu } from '@adecore/ui';
|
|
11
11
|
|
|
12
|
-
/*
|
|
13
|
-
* "Bookmarks" in the menu of a chat node or a chat view: the chat's bookmarks in the order of the
|
|
14
|
-
* thread, each a jump to its message, with rename and remove on the row. A chat without bookmarks
|
|
15
|
-
* draws nothing.
|
|
16
|
-
*/
|
|
12
|
+
/* A chat's bookmarks in thread order, each a jump to its message with rename and remove; nothing without bookmarks. */
|
|
17
13
|
export function BookmarkSubmenu({ chatId }: { chatId: string }) {
|
|
18
14
|
const { t } = useTranslation('agent-chat');
|
|
19
15
|
const scope = useChatScope();
|
|
@@ -123,9 +123,8 @@ const CodeLine = memo(function CodeLine({ tokens }: { tokens: CodeToken[] }) {
|
|
|
123
123
|
|
|
124
124
|
/*
|
|
125
125
|
* A fenced block, highlighted a line at a time. Until shiki and the grammar are there it holds its
|
|
126
|
-
* place invisibly
|
|
127
|
-
*
|
|
128
|
-
* closes, so closing it does not draw it again.
|
|
126
|
+
* place invisibly, so bare code never turns colored. An open fence is tokenized as it grows, and
|
|
127
|
+
* closing it does not draw it again.
|
|
129
128
|
*/
|
|
130
129
|
export function CodeBlock({ code, lang, actions }: { code: string; lang: string; actions?: ReactNode }) {
|
|
131
130
|
const { t } = useTranslation('agent-chat');
|