@adecore/agents-react 0.21.2 → 0.22.1

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.
Files changed (104) hide show
  1. package/dist/agents/model-requirement.d.ts +6 -0
  2. package/dist/agents/model-requirement.js +26 -0
  3. package/dist/chat/chat-client.d.ts +2 -1
  4. package/dist/chat/chat-client.js +2 -0
  5. package/dist/chat/drafts.d.ts +2 -1
  6. package/dist/chat/drafts.js +15 -5
  7. package/dist/chat/logic/picker-entries.d.ts +10 -0
  8. package/dist/chat/logic/picker-entries.js +19 -0
  9. package/dist/chat/logic/timeline.d.ts +5 -0
  10. package/dist/chat/logic/timeline.js +10 -1
  11. package/dist/chat/prompt-hold.d.ts +8 -0
  12. package/dist/chat/prompt-hold.js +16 -0
  13. package/dist/chat/quote.d.ts +4 -0
  14. package/dist/chat/quote.js +19 -0
  15. package/dist/chat/stash.d.ts +8 -2
  16. package/dist/chat/stash.js +21 -5
  17. package/dist/chat/subagent-list.d.ts +7 -1
  18. package/dist/chat/subagent-list.js +13 -1
  19. package/dist/chat/timeline-scroll.d.ts +6 -2
  20. package/dist/chat/timeline-scroll.js +12 -7
  21. package/dist/chat/ui/ChatActivity.js +22 -8
  22. package/dist/chat/ui/Composer.js +103 -19
  23. package/dist/chat/ui/ComposerInput.d.ts +3 -1
  24. package/dist/chat/ui/ComposerInput.js +13 -4
  25. package/dist/chat/ui/Pickers.js +26 -18
  26. package/dist/chat/ui/PromptComposer.d.ts +8 -1
  27. package/dist/chat/ui/PromptComposer.js +18 -6
  28. package/dist/chat/ui/QuoteButton.js +2 -2
  29. package/dist/chat/ui/SubagentControls.d.ts +4 -2
  30. package/dist/chat/ui/SubagentControls.js +12 -8
  31. package/dist/chat/ui/SubagentInfo.d.ts +1 -1
  32. package/dist/chat/ui/SubagentInfo.js +9 -15
  33. package/dist/chat/ui/SubagentStopButton.js +2 -1
  34. package/dist/chat/ui/Timeline.js +24 -3
  35. package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
  36. package/dist/chat/ui/composer/menu-aria.js +18 -0
  37. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
  38. package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
  39. package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
  40. package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
  41. package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
  42. package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
  43. package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
  44. package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
  45. package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
  46. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
  47. package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
  48. package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
  49. package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
  50. package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
  51. package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
  52. package/dist/chat/ui/quote-selection.d.ts +5 -2
  53. package/dist/chat/ui/quote-selection.js +3 -2
  54. package/dist/chat/ui/rows/MessageRows.js +24 -5
  55. package/dist/chat/ui/rows/Rows.js +1 -1
  56. package/dist/chat/ui/rows/WorkRows.d.ts +3 -1
  57. package/dist/chat/ui/rows/WorkRows.js +4 -4
  58. package/dist/host.d.ts +1 -0
  59. package/dist/host.js +1 -0
  60. package/dist/locales/en/agent-chat.json +39 -10
  61. package/dist/locales/nl/agent-chat.json +39 -10
  62. package/dist/state/chats.d.ts +2 -1
  63. package/dist/state/chats.js +19 -1
  64. package/package.json +29 -4
  65. package/src/agents/model-requirement.ts +36 -0
  66. package/src/chat/chat-client.ts +5 -0
  67. package/src/chat/drafts.ts +27 -7
  68. package/src/chat/logic/picker-entries.ts +25 -0
  69. package/src/chat/logic/timeline.ts +17 -2
  70. package/src/chat/prompt-hold.ts +27 -0
  71. package/src/chat/quote.ts +20 -0
  72. package/src/chat/stash.ts +31 -5
  73. package/src/chat/subagent-list.ts +19 -5
  74. package/src/chat/timeline-scroll.ts +24 -10
  75. package/src/chat/ui/ChatActivity.tsx +35 -9
  76. package/src/chat/ui/Composer.tsx +197 -52
  77. package/src/chat/ui/ComposerInput.tsx +18 -3
  78. package/src/chat/ui/Pickers.tsx +51 -30
  79. package/src/chat/ui/PromptComposer.tsx +37 -8
  80. package/src/chat/ui/QuoteButton.tsx +3 -2
  81. package/src/chat/ui/SubagentControls.tsx +22 -19
  82. package/src/chat/ui/SubagentInfo.tsx +24 -42
  83. package/src/chat/ui/SubagentStopButton.tsx +2 -1
  84. package/src/chat/ui/Timeline.tsx +26 -4
  85. package/src/chat/ui/composer/menu-aria.ts +29 -0
  86. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
  87. package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
  88. package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
  89. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
  90. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
  91. package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
  92. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
  93. package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
  94. package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
  95. package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
  96. package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
  97. package/src/chat/ui/quote-selection.ts +6 -4
  98. package/src/chat/ui/rows/MessageRows.tsx +44 -5
  99. package/src/chat/ui/rows/Rows.tsx +3 -1
  100. package/src/chat/ui/rows/WorkRows.tsx +15 -1
  101. package/src/host.ts +3 -0
  102. package/src/locales/en/agent-chat.json +39 -10
  103. package/src/locales/nl/agent-chat.json +39 -10
  104. package/src/state/chats.ts +31 -2
