@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/chat-client.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ChatAttachResult,
|
|
5
5
|
ChatBookmark,
|
|
6
6
|
ChatPreferencesPayload,
|
|
7
|
+
ChatQuoteSource,
|
|
7
8
|
ChatSkill,
|
|
8
9
|
ChatVisual,
|
|
9
10
|
ModelSelection,
|
|
@@ -35,6 +36,8 @@ export interface ChatSendExtras {
|
|
|
35
36
|
skills?: string[];
|
|
36
37
|
/* Chats of the project picked with `@`; only their ids travel, the agent reads them itself. */
|
|
37
38
|
chats?: string[];
|
|
39
|
+
/* The reply a quote at the head of the text was taken from. */
|
|
40
|
+
quoteSource?: ChatQuoteSource;
|
|
38
41
|
attachments?: ChatAttachmentUpload[];
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -63,6 +66,7 @@ export class ChatClient {
|
|
|
63
66
|
private readonly mounted = new MountedRegistry<Mounted>();
|
|
64
67
|
private readonly unsubscribe: Array<() => void> = [];
|
|
65
68
|
private readonly earlier = new Map<string, Promise<void>>();
|
|
69
|
+
private readonly inspections = new Map<string, Promise<ChatAttachResult>>();
|
|
66
70
|
private preferences: ChatPreferencesPayload | null = null;
|
|
67
71
|
|
|
68
72
|
constructor(transport: ChatTransport, sink: ChatSink, providers: ProviderSink | null = null) {
|
|
@@ -89,8 +93,6 @@ export class ChatClient {
|
|
|
89
93
|
}
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
private readonly inspections = new Map<string, Promise<ChatAttachResult>>();
|
|
93
|
-
|
|
94
96
|
inspect(chatId: string): Promise<ChatAttachResult> {
|
|
95
97
|
const pending = this.inspections.get(chatId);
|
|
96
98
|
if (pending) {
|
|
@@ -171,9 +173,11 @@ export class ChatClient {
|
|
|
171
173
|
return this.transport.request('chat.send', {
|
|
172
174
|
chatId,
|
|
173
175
|
text,
|
|
176
|
+
delivery: extras.delivery,
|
|
174
177
|
mentions: extras.mentions,
|
|
175
178
|
skills: extras.skills,
|
|
176
179
|
chats: extras.chats,
|
|
180
|
+
quoteSource: extras.quoteSource,
|
|
177
181
|
attachments: extras.attachments
|
|
178
182
|
});
|
|
179
183
|
}
|
|
@@ -347,7 +351,7 @@ export class ChatClient {
|
|
|
347
351
|
this.sendPreferences();
|
|
348
352
|
void this.loadProviders();
|
|
349
353
|
void this.loadStatuses();
|
|
350
|
-
void this.mounted.reattachAll((chatId) => this.attach(chatId)
|
|
354
|
+
void this.mounted.reattachAll((chatId) => this.attach(chatId));
|
|
351
355
|
return;
|
|
352
356
|
}
|
|
353
357
|
this.mounted.detachAll();
|
package/src/chat/drafts.ts
CHANGED
|
@@ -14,12 +14,22 @@ export interface ChatDraft {
|
|
|
14
14
|
attachments: ChatAttachmentUpload[];
|
|
15
15
|
/* A piece of an answer that goes above the text as a blockquote; empty for none. */
|
|
16
16
|
quote: string;
|
|
17
|
+
/* The reply the quote was taken from; null when the quote names none. */
|
|
18
|
+
quoteSource: string | null;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
export const EMPTY_DRAFT: ChatDraft = { text: '', mentions: [], skills: [], chats: [], attachments: [], quote: '' };
|
|
21
|
+
export const EMPTY_DRAFT: ChatDraft = { text: '', mentions: [], skills: [], chats: [], attachments: [], quote: '', quoteSource: null };
|
|
20
22
|
|
|
21
23
|
// Older records only had text; the rest is filled in on read.
|
|
22
|
-
type DraftRecord = {
|
|
24
|
+
type DraftRecord = {
|
|
25
|
+
text: string;
|
|
26
|
+
mentions?: string[];
|
|
27
|
+
skills?: string[];
|
|
28
|
+
chats?: string[];
|
|
29
|
+
attachments?: ChatAttachmentUpload[];
|
|
30
|
+
quote?: string;
|
|
31
|
+
quoteSource?: string;
|
|
32
|
+
};
|
|
23
33
|
|
|
24
34
|
const storage = persistedJson<Record<string, DraftRecord>>(
|
|
25
35
|
storageKey,
|
|
@@ -28,14 +38,6 @@ const storage = persistedJson<Record<string, DraftRecord>>(
|
|
|
28
38
|
() => chatStorageLegacyKeys('drafts')
|
|
29
39
|
);
|
|
30
40
|
|
|
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
41
|
export function isEmptyDraft(draft: ChatDraft): boolean {
|
|
40
42
|
return draft.text.trim() === '' && draft.attachments.length === 0 && draft.quote === '';
|
|
41
43
|
}
|
|
@@ -46,7 +48,7 @@ export function isEmptyDraft(draft: ChatDraft): boolean {
|
|
|
46
48
|
*/
|
|
47
49
|
export const useDrafts = createChatStore<{ ids: string[] }>(
|
|
48
50
|
() => ({ ids: [] }),
|
|
49
|
-
(store) => store.setState({ ids: Object.keys(
|
|
51
|
+
(store) => store.setState({ ids: Object.keys(storage.read()) })
|
|
50
52
|
);
|
|
51
53
|
|
|
52
54
|
export function useHasDraft(chatId: string): boolean {
|
|
@@ -63,7 +65,7 @@ function trackDraft(chatId: string, held: boolean): void {
|
|
|
63
65
|
|
|
64
66
|
/* An unsent prompt per chat node, kept across reloads so a half-written message is never lost. */
|
|
65
67
|
export function readDraft(chatId: string): ChatDraft {
|
|
66
|
-
const record =
|
|
68
|
+
const record = storage.read()[chatId];
|
|
67
69
|
return record
|
|
68
70
|
? {
|
|
69
71
|
text: record.text,
|
|
@@ -71,17 +73,18 @@ export function readDraft(chatId: string): ChatDraft {
|
|
|
71
73
|
skills: record.skills ?? [],
|
|
72
74
|
chats: record.chats ?? [],
|
|
73
75
|
attachments: record.attachments ?? [],
|
|
74
|
-
quote: record.quote ?? ''
|
|
76
|
+
quote: record.quote ?? '',
|
|
77
|
+
quoteSource: record.quoteSource ?? null
|
|
75
78
|
}
|
|
76
79
|
: EMPTY_DRAFT;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
export function writeDraft(chatId: string, draft: ChatDraft): void {
|
|
80
|
-
const drafts =
|
|
83
|
+
const drafts = storage.read();
|
|
81
84
|
trackDraft(chatId, !isEmptyDraft(draft));
|
|
82
85
|
if (isEmptyDraft(draft)) {
|
|
83
86
|
delete drafts[chatId];
|
|
84
|
-
|
|
87
|
+
storage.write(drafts);
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
87
90
|
drafts[chatId] = {
|
|
@@ -90,14 +93,22 @@ export function writeDraft(chatId: string, draft: ChatDraft): void {
|
|
|
90
93
|
skills: draft.skills,
|
|
91
94
|
chats: draft.chats,
|
|
92
95
|
attachments: draft.attachments,
|
|
93
|
-
quote: draft.quote
|
|
96
|
+
quote: draft.quote,
|
|
97
|
+
...(draft.quoteSource === null ? {} : { quoteSource: draft.quoteSource })
|
|
94
98
|
};
|
|
95
|
-
if (
|
|
99
|
+
if (storage.write(drafts)) {
|
|
96
100
|
return;
|
|
97
101
|
}
|
|
98
102
|
// A file can outgrow the storage quota; the text is the part worth keeping then.
|
|
99
|
-
drafts[chatId] = {
|
|
100
|
-
|
|
103
|
+
drafts[chatId] = {
|
|
104
|
+
text: draft.text,
|
|
105
|
+
mentions: draft.mentions,
|
|
106
|
+
skills: draft.skills,
|
|
107
|
+
chats: draft.chats,
|
|
108
|
+
quote: draft.quote,
|
|
109
|
+
...(draft.quoteSource === null ? {} : { quoteSource: draft.quoteSource })
|
|
110
|
+
};
|
|
111
|
+
storage.write(drafts);
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
/* Text handed to a draft from outside the composer goes under what was already typed, never over it. */
|
|
@@ -116,7 +127,7 @@ function union(first: readonly string[], second: readonly string[]): string[] {
|
|
|
116
127
|
*/
|
|
117
128
|
export function takeBackIntoDraft(
|
|
118
129
|
current: ChatDraft,
|
|
119
|
-
taken: Omit<ChatDraft, 'quote'>
|
|
130
|
+
taken: Omit<ChatDraft, 'quote' | 'quoteSource'>
|
|
120
131
|
): { draft: ChatDraft; rejected: Array<{ name: string; reason: string }> } {
|
|
121
132
|
const checked = checkAttachmentLimits(
|
|
122
133
|
current.attachments.length,
|
|
@@ -130,7 +141,8 @@ export function takeBackIntoDraft(
|
|
|
130
141
|
skills: union(taken.skills, current.skills),
|
|
131
142
|
chats: union(taken.chats, current.chats),
|
|
132
143
|
attachments: [...checked.accepted.map((entry) => entry.upload), ...current.attachments],
|
|
133
|
-
quote: current.quote
|
|
144
|
+
quote: current.quote,
|
|
145
|
+
quoteSource: current.quoteSource
|
|
134
146
|
},
|
|
135
147
|
rejected: checked.rejected
|
|
136
148
|
};
|
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
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ModelInfo, ProviderInfo } from '@adecore/agent-contracts';
|
|
2
|
+
|
|
3
|
+
/* A model row, or the one row that folds the legacy models open. Both take a turn in the arrow keys. */
|
|
4
|
+
export type PickerEntry = { kind: 'model'; provider: ProviderInfo; model: ModelInfo } | { kind: 'legacy'; count: number };
|
|
5
|
+
|
|
6
|
+
function matches(model: ModelInfo, query: string): boolean {
|
|
7
|
+
return model.name.toLowerCase().includes(query) || model.slug.toLowerCase().includes(query);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* The rows of the model list. A model an old CLI cannot run is a row like any other, so a search for
|
|
12
|
+
* only that model still finds the row that explains why it cannot be picked.
|
|
13
|
+
*/
|
|
14
|
+
export function pickerEntriesOf(providers: readonly ProviderInfo[], query: string, legacyOpen: boolean): PickerEntry[] {
|
|
15
|
+
const rows = providers.flatMap((entry) => entry.models.map((model) => ({ kind: 'model' as const, provider: entry, model })));
|
|
16
|
+
if (query !== '') {
|
|
17
|
+
return rows.filter((row) => matches(row.model, query));
|
|
18
|
+
}
|
|
19
|
+
const live = rows.filter((row) => !row.model.legacy);
|
|
20
|
+
const legacy = rows.filter((row) => row.model.legacy);
|
|
21
|
+
if (legacy.length === 0) {
|
|
22
|
+
return live;
|
|
23
|
+
}
|
|
24
|
+
return legacyOpen ? [...live, { kind: 'legacy', count: legacy.length }, ...legacy] : [...live, { kind: 'legacy', count: legacy.length }];
|
|
25
|
+
}
|
|
@@ -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[];
|
|
@@ -52,7 +52,7 @@ export type TimelineRow =
|
|
|
52
52
|
| { kind: 'question'; id: string; item: ChatQuestionItem }
|
|
53
53
|
| { kind: 'note'; id: string; level: 'info' | 'warning' | 'error'; text: string; from?: string }
|
|
54
54
|
| { kind: 'compaction'; id: string; preTokens: number | null }
|
|
55
|
-
| { kind: 'changed-files'; id: string; turnId: string; tools: ChatToolItem[]; diff: ChatCheckpointDiff | null; checkpoint: boolean }
|
|
55
|
+
| { kind: 'changed-files'; id: string; turnId: string; tools: ChatToolItem[]; diff: ChatCheckpointDiff | null; checkpoint: boolean; baseMoved?: BaseMoved }
|
|
56
56
|
| { kind: 'turn-fold'; id: string; turn: ChatTurnItem; label: string; work: string[]; hiddenCount: number; expanded: boolean }
|
|
57
57
|
| { kind: 'forks'; id: string; turnId: string }
|
|
58
58
|
| { kind: 'working'; id: string; startedAt: 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}`;
|
|
@@ -319,6 +322,12 @@ function rowsForItems(items: ChatItem[], options: TimelineOptions, children: Map
|
|
|
319
322
|
return rows;
|
|
320
323
|
}
|
|
321
324
|
|
|
325
|
+
/* The branch the turn started on was not the one it ended on, so its diff also holds what came in with the move. */
|
|
326
|
+
export interface BaseMoved {
|
|
327
|
+
from: string;
|
|
328
|
+
to: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
322
331
|
/* The card of a settled turn: the checkpoint diff when the host took one, else what the tool calls carry. */
|
|
323
332
|
function changedFilesRow(turn: ChatTurnItem, items: ChatItem[]): TimelineRow | null {
|
|
324
333
|
const edits = items.filter(
|
|
@@ -329,7 +338,16 @@ function changedFilesRow(turn: ChatTurnItem, items: ChatItem[]): TimelineRow | n
|
|
|
329
338
|
if (empty) {
|
|
330
339
|
return null;
|
|
331
340
|
}
|
|
332
|
-
|
|
341
|
+
const moved = turn.checkpointHead !== undefined && turn.checkpointHeadAfter !== undefined && turn.checkpointHead !== turn.checkpointHeadAfter;
|
|
342
|
+
return {
|
|
343
|
+
kind: 'changed-files',
|
|
344
|
+
id: `files-${turn.id}`,
|
|
345
|
+
turnId: turn.id,
|
|
346
|
+
tools: edits,
|
|
347
|
+
diff,
|
|
348
|
+
checkpoint: turn.checkpoint !== undefined,
|
|
349
|
+
...(moved ? { baseMoved: { from: turn.checkpointHead!, to: turn.checkpointHeadAfter! } } : {})
|
|
350
|
+
};
|
|
333
351
|
}
|
|
334
352
|
|
|
335
353
|
function lastAssistantRow(rows: TimelineRow[]): TimelineRow | null {
|
|
@@ -345,12 +363,14 @@ function lastAssistantRow(rows: TimelineRow[]): TimelineRow | null {
|
|
|
345
363
|
return null;
|
|
346
364
|
}
|
|
347
365
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
366
|
+
interface TurnChunk {
|
|
367
|
+
turnId: string | null;
|
|
368
|
+
items: ChatItem[];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* Items grouped by turn, in order of first appearance; runs of turnless items keep their place. */
|
|
372
|
+
function chunksByTurn(items: ChatItem[]): TurnChunk[] {
|
|
373
|
+
const chunks: TurnChunk[] = [];
|
|
354
374
|
const byTurn = new Map<string, ChatItem[]>();
|
|
355
375
|
for (const item of items) {
|
|
356
376
|
if (item.turnId === null) {
|
|
@@ -370,8 +390,14 @@ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions):
|
|
|
370
390
|
}
|
|
371
391
|
bucket.push(item);
|
|
372
392
|
}
|
|
393
|
+
return chunks;
|
|
394
|
+
}
|
|
373
395
|
|
|
374
|
-
|
|
396
|
+
/* The whole thread as rows; items without a turn (older records) are shown as they are. */
|
|
397
|
+
export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions): TimelineRow[] {
|
|
398
|
+
const rows: TimelineRow[] = [];
|
|
399
|
+
const children = groupChildren(items);
|
|
400
|
+
for (const chunk of chunksByTurn(items)) {
|
|
375
401
|
if (chunk.turnId === null) {
|
|
376
402
|
rows.push(...rowsForItems(chunk.items, options, children));
|
|
377
403
|
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}`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* How long a person must stop typing before a waiting prompt card takes the composer's place. */
|
|
2
|
+
export const PROMPT_HOLD_PAUSE_MS = 2000;
|
|
3
|
+
|
|
4
|
+
export interface HoldInput {
|
|
5
|
+
/* When the last change to the draft was made; null when none was since the hold last lifted. */
|
|
6
|
+
typedAt: number | null;
|
|
7
|
+
/* An IME composition is under way, which can run for any length without a change. */
|
|
8
|
+
composing: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type HoldRelease = 'pause' | 'send' | 'blur' | 'show';
|
|
12
|
+
|
|
13
|
+
/* A card waits while the person is composing, or typed less than the pause ago. */
|
|
14
|
+
export function holdsPrompt(input: HoldInput, now: number): boolean {
|
|
15
|
+
if (input.composing) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return input.typedAt !== null && now - input.typedAt < PROMPT_HOLD_PAUSE_MS;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* The milliseconds until the hold lifts by itself; null when it holds until something else lifts it, or does not hold. */
|
|
22
|
+
export function holdLiftsIn(input: HoldInput, now: number): number | null {
|
|
23
|
+
if (input.composing || input.typedAt === null) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return Math.max(0, input.typedAt + PROMPT_HOLD_PAUSE_MS - now);
|
|
27
|
+
}
|
package/src/chat/quote.ts
CHANGED
|
@@ -24,3 +24,23 @@ export function withQuote(quote: string, message: string): string {
|
|
|
24
24
|
.join('\n');
|
|
25
25
|
return message === '' ? block : `${block}\n\n${message}`;
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* The inverse of `withQuote`: the quote a message opens with and what follows it. Null when the text
|
|
30
|
+
* does not open with a blockquote that ends on a blank line or the end of the text.
|
|
31
|
+
*/
|
|
32
|
+
export function splitQuote(text: string): { quote: string; rest: string } | null {
|
|
33
|
+
const lines = text.split('\n');
|
|
34
|
+
let end = 0;
|
|
35
|
+
while (end < lines.length && (lines[end] === '>' || lines[end]!.startsWith('> '))) {
|
|
36
|
+
end += 1;
|
|
37
|
+
}
|
|
38
|
+
if (end === 0 || (end < lines.length && lines[end] !== '')) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const quote = lines
|
|
42
|
+
.slice(0, end)
|
|
43
|
+
.map((line) => line.slice(2))
|
|
44
|
+
.join('\n');
|
|
45
|
+
return { quote, rest: lines.slice(end + 1).join('\n') };
|
|
46
|
+
}
|
|
@@ -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
|
|