@adecore/agents-react 0.35.5 → 0.37.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 +5 -0
- package/dist/chat/chat-client.js +91 -20
- package/dist/chat/index.d.ts +1 -0
- package/dist/chat/index.js +2 -1
- package/dist/chat/logic/picker-entries.js +11 -6
- package/dist/chat/logic/scrubber.d.ts +4 -2
- package/dist/chat/logic/scrubber.js +3 -3
- package/dist/chat/pending-prompts.d.ts +15 -0
- package/dist/chat/pending-prompts.js +93 -0
- package/dist/chat/reading-width.js +6 -2
- package/dist/chat/ui/Pickers.js +6 -3
- package/dist/chat/ui/PromptComposer.d.ts +2 -2
- package/dist/chat/ui/PromptComposer.js +92 -98
- package/dist/chat/ui/RunSettings.js +2 -2
- package/dist/chat/ui/SubagentTimeline.js +128 -182
- package/dist/chat/ui/Timeline.js +86 -60
- package/dist/chat/ui/code-lines.d.ts +5 -0
- package/dist/chat/ui/code-lines.js +37 -0
- package/dist/chat/ui/composer/ComposerControls.js +1 -1
- package/dist/chat/ui/composer/use-pending-requests.d.ts +2 -2
- package/dist/chat/ui/composer/use-pending-requests.js +29 -37
- package/dist/chat/ui/highlighter.d.ts +1 -1
- package/dist/chat/ui/highlighter.js +4 -2
- package/dist/chat/ui/rows/MessageRows.js +1 -1
- package/dist/locales/en/agent-chat.d.ts +1 -0
- package/dist/locales/en/agent-chat.js +1 -0
- package/dist/locales/nl/agent-chat.d.ts +1 -0
- package/dist/locales/nl/agent-chat.js +1 -0
- package/dist/prompts/logic/subjects.d.ts +5 -0
- package/dist/prompts/ui/PromptCard.js +1 -1
- package/dist/prompts/ui/PromptView.js +104 -95
- package/dist/state/chats.d.ts +4 -0
- package/dist/state/chats.js +84 -48
- package/dist/theme.css +1 -1
- package/package.json +8 -8
- package/src/chat/chat-client.ts +92 -21
- package/src/chat/index.ts +2 -0
- package/src/chat/logic/picker-entries.ts +6 -4
- package/src/chat/logic/scrubber.ts +8 -6
- package/src/chat/pending-prompts.ts +108 -0
- package/src/chat/reading-width.ts +9 -2
- package/src/chat/ui/Pickers.tsx +3 -6
- package/src/chat/ui/PromptComposer.tsx +10 -7
- package/src/chat/ui/RunSettings.tsx +2 -2
- package/src/chat/ui/SubagentTimeline.tsx +1 -24
- package/src/chat/ui/Timeline.tsx +66 -40
- package/src/chat/ui/code-lines.ts +37 -0
- package/src/chat/ui/composer/ComposerControls.tsx +1 -1
- package/src/chat/ui/composer/use-pending-requests.ts +13 -21
- package/src/chat/ui/highlighter.ts +4 -3
- package/src/chat/ui/rows/MessageRows.tsx +1 -1
- package/src/locales/en/agent-chat.ts +1 -0
- package/src/locales/nl/agent-chat.ts +1 -0
- package/src/prompts/logic/subjects.ts +4 -1
- package/src/prompts/ui/PromptCard.tsx +1 -1
- package/src/prompts/ui/PromptView.tsx +7 -1
- package/src/state/chats.ts +63 -18
- package/src/theme.css +1 -1
package/src/chat/chat-client.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface ChatSendExtras {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
interface Mounted extends ChatOpenOptions, MountedEntry {
|
|
45
|
+
observerOnly?: boolean;
|
|
45
46
|
// The last place in the chat's stream the store holds, so a reattach asks only for what came after.
|
|
46
47
|
seq?: number;
|
|
47
48
|
}
|
|
@@ -66,8 +67,10 @@ export class ChatClient {
|
|
|
66
67
|
private readonly mounted = new MountedRegistry<Mounted>();
|
|
67
68
|
private readonly unsubscribe: Array<() => void> = [];
|
|
68
69
|
private readonly earlier = new Map<string, Promise<void>>();
|
|
70
|
+
private readonly observers = new Map<string, number>();
|
|
69
71
|
private readonly inspections = new Map<string, Promise<ChatAttachResult>>();
|
|
70
72
|
private preferences: ChatPreferencesPayload | null = null;
|
|
73
|
+
private checkpoints = new WeakMap<object, number>();
|
|
71
74
|
|
|
72
75
|
constructor(transport: ChatTransport, sink: ChatSink, providers: ProviderSink | null = null) {
|
|
73
76
|
this.transport = transport;
|
|
@@ -75,10 +78,10 @@ export class ChatClient {
|
|
|
75
78
|
this.providers = providers;
|
|
76
79
|
this.unsubscribe.push(
|
|
77
80
|
transport.on('chat.event', ({ chatId, event, seq }) => {
|
|
81
|
+
const previous = this.sequence(chatId);
|
|
78
82
|
this.sink.apply(chatId, event);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
entry.seq = seq;
|
|
83
|
+
if (previous !== undefined || (!this.sink.historyIdentity && this.mounted.get(chatId)?.attached)) {
|
|
84
|
+
this.rememberSequence(chatId, seq);
|
|
82
85
|
}
|
|
83
86
|
}),
|
|
84
87
|
// Every chat on the machine, attached or not, so a node waiting on a person says so on a view nobody has open.
|
|
@@ -109,6 +112,31 @@ export class ChatClient {
|
|
|
109
112
|
return read;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
/* Shares a live attachment without creating a chat; a visible composer and several ancestors may hold it together. */
|
|
116
|
+
observe(chatId: string): () => void {
|
|
117
|
+
this.observers.set(chatId, (this.observers.get(chatId) ?? 0) + 1);
|
|
118
|
+
if (!this.mounted.has(chatId)) {
|
|
119
|
+
this.mounted.set(chatId, { attached: false, observerOnly: true });
|
|
120
|
+
void this.attach(chatId).catch(() => undefined);
|
|
121
|
+
}
|
|
122
|
+
let released = false;
|
|
123
|
+
return () => {
|
|
124
|
+
if (released) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
released = true;
|
|
128
|
+
const remaining = (this.observers.get(chatId) ?? 1) - 1;
|
|
129
|
+
if (remaining > 0) {
|
|
130
|
+
this.observers.set(chatId, remaining);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.observers.delete(chatId);
|
|
134
|
+
if (this.mounted.get(chatId)?.observerOnly) {
|
|
135
|
+
void this.detach(chatId);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
112
140
|
/* Answers false when the transport is not connected; the chat opens once it is. */
|
|
113
141
|
async open(chatId: string, options: ChatOpenOptions): Promise<boolean> {
|
|
114
142
|
this.mounted.set(chatId, { ...options, attached: false });
|
|
@@ -135,7 +163,8 @@ export class ChatClient {
|
|
|
135
163
|
}
|
|
136
164
|
// The account belonged to the CLI the chat leaves, and the seq to its stream: the new chat's
|
|
137
165
|
// stream starts over, so offering it the old seq would read as nothing missed and reset nothing.
|
|
138
|
-
const { attached: _attached, account: _account, seq: _seq, ...options } = entry;
|
|
166
|
+
const { attached: _attached, account: _account, seq: _seq, observerOnly: _observerOnly, ...options } = entry;
|
|
167
|
+
this.rememberSequence(chatId, undefined);
|
|
139
168
|
this.mounted.delete(chatId);
|
|
140
169
|
await this.transport.request('chat.kill', { chatId });
|
|
141
170
|
try {
|
|
@@ -157,6 +186,10 @@ export class ChatClient {
|
|
|
157
186
|
|
|
158
187
|
async detach(chatId: string): Promise<void> {
|
|
159
188
|
const entry = this.mounted.get(chatId);
|
|
189
|
+
if (entry && this.observers.has(chatId)) {
|
|
190
|
+
entry.observerOnly = true;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
160
193
|
this.mounted.delete(chatId);
|
|
161
194
|
if (!entry?.attached) {
|
|
162
195
|
return;
|
|
@@ -268,13 +301,17 @@ export class ChatClient {
|
|
|
268
301
|
}
|
|
269
302
|
const read = this.transport
|
|
270
303
|
.request('chat.history', { chatId, cursor, limit: HISTORY_PAGE })
|
|
271
|
-
.then((page) =>
|
|
304
|
+
.then((page) => {
|
|
305
|
+
const seq = this.sequence(chatId);
|
|
306
|
+
this.sink.prepend(chatId, cursor, page);
|
|
307
|
+
this.rememberSequence(chatId, seq);
|
|
308
|
+
})
|
|
272
309
|
.catch(async (e: unknown) => {
|
|
273
310
|
const entry = this.mounted.get(chatId);
|
|
274
311
|
if (errorCode(e) !== 'history-expired' || !entry) {
|
|
275
312
|
throw e;
|
|
276
313
|
}
|
|
277
|
-
|
|
314
|
+
this.rememberSequence(chatId, undefined);
|
|
278
315
|
await this.attach(chatId);
|
|
279
316
|
})
|
|
280
317
|
.finally(() => this.earlier.delete(chatId));
|
|
@@ -292,6 +329,8 @@ export class ChatClient {
|
|
|
292
329
|
off();
|
|
293
330
|
}
|
|
294
331
|
this.unsubscribe.length = 0;
|
|
332
|
+
this.observers.clear();
|
|
333
|
+
this.checkpoints = new WeakMap();
|
|
295
334
|
for (const chatId of [...this.mounted.keys()]) {
|
|
296
335
|
void this.detach(chatId);
|
|
297
336
|
}
|
|
@@ -307,20 +346,22 @@ export class ChatClient {
|
|
|
307
346
|
if (!entry) {
|
|
308
347
|
return;
|
|
309
348
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
349
|
+
if (!entry.observerOnly) {
|
|
350
|
+
await this.transport.request('chat.create', {
|
|
351
|
+
chatId,
|
|
352
|
+
provider: entry.provider,
|
|
353
|
+
account: entry.account,
|
|
354
|
+
cwd: entry.cwd,
|
|
355
|
+
resume: entry.resume,
|
|
356
|
+
selection: entry.selection,
|
|
357
|
+
runtimeMode: entry.runtimeMode
|
|
358
|
+
});
|
|
359
|
+
}
|
|
319
360
|
// Closed or opened again in the meantime: an opening that is still there attaches itself.
|
|
320
361
|
if (this.mounted.get(chatId) !== entry) {
|
|
321
362
|
return;
|
|
322
363
|
}
|
|
323
|
-
const since =
|
|
364
|
+
const since = this.sequence(chatId);
|
|
324
365
|
const result = await this.transport.request('chat.attach', { chatId, historyLimit: HISTORY_PAGE, ...(since === undefined ? {} : { since }) });
|
|
325
366
|
const current = this.mounted.get(chatId);
|
|
326
367
|
if (current !== entry) {
|
|
@@ -330,24 +371,54 @@ export class ChatClient {
|
|
|
330
371
|
}
|
|
331
372
|
return;
|
|
332
373
|
}
|
|
333
|
-
|
|
334
|
-
if (result.
|
|
335
|
-
|
|
374
|
+
// A cache eviction while the request was in flight also invalidates its replay base.
|
|
375
|
+
if (result.events && since !== undefined && this.sequence(chatId) === undefined) {
|
|
376
|
+
await this.attach(chatId);
|
|
377
|
+
return;
|
|
336
378
|
}
|
|
379
|
+
entry.attached = true;
|
|
337
380
|
if (result.events && since !== undefined) {
|
|
338
|
-
|
|
339
|
-
this.sink.
|
|
381
|
+
if (this.sink.replay) {
|
|
382
|
+
this.sink.replay(chatId, result.events);
|
|
383
|
+
} else {
|
|
384
|
+
for (const event of result.events) {
|
|
385
|
+
this.sink.apply(chatId, event);
|
|
386
|
+
}
|
|
340
387
|
}
|
|
341
388
|
} else {
|
|
342
389
|
const page =
|
|
343
390
|
result.history === undefined ? undefined : { history: result.history, pending: result.pending ?? [], subagents: result.subagents ?? [] };
|
|
344
391
|
this.sink.reset(chatId, result.info, result.items, page);
|
|
345
392
|
}
|
|
393
|
+
this.rememberSequence(chatId, result.seq);
|
|
346
394
|
// A host without bookmarks or visuals sends none, and then this chat has none.
|
|
347
395
|
this.sink.bookmarks(chatId, result.bookmarks ?? []);
|
|
348
396
|
this.sink.visuals(chatId, result.visuals ?? []);
|
|
349
397
|
}
|
|
350
398
|
|
|
399
|
+
private sequence(chatId: string): number | undefined {
|
|
400
|
+
if (!this.sink.historyIdentity) {
|
|
401
|
+
return this.mounted.get(chatId)?.seq;
|
|
402
|
+
}
|
|
403
|
+
const identity = this.sink.historyIdentity(chatId);
|
|
404
|
+
return identity === undefined ? undefined : this.checkpoints.get(identity);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private rememberSequence(chatId: string, seq: number | undefined): void {
|
|
408
|
+
const entry = this.mounted.get(chatId);
|
|
409
|
+
if (entry) {
|
|
410
|
+
entry.seq = seq;
|
|
411
|
+
}
|
|
412
|
+
const identity = this.sink.historyIdentity?.(chatId);
|
|
413
|
+
if (identity !== undefined) {
|
|
414
|
+
if (seq === undefined) {
|
|
415
|
+
this.checkpoints.delete(identity);
|
|
416
|
+
} else {
|
|
417
|
+
this.checkpoints.set(identity, seq);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
351
422
|
private sendPreferences(): void {
|
|
352
423
|
if (this.preferences === null) {
|
|
353
424
|
return;
|
package/src/chat/index.ts
CHANGED
|
@@ -78,3 +78,5 @@ export {
|
|
|
78
78
|
} from './subagent-view';
|
|
79
79
|
export { stepTimelineMessage, jumpToTimelineItem, type TurnTarget, registerTurnJumper, jumpToTimelineTurn, type ItemJumpOptions } from './timeline-scroll';
|
|
80
80
|
export { type StatusWord, statusLookOf, taskStatusWord, type StatusLook, type TaskState } from '../agents/status-look';
|
|
81
|
+
|
|
82
|
+
export { ChatPromptFeed } from './pending-prompts';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentKind, ModelInfo, ProviderInfo } from '@adecore/agent-contracts';
|
|
2
|
+
import { nameCollator } from '@adecore/ui/format';
|
|
2
3
|
|
|
3
4
|
/*
|
|
4
5
|
* A provider with more live models than this keeps the list short: the model that is picked and the default,
|
|
@@ -21,6 +22,8 @@ export interface PickerCurrent {
|
|
|
21
22
|
export type FavoritesOf = (kind: AgentKind) => ReadonlySet<string>;
|
|
22
23
|
|
|
23
24
|
const NO_FAVORITES: FavoritesOf = () => new Set();
|
|
25
|
+
const VENDOR_ORDER: Intl.CollatorOptions = { sensitivity: 'base' };
|
|
26
|
+
const MODEL_ORDER: Intl.CollatorOptions = { sensitivity: 'base', numeric: true };
|
|
24
27
|
|
|
25
28
|
function matches(model: ModelInfo, query: string): boolean {
|
|
26
29
|
return model.name.toLowerCase().includes(query) || model.slug.toLowerCase().includes(query) || (model.vendor?.toLowerCase().includes(query) ?? false);
|
|
@@ -34,14 +37,13 @@ export function modelsByVendor(models: ModelInfo[]): ModelInfo[] {
|
|
|
34
37
|
if (!models.some((model) => model.vendor !== undefined)) {
|
|
35
38
|
return models;
|
|
36
39
|
}
|
|
40
|
+
const vendorOrder = nameCollator(VENDOR_ORDER).compare;
|
|
41
|
+
const modelOrder = nameCollator(MODEL_ORDER).compare;
|
|
37
42
|
return [...models].sort((a, b) => {
|
|
38
43
|
if (a.vendor === undefined || b.vendor === undefined) {
|
|
39
44
|
return a.vendor === b.vendor ? 0 : a.vendor === undefined ? 1 : -1;
|
|
40
45
|
}
|
|
41
|
-
return (
|
|
42
|
-
a.vendor.localeCompare(b.vendor, undefined, { sensitivity: 'base' }) ||
|
|
43
|
-
a.name.localeCompare(b.name, undefined, { sensitivity: 'base', numeric: true })
|
|
44
|
-
);
|
|
46
|
+
return vendorOrder(a.vendor, b.vendor) || modelOrder(a.name, b.name);
|
|
45
47
|
});
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -207,14 +207,16 @@ function lastIndexAtOrBelow(length: number, valueAt: (index: number) => number,
|
|
|
207
207
|
return found;
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
type MessageStarts = Pick<readonly number[], 'length' | 'at'>;
|
|
211
|
+
|
|
210
212
|
/* The last index whose start lies at or above a line, or -1. */
|
|
211
|
-
function lastAtOrAbove(starts:
|
|
212
|
-
return lastIndexAtOrBelow(starts.length, (index) => starts
|
|
213
|
+
function lastAtOrAbove(starts: MessageStarts, line: number): number {
|
|
214
|
+
return lastIndexAtOrBelow(starts.length, (index) => starts.at(index)!, line);
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
export interface ReadingPosition {
|
|
216
218
|
/* Where each message's row starts in the thread, in the virtualizer's measurements. */
|
|
217
|
-
starts:
|
|
219
|
+
starts: MessageStarts;
|
|
218
220
|
scrollTop: number;
|
|
219
221
|
/* The height of the thread a person can read, without what the composer covers. */
|
|
220
222
|
visibleHeight: number;
|
|
@@ -231,13 +233,13 @@ export interface MessageRange {
|
|
|
231
233
|
*/
|
|
232
234
|
export function messagesInView({ starts, scrollTop, visibleHeight }: ReadingPosition): MessageRange | null {
|
|
233
235
|
const bottom = scrollTop + visibleHeight;
|
|
234
|
-
if (starts.length === 0 || visibleHeight <= 0 || starts
|
|
236
|
+
if (starts.length === 0 || visibleHeight <= 0 || starts.at(0)! >= bottom) {
|
|
235
237
|
return null;
|
|
236
238
|
}
|
|
237
239
|
const first = Math.max(0, lastAtOrAbove(starts, scrollTop));
|
|
238
240
|
let last = lastAtOrAbove(starts, bottom);
|
|
239
241
|
// A row that starts on the very last line has not a pixel on screen yet.
|
|
240
|
-
if (starts
|
|
242
|
+
if (starts.at(last)! >= bottom) {
|
|
241
243
|
last--;
|
|
242
244
|
}
|
|
243
245
|
return { first, last: Math.max(first, last) };
|
|
@@ -253,7 +255,7 @@ export function slotInView(slot: TickSlot, range: MessageRange | null): boolean
|
|
|
253
255
|
* message's start at the top, so back from there is the one before it, and back from the middle of
|
|
254
256
|
* a long answer is the question it belongs to. A pixel of slack absorbs rounding in the scroller.
|
|
255
257
|
*/
|
|
256
|
-
export function stepMessage(starts:
|
|
258
|
+
export function stepMessage(starts: MessageStarts, eligible: (index: number) => boolean, scrollTop: number, direction: -1 | 1): number | null {
|
|
257
259
|
if (direction === -1) {
|
|
258
260
|
for (let i = lastAtOrAbove(starts, scrollTop - 1.5); i >= 0; i--) {
|
|
259
261
|
if (eligible(i)) {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { CHAT_REQUEST_LIMITS, type ChatInfo } from '@adecore/agent-contracts';
|
|
2
|
+
import type { PromptSubject } from '../prompts/logic/subjects';
|
|
3
|
+
import type { PendingPrompt } from '../prompts/logic/prompts';
|
|
4
|
+
import type { ChatScope } from '../scope';
|
|
5
|
+
import { useChats, waitingRequestsOf } from '../state/chats';
|
|
6
|
+
import { subagentTitle } from './subagent-list';
|
|
7
|
+
|
|
8
|
+
type ChatPromptSubject = Extract<PromptSubject, { kind: 'chat' }>;
|
|
9
|
+
|
|
10
|
+
function stillWaiting(item: PendingPrompt, info: ChatInfo): boolean {
|
|
11
|
+
return (
|
|
12
|
+
info.status === 'needs-you' &&
|
|
13
|
+
(info.requests === undefined ||
|
|
14
|
+
info.requests.length >= CHAT_REQUEST_LIMITS.perChat ||
|
|
15
|
+
info.requests.some((request) => request.requestId === item.requestId))
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* Follows descendant requests while the main chat is open, including children discovered in later events or older history pages. */
|
|
20
|
+
export class ChatPromptFeed {
|
|
21
|
+
private readonly scope: ChatScope;
|
|
22
|
+
private readonly chatId: string;
|
|
23
|
+
private readonly changed: (subjects: PromptSubject[]) => void;
|
|
24
|
+
private readonly children = new Map<string, () => void>();
|
|
25
|
+
private unsubscribe: (() => void) | undefined;
|
|
26
|
+
private current: ChatPromptSubject[] | null = null;
|
|
27
|
+
private disposed = false;
|
|
28
|
+
|
|
29
|
+
constructor(scope: ChatScope, chatId: string, changed: (subjects: PromptSubject[]) => void) {
|
|
30
|
+
this.scope = scope;
|
|
31
|
+
this.chatId = chatId;
|
|
32
|
+
this.changed = changed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
start(): void {
|
|
36
|
+
this.unsubscribe = useChats.subscribe(() => this.update());
|
|
37
|
+
this.update();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
dispose(): void {
|
|
41
|
+
this.disposed = true;
|
|
42
|
+
this.unsubscribe?.();
|
|
43
|
+
for (const release of this.children.values()) {
|
|
44
|
+
release();
|
|
45
|
+
}
|
|
46
|
+
this.children.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private update(): void {
|
|
50
|
+
if (this.disposed) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const store = useChats.getState();
|
|
54
|
+
const subjects = new Map<string, ChatPromptSubject>();
|
|
55
|
+
const seen = new Set<string>();
|
|
56
|
+
const visit = (chatId: string, source?: ChatPromptSubject['source']): void => {
|
|
57
|
+
if (seen.has(chatId)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
seen.add(chatId);
|
|
61
|
+
const key = this.scope.keyOf(chatId);
|
|
62
|
+
const row = store.byKey[key];
|
|
63
|
+
const info = store.statusByKey[key]?.info ?? row?.info;
|
|
64
|
+
for (const item of waitingRequestsOf(row)) {
|
|
65
|
+
if (info !== undefined && (chatId === this.chatId || stillWaiting(item, info))) {
|
|
66
|
+
subjects.set(`${chatId}:${item.requestId}`, { kind: 'chat', nodeId: chatId, item, ...(source === undefined ? {} : { source }) });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const item of Object.values(row?.structure ?? {})) {
|
|
70
|
+
if (item.kind !== 'subagent' || item.childId === undefined || seen.has(item.childId)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const childInfo = store.statusByKey[this.scope.keyOf(item.childId)]?.info;
|
|
74
|
+
if (item.status !== 'running' && childInfo?.status !== 'needs-you') {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!this.children.has(item.childId)) {
|
|
78
|
+
this.children.set(item.childId, this.scope.chats.observe(item.childId));
|
|
79
|
+
}
|
|
80
|
+
visit(item.childId, { label: subagentTitle(item), provider: childInfo?.provider });
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
visit(this.chatId);
|
|
84
|
+
for (const [chatId, release] of this.children) {
|
|
85
|
+
if (!seen.has(chatId)) {
|
|
86
|
+
this.children.delete(chatId);
|
|
87
|
+
release();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const next = [...subjects.values()];
|
|
91
|
+
if (
|
|
92
|
+
this.current === null ||
|
|
93
|
+
next.length !== this.current.length ||
|
|
94
|
+
next.some((subject, index) => {
|
|
95
|
+
const previous = this.current?.[index];
|
|
96
|
+
return (
|
|
97
|
+
subject.nodeId !== previous?.nodeId ||
|
|
98
|
+
subject.item !== previous.item ||
|
|
99
|
+
subject.source?.label !== previous.source?.label ||
|
|
100
|
+
subject.source?.provider !== previous.source?.provider
|
|
101
|
+
);
|
|
102
|
+
})
|
|
103
|
+
) {
|
|
104
|
+
this.current = next;
|
|
105
|
+
this.changed(next);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -24,10 +24,17 @@ export function readingMeasure(width: ChatReadingWidth): number {
|
|
|
24
24
|
return MEASURES[width];
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/*
|
|
27
|
+
/* A wide visual outgrows the reading column, but only a full-width chat lets it run past this. */
|
|
28
|
+
const WIDE_MEASURE = 1200;
|
|
29
|
+
|
|
30
|
+
/* What `.chat-column-content`, `.chat-wide-content` and `.chat-composer-content` read their width from. */
|
|
28
31
|
export function readingWidthStyle(width: ChatReadingWidth): CSSProperties {
|
|
29
32
|
const measure = MEASURES[width];
|
|
30
|
-
|
|
33
|
+
const bounded = Number.isFinite(measure);
|
|
34
|
+
return {
|
|
35
|
+
'--chat-measure': bounded ? `${measure}px` : '100%',
|
|
36
|
+
'--chat-wide-measure': bounded ? `${WIDE_MEASURE}px` : '100%'
|
|
37
|
+
} as CSSProperties;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
export function readingWidthLabel(width: ChatReadingWidth): string {
|
package/src/chat/ui/Pickers.tsx
CHANGED
|
@@ -13,15 +13,12 @@ import { ModelMarks } from './ModelMarks';
|
|
|
13
13
|
import { pickerEntriesOf, type PickerEntry } from '../logic/picker-entries';
|
|
14
14
|
import { favoriteModelsOf, toggleFavoriteModel, useChatPreferences } from '../preferences';
|
|
15
15
|
import { forgetStashed, STASH_SHORTCUT, useStash, type StashedPrompt } from '../stash';
|
|
16
|
-
import { Icon, IconButton, Menu, Popover, Tooltip } from '@adecore/ui';
|
|
16
|
+
import { Button, Icon, IconButton, Menu, Popover, Tooltip } from '@adecore/ui';
|
|
17
17
|
import { useHostMachineLabel } from '../../host-hooks';
|
|
18
18
|
|
|
19
19
|
const triggerClass =
|
|
20
20
|
'flex h-7 shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-2 text-xs text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text';
|
|
21
21
|
|
|
22
|
-
const chipClass =
|
|
23
|
-
'flex h-6 min-w-0 shrink items-center gap-1.5 whitespace-nowrap rounded-md bg-surface-hover px-2 text-xs text-text hover:bg-surface-active data-[popup-open]:bg-surface-active';
|
|
24
|
-
|
|
25
22
|
interface ModelPickerProps {
|
|
26
23
|
/* Whose models may be picked: every installed chat CLI before the first message, the chat's own after it. */
|
|
27
24
|
providers: ProviderInfo[];
|
|
@@ -35,7 +32,7 @@ interface ModelPickerProps {
|
|
|
35
32
|
kbd?: string | null;
|
|
36
33
|
/* Where the popup opens: over the composer by default, below the trigger in a form. */
|
|
37
34
|
side?: 'top' | 'bottom';
|
|
38
|
-
/* `chip` names the provider beside the model
|
|
35
|
+
/* `chip` names the provider beside the model on a secondary button, for a picker that sits in a card or a form. */
|
|
39
36
|
trigger?: 'plain' | 'chip';
|
|
40
37
|
/* Where the focus goes when the popup closes; by default back to the trigger. */
|
|
41
38
|
finalFocus?: RefObject<HTMLElement | null>;
|
|
@@ -149,7 +146,7 @@ export function ModelPicker({
|
|
|
149
146
|
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
150
147
|
<Tooltip label={owner ? `${owner.name} · ${current}` : t('pickers.model.choose')} kbd={kbd ?? undefined}>
|
|
151
148
|
{trigger === 'chip' ? (
|
|
152
|
-
<Popover.Trigger className=
|
|
149
|
+
<Popover.Trigger render={<Button variant="secondary" size="sm" className="min-w-0 shrink gap-1.5" />}>
|
|
153
150
|
{chip.mark === 'logo' ? <ProviderLogo provider={provider as UsageProvider} size={12} /> : <AgentIcon kind={provider} size={12} />}
|
|
154
151
|
<span className="truncate">{chip.name}</span>
|
|
155
152
|
<span className="max-w-32 truncate text-text-muted">{chip.model}</span>
|
|
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
3
3
|
import { ArrowDown, Hand, MessageCircleQuestionMark } from 'lucide-react';
|
|
4
4
|
import type { HoldRelease } from '../prompt-hold';
|
|
5
5
|
import { focusPromptHeading, focusPromptStart } from '../../prompts/logic/focus';
|
|
6
|
-
import { orderPrompts
|
|
6
|
+
import { orderPrompts } from '../../prompts/logic/prompts';
|
|
7
7
|
import { answerPrompt, isBlockingSubject, promptCreatedAt, promptIdOf, type PromptSubject } from '../../prompts/logic/subjects';
|
|
8
8
|
import { useFocusAfterAnswer } from '../../prompts/logic/useFocusAfterAnswer';
|
|
9
9
|
import { usePromptSession } from '../../prompts/logic/usePromptSession';
|
|
@@ -12,6 +12,7 @@ import { useChatHost } from '../../host';
|
|
|
12
12
|
import { useChatScope } from '../../scope';
|
|
13
13
|
import { useChatActions } from '../actions';
|
|
14
14
|
import { Button, Icon } from '@adecore/ui';
|
|
15
|
+
import { useProviders } from '../../state/providers';
|
|
15
16
|
import { useHostExtraPrompts } from '../../host-hooks';
|
|
16
17
|
|
|
17
18
|
/* Keeps a prompt card behind the draft while the person types; `released` says what lifted the hold last. */
|
|
@@ -39,7 +40,7 @@ export function PromptComposer({
|
|
|
39
40
|
children
|
|
40
41
|
}: {
|
|
41
42
|
chatId: string;
|
|
42
|
-
pending:
|
|
43
|
+
pending: PromptSubject[];
|
|
43
44
|
focused: boolean;
|
|
44
45
|
/* Set where the prompts are answered somewhere else; the composer only says where they went, and this goes there. */
|
|
45
46
|
onShowElsewhere?: () => void;
|
|
@@ -56,13 +57,11 @@ export function PromptComposer({
|
|
|
56
57
|
const { id } = useChatScope();
|
|
57
58
|
const host = useChatHost();
|
|
58
59
|
const actions = useChatActions();
|
|
60
|
+
const providers = useProviders((row) => row.providers);
|
|
59
61
|
const elsewhere = onShowElsewhere !== undefined;
|
|
60
62
|
// A card the app raises itself waits in the chat beside the CLI's own requests.
|
|
61
63
|
const extra = useHostExtraPrompts(id, chatId);
|
|
62
|
-
const subjects: PromptSubject[] = [
|
|
63
|
-
...pending.map((item): PromptSubject => ({ kind: 'chat', nodeId: chatId, item })),
|
|
64
|
-
...extra.map((prompt): PromptSubject => ({ kind: 'host', nodeId: chatId, prompt }))
|
|
65
|
-
];
|
|
64
|
+
const subjects: PromptSubject[] = [...pending, ...extra.map((prompt): PromptSubject => ({ kind: 'host', nodeId: chatId, prompt }))];
|
|
66
65
|
const session = usePromptSession({ prompts: subjects, idOf: promptIdOf, pick: pickSubject, disabled });
|
|
67
66
|
const ref = useRef<HTMLDivElement>(null);
|
|
68
67
|
const { active, activeId: activeKey } = session;
|
|
@@ -125,7 +124,11 @@ export function PromptComposer({
|
|
|
125
124
|
}}
|
|
126
125
|
more={session.waiting.length - 1}
|
|
127
126
|
hasDraft={hasDraft}
|
|
128
|
-
denyReason={
|
|
127
|
+
denyReason={
|
|
128
|
+
active.kind === 'chat' && active.source?.provider !== undefined
|
|
129
|
+
? providers.find((provider) => provider.kind === active.source?.provider)?.capabilities.denyReason === true
|
|
130
|
+
: denyReason
|
|
131
|
+
}
|
|
129
132
|
disabled={disabled}
|
|
130
133
|
sending={session.sending}
|
|
131
134
|
error={session.errorOf(promptIdOf(active))}
|
|
@@ -312,7 +312,7 @@ export function RunSettings({
|
|
|
312
312
|
disabled={disabled}
|
|
313
313
|
className={clsx(
|
|
314
314
|
'flex h-8 min-w-0 items-center gap-2 rounded-full border pr-3 pl-2.5 text-xs whitespace-nowrap text-text-muted @max-xl/composer:gap-1.5 @max-sm/composer:pr-2.5',
|
|
315
|
-
open ? 'border-accent bg-accent-soft' : 'border-border hover:bg-surface-hover'
|
|
315
|
+
open ? 'border-accent bg-accent-soft' : 'border-border hover:bg-surface-hover-edged'
|
|
316
316
|
)}
|
|
317
317
|
>
|
|
318
318
|
<AgentIcon kind={provider} size={14} className="shrink-0" />
|
|
@@ -665,7 +665,7 @@ function OptionRow({ option, selection, onChange }: { option: ModelOptionDescrip
|
|
|
665
665
|
value={value}
|
|
666
666
|
onValueChange={(next: string) => onChange(next)}
|
|
667
667
|
aria-label={option.label}
|
|
668
|
-
className="flex gap-0.5 rounded-md bg-surface-sunken p-0.5"
|
|
668
|
+
className="flex gap-0.5 rounded-md bg-surface-sunken p-0.5 dark:bg-surface-idle"
|
|
669
669
|
>
|
|
670
670
|
{option.choices.map((choice) => (
|
|
671
671
|
<Menu.RadioItem
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useLayoutEffect, useRef, useState
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { Bot, LoaderCircle } from 'lucide-react';
|
|
4
4
|
import { deriveTimelineRows } from '../logic/timeline';
|
|
@@ -9,8 +9,6 @@ import { EMPTY_TARGET, readTimelineTarget, type TimelineTarget } from '../logic/
|
|
|
9
9
|
import { openBelow, useSubagentTrail, type SubagentStep } from '../subagent-view';
|
|
10
10
|
import { Row } from './rows/Rows';
|
|
11
11
|
import { ReplyContext } from './reply-context';
|
|
12
|
-
import { PromptComposer } from './PromptComposer';
|
|
13
|
-
import { useProviders } from '../../state/providers';
|
|
14
12
|
import { SubagentInfo } from './SubagentInfo';
|
|
15
13
|
import { TimelineMenuPopup } from './TimelineMenu';
|
|
16
14
|
import { FOLLOW_THRESHOLD_PX, rowRhythm } from './rows/row-rhythm';
|
|
@@ -33,14 +31,8 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
|
|
|
33
31
|
const { t } = useTranslation('agent-chat');
|
|
34
32
|
const scope = useChatScope();
|
|
35
33
|
const { commandLooks } = useChatHost();
|
|
36
|
-
const connected = useSyncExternalStore(
|
|
37
|
-
(changed) => scope.transport.subscribeStatus(changed),
|
|
38
|
-
() => scope.transport.status === 'open',
|
|
39
|
-
() => false
|
|
40
|
-
);
|
|
41
34
|
const { trail, show } = useSubagentTrail(chatId);
|
|
42
35
|
const [state, setState] = useState<SubagentConversationState>(INITIAL_CONVERSATION);
|
|
43
|
-
const denyReason = useProviders((row) => row.providers.find((provider) => provider.kind === state.context?.provider)?.capabilities.denyReason === true);
|
|
44
36
|
const controller = useRef<SubagentConversation | null>(null);
|
|
45
37
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
46
38
|
const threadRef = useRef<HTMLDivElement>(null);
|
|
@@ -183,21 +175,6 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
|
|
|
183
175
|
</ReplyContext.Provider>
|
|
184
176
|
</FileLinkContext.Provider>
|
|
185
177
|
)}
|
|
186
|
-
{state.context?.chatId !== undefined && state.pending.length > 0 && (
|
|
187
|
-
<div className="chat-column-content shrink-0">
|
|
188
|
-
<PromptComposer
|
|
189
|
-
key={state.context.chatId}
|
|
190
|
-
chatId={state.context.chatId}
|
|
191
|
-
pending={state.pending}
|
|
192
|
-
focused={false}
|
|
193
|
-
disabled={!connected}
|
|
194
|
-
hasDraft={false}
|
|
195
|
-
denyReason={denyReason}
|
|
196
|
-
>
|
|
197
|
-
{null}
|
|
198
|
-
</PromptComposer>
|
|
199
|
-
</div>
|
|
200
|
-
)}
|
|
201
178
|
</div>
|
|
202
179
|
);
|
|
203
180
|
}
|