@@ -91,7 +91,8 @@
91
91
  "user": {
92
92
  "heading": "Jij",
93
93
  "showAll": "Alles tonen",
94
- "showLess": "Minder tonen"
94
+ "showLess": "Minder tonen",
95
+ "quoteGone": "Het antwoord waar dit citaat vandaan kwam, staat niet meer in deze chat."
95
96
  },
96
97
  "report": {
97
98
  "heading": "Rapport"
@@ -107,7 +108,8 @@
107
108
  "note": {
108
109
  "hide": "Verbergen",
109
110
  "show": "Tonen",
110
- "openFork": "Fork openen"
111
+ "openFork": "Fork openen",
112
+ "openWaiting": "Openen om te antwoorden"
111
113
  },
112
114
  "compaction": {
113
115
  "plain": "Context gecomprimeerd",
@@ -165,6 +167,9 @@
165
167
  },
166
168
  "changedFiles_one": "{{count}} gewijzigd bestand",
167
169
  "changedFiles_other": "{{count}} gewijzigde bestanden",
170
+ "folderChanges_one": "{{count}} wijziging in de map tijdens deze beurt",
171
+ "folderChanges_other": "{{count}} wijzigingen in de map tijdens deze beurt",
172
+ "baseMoved": "De branch is tijdens deze beurt verplaatst ({{from}} naar {{to}}), dus dit omvat ook wat daarmee binnenkwam.",
168
173
  "andMore": "en meer, afgekapt",
169
174
  "edits_one": "{{count}} bewerking",
170
175
  "edits_other": "{{count}} bewerkingen",
@@ -249,6 +254,17 @@
249
254
  },
