@adecore/agents-react 0.21.1 → 0.22.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/agents/model-requirement.d.ts +6 -0
- package/dist/agents/model-requirement.js +26 -0
- package/dist/agents/status-look.js +2 -3
- package/dist/chat/chat-client.d.ts +3 -2
- package/dist/chat/chat-client.js +4 -2
- package/dist/chat/drafts.d.ts +2 -1
- package/dist/chat/drafts.js +21 -17
- 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/picker-entries.d.ts +10 -0
- package/dist/chat/logic/picker-entries.js +19 -0
- package/dist/chat/logic/resume-compaction.js +2 -11
- package/dist/chat/logic/scrubber.js +9 -18
- package/dist/chat/logic/timeline.d.ts +5 -0
- package/dist/chat/logic/timeline.js +25 -11
- package/dist/chat/logic/tool-catalog.js +3 -4
- package/dist/chat/mentions.js +5 -6
- package/dist/chat/prompt-hold.d.ts +8 -0
- package/dist/chat/prompt-hold.js +16 -0
- package/dist/chat/quote.d.ts +4 -0
- package/dist/chat/quote.js +19 -0
- package/dist/chat/runtime-modes.js +2 -4
- package/dist/chat/stash.d.ts +8 -2
- package/dist/chat/stash.js +22 -8
- package/dist/chat/subagent-list.d.ts +7 -1
- package/dist/chat/subagent-list.js +13 -1
- package/dist/chat/thumbnails.js +3 -4
- package/dist/chat/timeline-scroll.d.ts +6 -2
- package/dist/chat/timeline-scroll.js +45 -46
- package/dist/chat/ui/BookmarkSubmenu.js +1 -5
- package/dist/chat/ui/ChatActivity.js +22 -8
- package/dist/chat/ui/CodeBlock.js +2 -3
- package/dist/chat/ui/Composer.js +160 -88
- package/dist/chat/ui/ComposerInput.d.ts +3 -1
- package/dist/chat/ui/ComposerInput.js +13 -4
- package/dist/chat/ui/Markdown.js +11 -11
- package/dist/chat/ui/Pickers.js +31 -27
- package/dist/chat/ui/PromptComposer.d.ts +8 -1
- package/dist/chat/ui/PromptComposer.js +18 -6
- package/dist/chat/ui/QuoteButton.js +4 -5
- package/dist/chat/ui/RunSettings.js +8 -2
- package/dist/chat/ui/Scrubber.js +2 -3
- package/dist/chat/ui/SubagentControls.d.ts +4 -2
- package/dist/chat/ui/SubagentControls.js +13 -11
- package/dist/chat/ui/SubagentInfo.d.ts +1 -1
- package/dist/chat/ui/SubagentInfo.js +9 -15
- package/dist/chat/ui/SubagentStopButton.js +2 -1
- package/dist/chat/ui/Timeline.js +39 -21
- 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/composer/menu-aria.d.ts +10 -0
- package/dist/chat/ui/composer/menu-aria.js +18 -0
- package/dist/chat/ui/intelligent-ui/render-context.d.ts +1 -1
- package/dist/chat/ui/quote-selection.d.ts +5 -2
- package/dist/chat/ui/quote-selection.js +3 -2
- package/dist/chat/ui/rows/MessageRows.js +26 -8
- package/dist/chat/ui/rows/Rows.js +1 -1
- package/dist/chat/ui/rows/WorkRows.d.ts +7 -4
- package/dist/chat/ui/rows/WorkRows.js +22 -17
- package/dist/chat/visual-viewport.d.ts +1 -1
- package/dist/chat/visual-viewport.js +9 -8
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/locales/en/agent-chat.json +30 -7
- package/dist/locales/nl/agent-chat.json +30 -7
- package/dist/port-transport.js +4 -8
- package/dist/state/chats.d.ts +2 -1
- package/dist/state/chats.js +29 -15
- 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 +24 -4
- package/src/agents/model-requirement.ts +36 -0
- package/src/agents/status-look.ts +2 -3
- package/src/chat/chat-client.ts +7 -3
- package/src/chat/drafts.ts +33 -21
- 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/picker-entries.ts +25 -0
- package/src/chat/logic/resume-compaction.ts +2 -11
- package/src/chat/logic/scrubber.ts +13 -17
- package/src/chat/logic/timeline.ts +39 -13
- package/src/chat/logic/tool-catalog.ts +7 -6
- package/src/chat/mentions.ts +5 -6
- package/src/chat/prompt-hold.ts +27 -0
- package/src/chat/quote.ts +20 -0
- package/src/chat/runtime-modes.ts +2 -4
- package/src/chat/stash.ts +32 -8
- package/src/chat/subagent-list.ts +19 -5
- package/src/chat/thumbnails.ts +3 -4
- package/src/chat/timeline-scroll.ts +58 -53
- package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
- package/src/chat/ui/ChatActivity.tsx +35 -9
- package/src/chat/ui/CodeBlock.tsx +2 -3
- package/src/chat/ui/Composer.tsx +263 -138
- package/src/chat/ui/ComposerInput.tsx +18 -3
- package/src/chat/ui/Markdown.tsx +13 -17
- package/src/chat/ui/Pickers.tsx +56 -39
- package/src/chat/ui/PromptComposer.tsx +37 -8
- package/src/chat/ui/QuoteButton.tsx +5 -5
- package/src/chat/ui/RunSettings.tsx +9 -2
- package/src/chat/ui/Scrubber.tsx +2 -3
- package/src/chat/ui/SubagentControls.tsx +23 -22
- package/src/chat/ui/SubagentInfo.tsx +24 -42
- package/src/chat/ui/SubagentStopButton.tsx +2 -1
- package/src/chat/ui/SubagentTimeline.tsx +1 -2
- package/src/chat/ui/Timeline.tsx +44 -24
- 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/composer/menu-aria.ts +29 -0
- package/src/chat/ui/intelligent-ui/render-context.ts +2 -2
- package/src/chat/ui/quote-selection.ts +6 -4
- 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 +47 -9
- package/src/chat/ui/rows/Rows.tsx +3 -1
- package/src/chat/ui/rows/WorkRows.tsx +101 -70
- package/src/chat/visual-viewport.ts +10 -8
- package/src/host.ts +4 -3
- package/src/locales/en/agent-chat.json +30 -7
- package/src/locales/nl/agent-chat.json +30 -7
- package/src/port-transport.ts +4 -8
- package/src/state/chats.ts +42 -16
- 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
package/src/chat/stash.ts
CHANGED
|
@@ -27,9 +27,17 @@ export interface StashedPrompt {
|
|
|
27
27
|
mentions: string[];
|
|
28
28
|
skills: string[];
|
|
29
29
|
attachments: StashedAttachment[];
|
|
30
|
+
/* A quote that knew its reply, kept apart from the text so it can lead back to it in the chat it came from. */
|
|
31
|
+
quote?: StashedQuote;
|
|
30
32
|
createdAt: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
export interface StashedQuote {
|
|
36
|
+
text: string;
|
|
37
|
+
chatId: string;
|
|
38
|
+
itemId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
function strings(value: unknown): string[] {
|
|
34
42
|
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
35
43
|
}
|
|
@@ -45,6 +53,18 @@ function attachments(value: unknown): StashedAttachment[] {
|
|
|
45
53
|
}));
|
|
46
54
|
}
|
|
47
55
|
|
|
56
|
+
function stashedQuote(value: unknown): StashedQuote | undefined {
|
|
57
|
+
if (!isRecord(value) || typeof value.text !== 'string' || typeof value.chatId !== 'string' || typeof value.itemId !== 'string') {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return { text: value.text, chatId: value.chatId, itemId: value.itemId };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stashedQuoteField(value: unknown): { quote?: StashedQuote } {
|
|
64
|
+
const quote = stashedQuote(value);
|
|
65
|
+
return quote === undefined ? {} : { quote };
|
|
66
|
+
}
|
|
67
|
+
|
|
48
68
|
/* Only the fields we know today, so anything an older or a broken write left behind falls away here. */
|
|
49
69
|
export function parseStash(raw: string | null): StashedPrompt[] {
|
|
50
70
|
if (raw === null) {
|
|
@@ -64,6 +84,7 @@ export function parseStash(raw: string | null): StashedPrompt[] {
|
|
|
64
84
|
mentions: strings(entry.mentions),
|
|
65
85
|
skills: strings(entry.skills),
|
|
66
86
|
attachments: attachments(entry.attachments),
|
|
87
|
+
...stashedQuoteField(entry.quote),
|
|
67
88
|
createdAt: typeof entry.createdAt === 'number' ? entry.createdAt : 0
|
|
68
89
|
}))
|
|
69
90
|
.slice(0, STASH_LIMIT);
|
|
@@ -77,14 +98,19 @@ export function withStashed(prompts: StashedPrompt[], entry: StashedPrompt): Sta
|
|
|
77
98
|
return [entry, ...prompts].slice(0, STASH_LIMIT);
|
|
78
99
|
}
|
|
79
100
|
|
|
80
|
-
/*
|
|
81
|
-
|
|
101
|
+
/*
|
|
102
|
+
* What a draft looks like on the shelf; null when there is nothing worth putting there. A quote that
|
|
103
|
+
* knows its reply stays apart, since it leads back to that reply in `chatId`; any other is folded into the text.
|
|
104
|
+
*/
|
|
105
|
+
export function stashedFrom(draft: ChatDraft, id: string, now: number, chatId: string): StashedPrompt | null {
|
|
82
106
|
if (isEmptyDraft(draft)) {
|
|
83
107
|
return null;
|
|
84
108
|
}
|
|
109
|
+
const kept = draft.quote !== '' && draft.quoteSource !== null;
|
|
85
110
|
return {
|
|
86
111
|
id,
|
|
87
|
-
text: withQuote(draft.quote, draft.text),
|
|
112
|
+
text: kept ? draft.text : withQuote(draft.quote, draft.text),
|
|
113
|
+
...(kept ? { quote: { text: draft.quote, chatId, itemId: draft.quoteSource! } } : {}),
|
|
88
114
|
mentions: [...draft.mentions],
|
|
89
115
|
skills: [...draft.skills],
|
|
90
116
|
attachments: draft.attachments.map((attachment) => ({ name: attachment.name, mime: attachment.mime, size: uploadBytes(attachment) })),
|
|
@@ -94,9 +120,7 @@ export function stashedFrom(draft: ChatDraft, id: string, now: number): StashedP
|
|
|
94
120
|
|
|
95
121
|
const storage = persistedJson<StashedPrompt[]>(storageKey, parseStash, [], () => chatStorageLegacyKeys('stash'));
|
|
96
122
|
|
|
97
|
-
/*
|
|
98
|
-
* Prompts put aside for later can move to another chat, so the shelf is shared across chats.
|
|
99
|
-
*/
|
|
123
|
+
/* Prompts put aside for later can move to another chat, so the shelf is shared across chats. */
|
|
100
124
|
export const useStash = createChatStore<{ prompts: StashedPrompt[] }>(
|
|
101
125
|
() => ({ prompts: [] }),
|
|
102
126
|
(store) => store.setState({ prompts: storage.read() })
|
|
@@ -108,8 +132,8 @@ function write(prompts: StashedPrompt[]): void {
|
|
|
108
132
|
}
|
|
109
133
|
|
|
110
134
|
/* Puts a draft on the shelf; false when the composer held nothing to put there. */
|
|
111
|
-
export function stashDraft(draft: ChatDraft): boolean {
|
|
112
|
-
const entry = stashedFrom(draft, `stash-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, Date.now());
|
|
135
|
+
export function stashDraft(draft: ChatDraft, chatId: string): boolean {
|
|
136
|
+
const entry = stashedFrom(draft, `stash-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, Date.now(), chatId);
|
|
113
137
|
if (!entry) {
|
|
114
138
|
return false;
|
|
115
139
|
}
|
|
@@ -48,6 +48,16 @@ export function taskRowStateOf(item: ChatSubagentItem, task: SubagentTask | null
|
|
|
48
48
|
return word;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/* How far delegated work is: what settled as done, what still runs or waits, and how many of those wait on a person. */
|
|
52
|
+
export function progressOf(states: readonly TaskRowState[]): { done: number; total: number; waiting: number; active: number } {
|
|
53
|
+
return {
|
|
54
|
+
done: states.filter((state) => state === 'done').length,
|
|
55
|
+
total: states.length,
|
|
56
|
+
waiting: states.filter((state) => state === 'waiting').length,
|
|
57
|
+
active: states.filter((state) => state === 'running' || state === 'paused' || state === 'waiting').length
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
/*
|
|
52
62
|
* What the flyout over the composer lists: every sub-agent still at work, and the ones that settled
|
|
53
63
|
* since the last message, so a finished batch stays until the next message is sent. That includes an
|
|
@@ -106,11 +116,12 @@ export function entryTimeOf(item: ChatSubagentItem, task: SubagentTask | null, n
|
|
|
106
116
|
}
|
|
107
117
|
|
|
108
118
|
/*
|
|
109
|
-
* What the Stop of an active entry does: a task stops the node working on it, and a subagent
|
|
110
|
-
*
|
|
111
|
-
* turn may still wait on it, so stopping the turn
|
|
119
|
+
* What the Stop of an active entry does: a task stops the node working on it, and a subagent whose CLI
|
|
120
|
+
* named its task id (`native`) is ended on its own, turn or not. Any other subagent of the CLI's own is
|
|
121
|
+
* only marked stopped; while the chat's turn runs that turn may still wait on it, so stopping the turn
|
|
122
|
+
* (the composer's Stop) is the only offer then.
|
|
112
123
|
*/
|
|
113
|
-
export type SubagentStop = 'task' | 'mark';
|
|
124
|
+
export type SubagentStop = 'task' | 'native' | 'mark';
|
|
114
125
|
|
|
115
126
|
export function stopOf(item: ChatSubagentItem, turnRunning: boolean): SubagentStop | null {
|
|
116
127
|
if (item.status !== 'running') {
|
|
@@ -119,11 +130,14 @@ export function stopOf(item: ChatSubagentItem, turnRunning: boolean): SubagentSt
|
|
|
119
130
|
if (item.origin === 'ruimte') {
|
|
120
131
|
return item.childId === undefined ? null : 'task';
|
|
121
132
|
}
|
|
133
|
+
if (item.native?.agentId) {
|
|
134
|
+
return 'native';
|
|
135
|
+
}
|
|
122
136
|
return turnRunning ? null : 'mark';
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
export function stopLabel(stop: SubagentStop): string {
|
|
126
|
-
return
|
|
140
|
+
return i18next.t(`agent-chat:subagents.stop.${stop}`);
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
/*
|
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 {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// A page is a screen minus a little, so the line you were reading is still on it after the jump.
|
|
2
2
|
const PAGE_OVERLAP = 0.9;
|
|
3
3
|
|
|
4
|
+
// How long a jump asked before its thread registered stays up for that thread.
|
|
5
|
+
const PENDING_JUMP_TTL_MS = 10_000;
|
|
6
|
+
|
|
4
7
|
type Scroller = {
|
|
5
8
|
element: HTMLElement;
|
|
6
9
|
follow: (enabled: boolean) => void;
|
|
@@ -10,6 +13,10 @@ type Scroller = {
|
|
|
10
13
|
|
|
11
14
|
const scrollers = new Map<string, Scroller>();
|
|
12
15
|
|
|
16
|
+
/* The timeline owns following; the composer subscribes here to show its jump-to-end button. */
|
|
17
|
+
const ends = new Map<string, boolean>();
|
|
18
|
+
const listeners = new Set<() => void>();
|
|
19
|
+
|
|
13
20
|
/* The timeline hands its scroller over so the composer, which never sees it, can page through it. */
|
|
14
21
|
export function registerTimeline(
|
|
15
22
|
chatId: string,
|
|
@@ -29,10 +36,6 @@ export function registerTimeline(
|
|
|
29
36
|
};
|
|
30
37
|
}
|
|
31
38
|
|
|
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
39
|
export function setTimelineAtEnd(chatId: string, atEnd: boolean): void {
|
|
37
40
|
if (ends.get(chatId) === atEnd) {
|
|
38
41
|
return;
|
|
@@ -97,72 +100,74 @@ export function stepTimelineMessage(key: string, direction: -1 | 1): boolean {
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
/*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
+
* Jumps asked from outside a thread (the chat's menu, a card that names a turn), keyed with the
|
|
104
|
+
* scope's `keyOf` like the steppers. A thread that is not on screen yet gets its jump when it
|
|
105
|
+
* registers, since the asker may have to show the chat first.
|
|
103
106
|
*/
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
107
|
+
function waitingJumps<T>(): { register(key: string, jump: (target: T) => void): () => void; jump(key: string, target: T): boolean } {
|
|
108
|
+
const jumpers = new Map<string, (target: T) => void>();
|
|
109
|
+
const waiting = new Map<string, { target: T; at: number }>();
|
|
110
|
+
return {
|
|
111
|
+
register(key, jump) {
|
|
112
|
+
jumpers.set(key, jump);
|
|
113
|
+
const pending = waiting.get(key);
|
|
114
|
+
if (pending !== undefined) {
|
|
115
|
+
waiting.delete(key);
|
|
116
|
+
// A jump nobody picked up for long is not what the person wants any more.
|
|
117
|
+
if (Date.now() - pending.at <= PENDING_JUMP_TTL_MS) {
|
|
118
|
+
jump(pending.target);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return () => {
|
|
122
|
+
if (jumpers.get(key) === jump) {
|
|
123
|
+
jumpers.delete(key);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
jump(key, target) {
|
|
128
|
+
const jump = jumpers.get(key);
|
|
129
|
+
if (jump === undefined) {
|
|
130
|
+
waiting.set(key, { target, at: Date.now() });
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
jump(target);
|
|
134
|
+
return true;
|
|
117
135
|
}
|
|
118
136
|
};
|
|
119
137
|
}
|
|
120
138
|
|
|
139
|
+
/* What a jump to an item may ask beyond scrolling to it. */
|
|
140
|
+
export interface ItemJumpOptions {
|
|
141
|
+
/* Lights the row up once it is in view. */
|
|
142
|
+
flash?: boolean;
|
|
143
|
+
/* Runs when the item is nowhere in the thread, after every earlier page has been tried. */
|
|
144
|
+
onMissing?(): void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const itemJumps = waitingJumps<{ itemId: string; options: ItemJumpOptions }>();
|
|
148
|
+
|
|
149
|
+
export function registerItemJumper(key: string, jump: (itemId: string, options: ItemJumpOptions) => void): () => void {
|
|
150
|
+
return itemJumps.register(key, ({ itemId, options }) => jump(itemId, options));
|
|
151
|
+
}
|
|
152
|
+
|
|
121
153
|
/* False when that chat has no thread on screen; the jump then waits for the next one that registers. */
|
|
122
|
-
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;
|
|
154
|
+
export function jumpToTimelineItem(key: string, itemId: string, options: ItemJumpOptions = {}): boolean {
|
|
155
|
+
return itemJumps.jump(key, { itemId, options });
|
|
130
156
|
}
|
|
131
157
|
|
|
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
|
-
*/
|
|
158
|
+
/* Which part of a turn a jump lands on: the message that opened it, or the files it changed. */
|
|
137
159
|
export type TurnTarget = 'prompt' | 'changes';
|
|
138
160
|
|
|
139
161
|
type TurnJump = (turnId: string, target: TurnTarget) => void;
|
|
140
|
-
const
|
|
141
|
-
const waitingTurnJumps = new Map<string, { turnId: string; target: TurnTarget }>();
|
|
162
|
+
const turnJumps = waitingJumps<{ turnId: string; target: TurnTarget }>();
|
|
142
163
|
|
|
143
164
|
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
|
-
};
|
|
165
|
+
return turnJumps.register(key, ({ turnId, target }) => jump(turnId, target));
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
/* False when that chat has no thread on screen; the jump then waits for the next one that registers. */
|
|
158
169
|
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;
|
|
170
|
+
return turnJumps.jump(key, { turnId, target });
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
/* 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();
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Activity, Square, SquareTerminal } from 'lucide-react';
|
|
4
|
+
import { Activity, MessageCircleQuestionMark, Square, SquareTerminal } from 'lucide-react';
|
|
5
5
|
import type { ChatBackgroundTask, ChatSubagentItem } from '@adecore/agent-contracts';
|
|
6
6
|
import { backgroundCounts } from '../activity';
|
|
7
|
-
import { badgeCountOf, entryTimeOf, flyoutSubagents, statusWordOf, subagentTitle, summaryWordOf, taskIdOf } from '../subagent-list';
|
|
7
|
+
import { badgeCountOf, entryTimeOf, flyoutSubagents, progressOf, statusWordOf, subagentTitle, summaryWordOf, taskIdOf, taskRowStateOf } from '../subagent-list';
|
|
8
8
|
import { useSubagentSupport } from '../subagent-support';
|
|
9
9
|
import { canOpenSubagent, crumbOf, openFromMain, useSubagentTrail } from '../subagent-view';
|
|
10
10
|
import { SubagentStopButton } from './SubagentStopButton';
|
|
11
11
|
import { formatElapsedShort } from '@adecore/ui/format';
|
|
12
12
|
import { Icon, IconButton, Popover, Surface, useNow } from '@adecore/ui';
|
|
13
|
-
import { useChatRow } from '../../state/chats';
|
|
13
|
+
import { useChatRow, useChats } from '../../state/chats';
|
|
14
14
|
import { chatHost } from '../../host';
|
|
15
15
|
import { useChatScope } from '../../scope';
|
|
16
16
|
import { useChatActions } from '../actions';
|
|
@@ -28,8 +28,20 @@ export function ChatActivity({ chatId }: { chatId: string }) {
|
|
|
28
28
|
const order = useChatRow(chatId, (row) => row?.order) ?? NO_ITEMS;
|
|
29
29
|
const structure = useChatRow(chatId, (row) => row?.structure);
|
|
30
30
|
const subagents = useMemo(() => (structure === undefined ? [] : flyoutSubagents(order, structure)), [order, structure]);
|
|
31
|
-
const { id } = useChatScope();
|
|
31
|
+
const { id, keyOf } = useChatScope();
|
|
32
32
|
const tasks = chatHost().tasks.useTasks(id);
|
|
33
|
+
const statuses = useChats((s) => s.statusByKey);
|
|
34
|
+
const progress = useMemo(
|
|
35
|
+
() =>
|
|
36
|
+
progressOf(
|
|
37
|
+
subagents.map((item) => {
|
|
38
|
+
const taskId = taskIdOf(item);
|
|
39
|
+
const child = item.childId === undefined ? undefined : statuses[keyOf(item.childId)]?.info;
|
|
40
|
+
return taskRowStateOf(item, taskId === null ? null : (tasks?.[taskId] ?? null), child);
|
|
41
|
+
})
|
|
42
|
+
),
|
|
43
|
+
[subagents, tasks, statuses, keyOf]
|
|
44
|
+
);
|
|
33
45
|
const words = useMemo(
|
|
34
46
|
() =>
|
|
35
47
|
subagents.map((item) => {
|
|
@@ -44,13 +56,25 @@ export function ChatActivity({ chatId }: { chatId: string }) {
|
|
|
44
56
|
}
|
|
45
57
|
const { shells, monitors } = backgroundCounts(background);
|
|
46
58
|
const Slot = chatHost().SubagentSlot;
|
|
59
|
+
const counting = progress.active > 0 && progress.total > 1;
|
|
60
|
+
const label = counting
|
|
61
|
+
? [t('activity.progress', { done: progress.done, total: progress.total }), progress.waiting > 0 && t('activity.needsYou', { count: progress.waiting })]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(', ')
|
|
64
|
+
: t('activity.subagents', { count: badgeCountOf(words) });
|
|
47
65
|
return (
|
|
48
66
|
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
49
67
|
{subagents.length > 0 &&
|
|
50
68
|
(Slot === null ? (
|
|
51
69
|
<Flyout
|
|
52
|
-
icon={
|
|
53
|
-
|
|
70
|
+
icon={
|
|
71
|
+
progress.waiting > 0 ? (
|
|
72
|
+
<Icon icon={MessageCircleQuestionMark} size={14} className="shrink-0 text-status-needs-you" />
|
|
73
|
+
) : (
|
|
74
|
+
<StatusIcon word={summaryWordOf(words)} />
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
label={label}
|
|
54
78
|
heading={t('activity.subagents', { count: subagents.length })}
|
|
55
79
|
>
|
|
56
80
|
<SubagentRows chatId={chatId} items={subagents} />
|
|
@@ -83,7 +107,7 @@ function Flyout({ icon, label, heading = label, children }: { icon: ReactNode; l
|
|
|
83
107
|
<Popover.Root>
|
|
84
108
|
<Surface
|
|
85
109
|
render={<Popover.Trigger />}
|
|
86
|
-
className="pointer-events-auto inline-flex h-7.5 items-center gap-1.5 rounded-full px-3 text-xs text-text-muted hover:text-text"
|
|
110
|
+
className="pointer-events-auto inline-flex h-7.5 items-center gap-1.5 rounded-full px-3 text-xs text-text-muted tabular-nums hover:text-text"
|
|
87
111
|
>
|
|
88
112
|
{icon}
|
|
89
113
|
{label}
|
|
@@ -148,11 +172,13 @@ function SubagentRows({ chatId, items }: { chatId: string; items: readonly ChatS
|
|
|
148
172
|
|
|
149
173
|
function SubagentEntry({ chatId, item, now }: { chatId: string; item: ChatSubagentItem; now: number }) {
|
|
150
174
|
const { t } = useTranslation('agent-chat');
|
|
151
|
-
const { id } = useChatScope();
|
|
175
|
+
const { id, keyOf } = useChatScope();
|
|
152
176
|
const refused = useSubagentSupport((s) => s.unsupported[id] === true);
|
|
153
177
|
const task = chatHost().tasks.useTask(id, taskIdOf(item));
|
|
154
178
|
const { show } = useSubagentTrail(chatId);
|
|
179
|
+
const child = useChats((s) => (item.childId === undefined ? undefined : s.statusByKey[keyOf(item.childId)]?.info));
|
|
155
180
|
const word = statusWordOf(item, task);
|
|
181
|
+
const state = taskRowStateOf(item, task, child);
|
|
156
182
|
return (
|
|
157
183
|
<ActivityRow
|
|
158
184
|
icon={<StatusIcon word={word} />}
|
|
@@ -160,7 +186,7 @@ function SubagentEntry({ chatId, item, now }: { chatId: string; item: ChatSubage
|
|
|
160
186
|
<>
|
|
161
187
|
{subagentTitle(item)}
|
|
162
188
|
{/* The icon says the state to the eye; a screen reader hears it with the title. */}
|
|
163
|
-
<span className="sr-only">, {t(`common.status.${word}`)}</span>
|
|
189
|
+
<span className="sr-only">, {state === 'waiting' ? t('rows.task.waiting') : t(`common.status.${word}`)}</span>
|
|
164
190
|
</>
|
|
165
191
|
}
|
|
166
192
|
time={entryTimeOf(item, task, now)}
|
|
@@ -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');
|