250
255
  "composer": {
251
256
  "slotFailed": "Dit onderdeel kon niet worden getekend",
257
+ "inputLabel": "Bericht",
258
+ "menu": {
259
+ "commands": "Commando's",
260
+ "skills": "Skills",
261
+ "mentions": "Vermeldingen"
262
+ },
263
+ "held": {
264
+ "question": "Er wacht een vraag. Die opent zodra je even stopt met typen.",
265
+ "approval": "Er wacht een goedkeuring. Die opent zodra je even stopt met typen.",
266
+ "show": "Nu tonen"
267
+ },
252
268
  "commands": {
253
269
  "model": "Wissel van model",
254
270
  "compact": "Comprimeer de context",
@@ -294,7 +310,8 @@
294
310
  },
295
311
  "quote": {
296
312
  "add": "Citeren",
297
- "remove": "Citaat verwijderen"
313
+ "remove": "Citaat verwijderen",
314
+ "goToSource": "Naar het antwoord"
298
315
  },
299
316
  "removeAttachment": "{{name}} verwijderen",
300
317
  "attach": "Bestanden toevoegen",
@@ -319,6 +336,9 @@
319
336
  "monitors_one": "{{count}} monitor",
320
337
  "monitors_other": "{{count}} monitors",
321
338
  "pausedUntil": "gepauzeerd tot {{time}}",
339
+ "progress": "{{done}} van {{total}} sub-agents klaar",
340
+ "needsYou_one": "{{count}} vraagt aandacht",
341
+ "needsYou_other": "{{count}} vragen aandacht",
322
342
  "took": {
323
343
  "done": "klaar in {{duration}}",
324
344
  "failed": "mislukt na {{duration}}",
@@ -332,8 +352,10 @@
332
352
  "subagents": {
333
353
  "stop": {
334
354
  "task": "Deze taak stoppen",
355
+ "native": "Deze sub-agent stoppen",
335
356
  "mark": "Markeren als gestopt. De CLI kan een losse sub-agent niet stoppen, dus die werkt mogelijk door tot het proces van de chat eindigt."
336
357
  },
358
+ "stale": "Deze sub-agent hoort bij een proces dat al gestopt is.",
337
359
  "title": "Sub-agents",
338
360
  "backToMain": "Terug naar de main agent",
339
361
  "loading": "Gesprek laden…",
@@ -344,9 +366,7 @@
344
366
  "stopFailed": "De sub-agent kon niet worden gestopt",
345
367
  "info": {
346
368
  "label": "Over deze sub-agent",
347
- "tokens": "{{tokens}} tokens",
348
- "showTask": "Opdracht tonen",
349
- "hideTask": "Opdracht verbergen"
369
+ "tokens": "{{tokens}} tokens"
350
370
  }
351
371
  },
352
372
  "modes": {
@@ -382,7 +402,10 @@
382
402
  "noMatch": "Geen modellen gevonden",
383
403
  "legacyGroup_one": "Legacy-modellen ({{count}})",
384
404
  "legacyGroup_other": "Legacy-modellen ({{count}})",
385
- "legacy": "Legacy"
405
+ "legacy": "Legacy",
406
+ "requires": "Vereist {{cli}} {{version}} of nieuwer op {{machine}}",
407
+ "requiresHere": "Vereist {{cli}} {{version}} of nieuwer",
408
+ "providerSettings": "Providerinstellingen"
386
409
  },
387
410
  "stash": {
388
411
  "title": "Stashed prompts",
@@ -504,7 +527,13 @@
504
527
  "chart": {
505
528
  "values": "Waarden",
506
529
  "summary": "{{count}} waarden, hoogste {{label}} met {{value}}",
507
- "summarySeries": "{{count}} categorieën, hoogste {{series}} bij {{label}} met {{value}}"
530
+ "summarySeries": "{{count}} categorieën, hoogste {{series}} bij {{label}} met {{value}}",
531
+ "rowsLeftOut": "De eerste {{shown}} van {{total}} rijen zijn getekend.",
532
+ "seriesLeftOut_one": "{{series}} is niet getekend.",
533
+ "seriesLeftOut_other": "{{series}} zijn niet getekend.",
534
+ "sideBySide": "Negatieve waarden kunnen niet gestapeld worden, dus de reeksen staan naast elkaar.",
535
+ "showValues": "Alle waarden tonen",
536
+ "hideValues": "Waarden verbergen"
508
537
  },
509
538
  "image": {
510
539
  "label": "Afbeelding",
@@ -528,8 +557,8 @@
528
557
  "answered": "Beantwoord met “{{label}}”",
529
558
  "chose": "Gekozen: “{{label}}”",
530
559
  "choseOlder": "Gekozen: “{{label}}”, bij het antwoord van {{time}}",
531
- "fixes_one": "{{count}} correctie",
532
- "fixes_other": "{{count}} correcties",
560
+ "notes_one": "{{count}} opmerking",
561
+ "notes_other": "{{count}} opmerkingen",
533
562
  "shownAsText": "Als tekst getoond",
534
563
  "unknownComponent": "Deze versie kan {{component}} niet tekenen",
535
564
  "renderFailed": "Dit deel kon niet getekend worden",
@@ -1,4 +1,4 @@
1
- import type { ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem, ChatVisual } from '@adecore/agent-contracts';
1
+ import type { AgentKind, ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem, ChatVisual } from '@adecore/agent-contracts';
2
2
  export interface ChatState {
3
3
  info: ChatInfo;
4
4
  items: Record<string, ChatItem>;
@@ -43,5 +43,6 @@ export declare function applyEvent(state: ChatState, event: ChatEvent): ChatStat
43
43
  export declare const useChats: import("zustand").UseBoundStore<import("zustand").StoreApi<ChatsStore>>;
44
44
  export declare function chatSink(keyOf: (chatId: string) => string): ChatSink;
45
45
  export declare function useChatRow<T>(chatId: string, select: (row: ChatState | undefined) => T): T;
46
+ export declare function useReportedModels(provider: AgentKind): ReadonlySet<string>;
46
47
  export declare function useCurrentItem<T extends ChatItem>(chatId: string, derived: T): T;
47
48
  export {};
@@ -1,5 +1,6 @@
1
+ import { useContext, useMemo } from 'react';
1
2
  import { create } from 'zustand';
2
- import { useChatScope } from '../scope.js';
3
+ import { ChatScopeContext, useChatScope } from '../scope.js';
3
4
  function isWaiting(item) {
4
5
  return (item?.kind === 'approval' && item.decision === 'pending') || (item?.kind === 'question' && item.state === 'pending');
5
6
  }
@@ -209,6 +210,23 @@ export function useChatRow(chatId, select) {
209
210
  const { keyOf } = useChatScope();
210
211
  return useChats((s) => select(s.byKey[keyOf(chatId)]));
211
212
  }
213
+ /*
214
+ * The models the CLI of this provider reported running in the chats this client holds of the host.
215
+ * A model it ran is proof the CLI offers it, whatever the version says. Outside a scope there is none.
216
+ */
217
+ export function useReportedModels(provider) {
218
+ const scope = useContext(ChatScopeContext);
219
+ const joined = useChats((s) => {
220
+ const names = new Set();
221
+ for (const [key, state] of Object.entries(s.statusByKey)) {
222
+ if (scope?.owns(key) === true && state.info.provider === provider && state.info.model !== null) {
223
+ names.add(state.info.model);
224
+ }
225
+ }
226
+ return [...names].sort().join('\n');
227
+ });
228
+ return useMemo(() => new Set(joined === '' ? [] : joined.split('\n')), [joined]);
229
+ }
212
230
  /* The item as the thread holds it now, for a row derived from the structure, which a delta leaves alone. */
213
231
  export function useCurrentItem(chatId, derived) {
214
232
  return useChatRow(chatId, (row) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents-react",
3
- "version": "0.21.2",
3
+ "version": "0.22.1",
4
4
  "description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -34,6 +34,11 @@
34
34
  "types": "./dist/agents/accounts.d.ts",
35
35
  "default": "./dist/agents/accounts.js"
36
36
  },
37
+ "./agents/model-requirement": {
38
+ "source": "./src/agents/model-requirement.ts",
39
+ "types": "./dist/agents/model-requirement.d.ts",
40
+ "default": "./dist/agents/model-requirement.js"
41
+ },
37
42
  "./agents/model-name": {
38
43
  "source": "./src/agents/model-name.ts",
39
44
  "types": "./dist/agents/model-name.d.ts",
@@ -104,6 +109,11 @@
104
109
  "types": "./dist/chat/guards.d.ts",
105
110
  "default": "./dist/chat/guards.js"
106
111
  },
112
+ "./chat/logic/picker-entries": {
113
+ "source": "./src/chat/logic/picker-entries.ts",
114
+ "types": "./dist/chat/logic/picker-entries.d.ts",
115
+ "default": "./dist/chat/logic/picker-entries.js"
116
+ },
107
117
  "./chat/logic/ansi": {
108
118
  "source": "./src/chat/logic/ansi.ts",
109
119
  "types": "./dist/chat/logic/ansi.d.ts",
@@ -219,6 +229,11 @@
219
229
  "types": "./dist/chat/preferences.d.ts",
220
230
  "default": "./dist/chat/preferences.js"
221
231
  },
232
+ "./chat/prompt-hold": {
233
+ "source": "./src/chat/prompt-hold.ts",
234
+ "types": "./dist/chat/prompt-hold.d.ts",
235
+ "default": "./dist/chat/prompt-hold.js"
236
+ },
222
237
  "./chat/quote": {
223
238
  "source": "./src/chat/quote.ts",
224
239
  "types": "./dist/chat/quote.d.ts",
@@ -494,6 +509,11 @@
494
509
  "types": "./dist/chat/ui/composer/keys.d.ts",
495
510
  "default": "./dist/chat/ui/composer/keys.js"
496
511
  },
512
+ "./chat/ui/composer/menu-aria": {
513
+ "source": "./src/chat/ui/composer/menu-aria.ts",
514
+ "types": "./dist/chat/ui/composer/menu-aria.d.ts",
515
+ "default": "./dist/chat/ui/composer/menu-aria.js"
516
+ },
497
517
  "./chat/ui/diff-theme": {
498
518
  "source": "./src/chat/ui/diff-theme.ts",
499
519
  "types": "./dist/chat/ui/diff-theme.d.ts",
@@ -619,6 +639,11 @@
619
639
  "types": "./dist/chat/ui/intelligent-ui/use-later.d.ts",
620
640
  "default": "./dist/chat/ui/intelligent-ui/use-later.js"
621
641
  },
642
+ "./chat/ui/intelligent-ui/use-truncated": {
643
+ "source": "./src/chat/ui/intelligent-ui/use-truncated.ts",
644
+ "types": "./dist/chat/ui/intelligent-ui/use-truncated.d.ts",
645
+ "default": "./dist/chat/ui/intelligent-ui/use-truncated.js"
646
+ },
622
647
  "./chat/ui/intelligent-ui/use-ui-links": {
623
648
  "source": "./src/chat/ui/intelligent-ui/use-ui-links.ts",
624
649
  "types": "./dist/chat/ui/intelligent-ui/use-ui-links.d.ts",
@@ -1011,8 +1036,8 @@
1011
1036
  "test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
1012
1037
  },
1013
1038
  "dependencies": {
1014
- "@adecore/agent-contracts": "0.21.2",
1015
- "@adecore/intelligent-ui": "0.21.2",
1039
+ "@adecore/agent-contracts": "0.22.1",
1040
+ "@adecore/intelligent-ui": "0.22.1",
1016
1041
  "@codemirror/commands": "^6.11.1",
1017
1042
  "@codemirror/language": "^6.12.4",
1018
1043
  "@codemirror/state": "^6.7.6",
@@ -1033,7 +1058,7 @@
1033
1058
  "zustand": "^5.0.15"
1034
1059
  },
1035
1060
  "peerDependencies": {
1036
- "@adecore/ui": "^0.21.2",
1061
+ "@adecore/ui": "^0.22.1",
1037
1062
  "i18next": "^26.4.2",
1038
1063
  "react": "^19.3.0",
1039
1064
  "react-dom": "^19.3.0",
@@ -0,0 +1,36 @@
1
+ import type { ModelInfo, ProviderInfo } from '@adecore/agent-contracts';
2
+
3
+ export interface ModelRequirement {
4
+ /* The CLI's name, as the provider list gives it. */
5
+ cli: string;
6
+ minVersion: string;
7
+ }
8
+
9
+ function partsOf(version: string, strict: boolean): number[] | null {
10
+ const found = (strict ? /^(\d+)\.(\d+)(?:\.(\d+))?$/ : /(\d+)\.(\d+)\.(\d+)/).exec(version);
11
+ return found === null ? null : [Number(found[1]), Number(found[2]), Number(found[3] ?? 0)];
12
+ }
13
+
14
+ /* Whether the model's reported name is this model: its slug, the slug with a suffix such as `[1m]`. */
15
+ function isReported(slug: string, reported: ReadonlySet<string>): boolean {
16
+ return [...reported].some((name) => name === slug || name.startsWith(`${slug}[`));
17
+ }
18
+
19
+ /*
20
+ * What a model asks of the CLI that is too old for it, or null when nothing blocks it: no minimum is
21
+ * known, the CLI's version is not a readable x.y.z (so an unknown version invents no requirement), the
22
+ * CLI is new enough, or a chat of this provider already reported running the model.
23
+ */
24
+ export function modelRequirementOf(model: ModelInfo, provider: ProviderInfo, reported: ReadonlySet<string>): ModelRequirement | null {
25
+ const minimum = model.minCliVersion === undefined ? null : partsOf(model.minCliVersion, true);
26
+ const installed = provider.version === null ? null : partsOf(provider.version, false);
27
+ if (minimum === null || installed === null || isReported(model.slug, reported)) {
28
+ return null;
29
+ }
30
+ for (let i = 0; i < 3; i++) {
31
+ if (installed[i] !== minimum[i]) {
32
+ return (installed[i] ?? 0) > (minimum[i] ?? 0) ? null : { cli: provider.name, minVersion: model.minCliVersion ?? '' };
33
+ }
34
+ }
35
+ return null;
36
+ }
@@ -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
 
@@ -170,9 +173,11 @@ export class ChatClient {
170
173
  return this.transport.request('chat.send', {
171
174
  chatId,
172
175
  text,
176
+ delivery: extras.delivery,
173
177
  mentions: extras.mentions,
174
178
  skills: extras.skills,
175
179
  chats: extras.chats,
180
+ quoteSource: extras.quoteSource,
176
181
  attachments: extras.attachments
177
182
  });
178
183
  }
@@ -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 = { text: string; mentions?: string[]; skills?: string[]; chats?: string[]; attachments?: ChatAttachmentUpload[]; quote?: string };
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,
@@ -63,7 +73,8 @@ export function readDraft(chatId: string): ChatDraft {
63
73
  skills: record.skills ?? [],
64
74
  chats: record.chats ?? [],
65
75
  attachments: record.attachments ?? [],
66
- quote: record.quote ?? ''
76
+ quote: record.quote ?? '',
77
+ quoteSource: record.quoteSource ?? null
67
78
  }
68
79
  : EMPTY_DRAFT;
69
80
  }
@@ -82,13 +93,21 @@ export function writeDraft(chatId: string, draft: ChatDraft): void {
82
93
  skills: draft.skills,
83
94
  chats: draft.chats,
84
95
  attachments: draft.attachments,
85
- quote: draft.quote
96
+ quote: draft.quote,
97
+ ...(draft.quoteSource === null ? {} : { quoteSource: draft.quoteSource })
86
98
  };
87
99
  if (storage.write(drafts)) {
88
100
  return;
89
101
  }
90
102
  // A file can outgrow the storage quota; the text is the part worth keeping then.
91
- drafts[chatId] = { text: draft.text, mentions: draft.mentions, skills: draft.skills, chats: draft.chats, quote: draft.quote };
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
+ };
92
111
  storage.write(drafts);
93
112
  }
94
113
 
@@ -108,7 +127,7 @@ function union(first: readonly string[], second: readonly string[]): string[] {
108
127
  */
109
128
  export function takeBackIntoDraft(
110
129
  current: ChatDraft,
111
- taken: Omit<ChatDraft, 'quote'>
130
+ taken: Omit<ChatDraft, 'quote' | 'quoteSource'>
112
131
  ): { draft: ChatDraft; rejected: Array<{ name: string; reason: string }> } {
113
132
  const checked = checkAttachmentLimits(
114
133
  current.attachments.length,
@@ -122,7 +141,8 @@ export function takeBackIntoDraft(
122
141
  skills: union(taken.skills, current.skills),
123
142
  chats: union(taken.chats, current.chats),
124
143
  attachments: [...checked.accepted.map((entry) => entry.upload), ...current.attachments],
125
- quote: current.quote
144
+ quote: current.quote,
145
+ quoteSource: current.quoteSource
126
146
  },
127
147
  rejected: checked.rejected
128
148
  };
@@ -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
+ }
@@ -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 }
@@ -322,6 +322,12 @@ function rowsForItems(items: ChatItem[], options: TimelineOptions, children: Map
322
322
  return rows;
323
323
  }
324
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
+
325
331
  /* The card of a settled turn: the checkpoint diff when the host took one, else what the tool calls carry. */
326
332
  function changedFilesRow(turn: ChatTurnItem, items: ChatItem[]): TimelineRow | null {
327
333
  const edits = items.filter(
@@ -332,7 +338,16 @@ function changedFilesRow(turn: ChatTurnItem, items: ChatItem[]): TimelineRow | n
332
338
  if (empty) {
333
339
  return null;
334
340
  }
335
- return { kind: 'changed-files', id: `files-${turn.id}`, turnId: turn.id, tools: edits, diff, checkpoint: turn.checkpoint !== undefined };
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
+ };
336
351
  }
337
352
 
338
353
  function lastAssistantRow(rows: TimelineRow[]): TimelineRow | null {
@@ -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
+ }
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
- /* What a draft looks like on the shelf, its quote folded into the text; null when there is nothing worth putting there. */
81
- export function stashedFrom(draft: ChatDraft, id: string, now: number): StashedPrompt | null {
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) })),
@@ -106,8 +132,8 @@ function write(prompts: StashedPrompt[]): void {
106
132
  }
107
133
 
108
134
  /* Puts a draft on the shelf; false when the composer held nothing to put there. */
109
- export function stashDraft(draft: ChatDraft): boolean {
110
- 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);
111
137
  if (!entry) {
112
138
  return false;
113
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 of the
110
- * CLI's own is only marked stopped, since no CLI stops one on its own. While the chat's turn runs that
111
- * turn may still wait on it, so stopping the turn (the composer's Stop) is the only offer then.
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 stop === 'task' ? i18next.t('agent-chat:subagents.stop.task') : i18next.t('agent-chat:subagents.stop.mark');
140
+ return i18next.t(`agent-chat:subagents.stop.${stop}`);
127
141
  }
128
142
 
129
143
  /*