@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.
Files changed (145) hide show
  1. package/dist/agents/model-requirement.d.ts +6 -0
  2. package/dist/agents/model-requirement.js +26 -0
  3. package/dist/agents/status-look.js +2 -3
  4. package/dist/chat/chat-client.d.ts +3 -2
  5. package/dist/chat/chat-client.js +4 -2
  6. package/dist/chat/drafts.d.ts +2 -1
  7. package/dist/chat/drafts.js +21 -17
  8. package/dist/chat/logic/ansi.js +5 -1
  9. package/dist/chat/logic/fork.js +8 -9
  10. package/dist/chat/logic/limit.js +17 -14
  11. package/dist/chat/logic/picker-entries.d.ts +10 -0
  12. package/dist/chat/logic/picker-entries.js +19 -0
  13. package/dist/chat/logic/resume-compaction.js +2 -11
  14. package/dist/chat/logic/scrubber.js +9 -18
  15. package/dist/chat/logic/timeline.d.ts +5 -0
  16. package/dist/chat/logic/timeline.js +25 -11
  17. package/dist/chat/logic/tool-catalog.js +3 -4
  18. package/dist/chat/mentions.js +5 -6
  19. package/dist/chat/prompt-hold.d.ts +8 -0
  20. package/dist/chat/prompt-hold.js +16 -0
  21. package/dist/chat/quote.d.ts +4 -0
  22. package/dist/chat/quote.js +19 -0
  23. package/dist/chat/runtime-modes.js +2 -4
  24. package/dist/chat/stash.d.ts +8 -2
  25. package/dist/chat/stash.js +22 -8
  26. package/dist/chat/subagent-list.d.ts +7 -1
  27. package/dist/chat/subagent-list.js +13 -1
  28. package/dist/chat/thumbnails.js +3 -4
  29. package/dist/chat/timeline-scroll.d.ts +6 -2
  30. package/dist/chat/timeline-scroll.js +45 -46
  31. package/dist/chat/ui/BookmarkSubmenu.js +1 -5
  32. package/dist/chat/ui/ChatActivity.js +22 -8
  33. package/dist/chat/ui/CodeBlock.js +2 -3
  34. package/dist/chat/ui/Composer.js +160 -88
  35. package/dist/chat/ui/ComposerInput.d.ts +3 -1
  36. package/dist/chat/ui/ComposerInput.js +13 -4
  37. package/dist/chat/ui/Markdown.js +11 -11
  38. package/dist/chat/ui/Pickers.js +31 -27
  39. package/dist/chat/ui/PromptComposer.d.ts +8 -1
  40. package/dist/chat/ui/PromptComposer.js +18 -6
  41. package/dist/chat/ui/QuoteButton.js +4 -5
  42. package/dist/chat/ui/RunSettings.js +8 -2
  43. package/dist/chat/ui/Scrubber.js +2 -3
  44. package/dist/chat/ui/SubagentControls.d.ts +4 -2
  45. package/dist/chat/ui/SubagentControls.js +13 -11
  46. package/dist/chat/ui/SubagentInfo.d.ts +1 -1
  47. package/dist/chat/ui/SubagentInfo.js +9 -15
  48. package/dist/chat/ui/SubagentStopButton.js +2 -1
  49. package/dist/chat/ui/Timeline.js +39 -21
  50. package/dist/chat/ui/TimelineMenu.js +3 -9
  51. package/dist/chat/ui/UnifiedDiff.js +3 -5
  52. package/dist/chat/ui/VisualFrame.js +3 -4
  53. package/dist/chat/ui/composer/keys.js +19 -11
  54. package/dist/chat/ui/composer/menu-aria.d.ts +10 -0
  55. package/dist/chat/ui/composer/menu-aria.js +18 -0
  56. package/dist/chat/ui/intelligent-ui/render-context.d.ts +1 -1
  57. package/dist/chat/ui/quote-selection.d.ts +5 -2
  58. package/dist/chat/ui/quote-selection.js +3 -2
  59. package/dist/chat/ui/rows/MessageRows.js +26 -8
  60. package/dist/chat/ui/rows/Rows.js +1 -1
  61. package/dist/chat/ui/rows/WorkRows.d.ts +7 -4
  62. package/dist/chat/ui/rows/WorkRows.js +22 -17
  63. package/dist/chat/visual-viewport.d.ts +1 -1
  64. package/dist/chat/visual-viewport.js +9 -8
  65. package/dist/host.d.ts +1 -0
  66. package/dist/host.js +1 -0
  67. package/dist/locales/en/agent-chat.json +30 -7
  68. package/dist/locales/nl/agent-chat.json +30 -7
  69. package/dist/port-transport.js +4 -8
  70. package/dist/state/chats.d.ts +2 -1
  71. package/dist/state/chats.js +29 -15
  72. package/dist/state/usage.js +44 -49
  73. package/dist/storage.js +7 -7
  74. package/dist/theme.css +6 -14
  75. package/dist/usage/LimitsList.js +1 -2
  76. package/dist/usage/UsageBreakdown.js +1 -3
  77. package/dist/usage/UsageDialog.js +2 -4
  78. package/dist/usage/format.js +6 -6
  79. package/dist/usage/money.js +1 -5
  80. package/dist/usage/summary.js +2 -4
  81. package/package.json +24 -4
  82. package/src/agents/model-requirement.ts +36 -0
  83. package/src/agents/status-look.ts +2 -3
  84. package/src/chat/chat-client.ts +7 -3
  85. package/src/chat/drafts.ts +33 -21
  86. package/src/chat/logic/ansi.ts +5 -1
  87. package/src/chat/logic/fork.ts +8 -9
  88. package/src/chat/logic/limit.ts +17 -16
  89. package/src/chat/logic/picker-entries.ts +25 -0
  90. package/src/chat/logic/resume-compaction.ts +2 -11
  91. package/src/chat/logic/scrubber.ts +13 -17
  92. package/src/chat/logic/timeline.ts +39 -13
  93. package/src/chat/logic/tool-catalog.ts +7 -6
  94. package/src/chat/mentions.ts +5 -6
  95. package/src/chat/prompt-hold.ts +27 -0
  96. package/src/chat/quote.ts +20 -0
  97. package/src/chat/runtime-modes.ts +2 -4
  98. package/src/chat/stash.ts +32 -8
  99. package/src/chat/subagent-list.ts +19 -5
  100. package/src/chat/thumbnails.ts +3 -4
  101. package/src/chat/timeline-scroll.ts +58 -53
  102. package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
  103. package/src/chat/ui/ChatActivity.tsx +35 -9
  104. package/src/chat/ui/CodeBlock.tsx +2 -3
  105. package/src/chat/ui/Composer.tsx +263 -138
  106. package/src/chat/ui/ComposerInput.tsx +18 -3
  107. package/src/chat/ui/Markdown.tsx +13 -17
  108. package/src/chat/ui/Pickers.tsx +56 -39
  109. package/src/chat/ui/PromptComposer.tsx +37 -8
  110. package/src/chat/ui/QuoteButton.tsx +5 -5
  111. package/src/chat/ui/RunSettings.tsx +9 -2
  112. package/src/chat/ui/Scrubber.tsx +2 -3
  113. package/src/chat/ui/SubagentControls.tsx +23 -22
  114. package/src/chat/ui/SubagentInfo.tsx +24 -42
  115. package/src/chat/ui/SubagentStopButton.tsx +2 -1
  116. package/src/chat/ui/SubagentTimeline.tsx +1 -2
  117. package/src/chat/ui/Timeline.tsx +44 -24
  118. package/src/chat/ui/TimelineMenu.tsx +3 -9
  119. package/src/chat/ui/UnifiedDiff.tsx +3 -5
  120. package/src/chat/ui/VisualFrame.tsx +3 -4
  121. package/src/chat/ui/composer/keys.ts +21 -11
  122. package/src/chat/ui/composer/menu-aria.ts +29 -0
  123. package/src/chat/ui/intelligent-ui/render-context.ts +2 -2
  124. package/src/chat/ui/quote-selection.ts +6 -4
  125. package/src/chat/ui/rehype-chips.ts +1 -1
  126. package/src/chat/ui/rehype-fade.ts +1 -1
  127. package/src/chat/ui/remark-html-as-text.ts +1 -1
  128. package/src/chat/ui/rows/MessageRows.tsx +47 -9
  129. package/src/chat/ui/rows/Rows.tsx +3 -1
  130. package/src/chat/ui/rows/WorkRows.tsx +101 -70
  131. package/src/chat/visual-viewport.ts +10 -8
  132. package/src/host.ts +4 -3
  133. package/src/locales/en/agent-chat.json +30 -7
  134. package/src/locales/nl/agent-chat.json +30 -7
  135. package/src/port-transport.ts +4 -8
  136. package/src/state/chats.ts +42 -16
  137. package/src/state/usage.ts +49 -60
  138. package/src/storage.ts +7 -7
  139. package/src/theme.css +6 -14
  140. package/src/usage/LimitsList.tsx +2 -4
  141. package/src/usage/UsageBreakdown.tsx +2 -5
  142. package/src/usage/UsageDialog.tsx +2 -4
  143. package/src/usage/format.ts +6 -6
  144. package/src/usage/money.ts +1 -5
  145. package/src/usage/summary.ts +2 -4
@@ -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",
@@ -93,15 +93,11 @@ export function portTransport(port) {
93
93
  return promise;
94
94
  },
95
95
  on(event, handler) {
96
- let handlers = eventHandlers.get(event);
97
- if (!handlers) {
98
- handlers = new Set();
99
- eventHandlers.set(event, handlers);
100
- }
101
- const set = handlers;
102
- set.add(handler);
96
+ const handlers = eventHandlers.get(event) ?? new Set();
97
+ eventHandlers.set(event, handlers);
98
+ handlers.add(handler);
103
99
  return () => {
104
- set.delete(handler);
100
+ handlers.delete(handler);
105
101
  };
106
102
  },
107
103
  subscribeStatus(handler) {
@@ -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
  }
@@ -113,6 +114,14 @@ export function applyEvent(state, event) {
113
114
  return stateOf(event.info, event.items);
114
115
  }
115
116
  }
117
+ /* Fields of a held chat that are not part of its thread; a chat this client does not hold stays as it is. */
118
+ function withFields(store, key, fields) {
119
+ const current = store.byKey[key];
120
+ if (!current) {
121
+ return {};
122
+ }
123
+ return { byKey: { ...store.byKey, [key]: { ...current, ...fields } } };
124
+ }
116
125
  export const useChats = create((set) => ({
117
126
  byKey: {},
118
127
  statusByKey: {},
@@ -166,22 +175,10 @@ export const useChats = create((set) => ({
166
175
  });
167
176
  },
168
177
  bookmarks(key, bookmarks) {
169
- set((s) => {
170
- const current = s.byKey[key];
171
- if (!current) {
172
- return {};
173
- }
174
- return { byKey: { ...s.byKey, [key]: { ...current, bookmarks } } };
175
- });
178
+ set((s) => withFields(s, key, { bookmarks }));
176
179
  },
177
180
  visuals(key, visuals) {
178
- set((s) => {
179
- const current = s.byKey[key];
180
- if (!current) {
181
- return {};
182
- }
183
- return { byKey: { ...s.byKey, [key]: { ...current, visuals } } };
184
- });
181
+ set((s) => withFields(s, key, { visuals }));
185
182
  },
186
183
  forget(key) {
187
184
  set((s) => {
@@ -213,6 +210,23 @@ export function useChatRow(chatId, select) {
213
210
  const { keyOf } = useChatScope();
214
211
  return useChats((s) => select(s.byKey[keyOf(chatId)]));
215
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
+ }
216
230
  /* The item as the thread holds it now, for a row derived from the structure, which a delta leaves alone. */
217
231
  export function useCurrentItem(chatId, derived) {
218
232
  return useChatRow(chatId, (row) => {
@@ -32,12 +32,6 @@ function parsePreferences(raw) {
32
32
  }
33
33
  }
34
34
  const storage = persistedJson(storageKey, parsePreferences, DEFAULTS, () => chatStorageLegacyKeys('usage'));
35
- function readPreferences() {
36
- return storage.read();
37
- }
38
- function persist(preferences) {
39
- storage.write(preferences);
40
- }
41
35
  /* The three stored choices out of the store, so a setter writes the other two back unchanged. */
42
36
  function chosen(state) {
43
37
  return { period: state.period, metric: state.metric, currency: state.currency };
@@ -49,10 +43,7 @@ function pad(value) {
49
43
  export function dayOf(date) {
50
44
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
51
45
  }
52
- /*
53
- * What a period asks for. Today counts from local midnight in hours, the way the commit log already
54
- * groups days, so every other period reads as one bar per calendar day.
55
- */
46
+ /* What a period asks for: today in hours from local midnight, every other period a bar per calendar day. */
56
47
  export function windowFor(period, now = new Date()) {
57
48
  const days = USAGE_PERIODS.find((entry) => entry.id === period)?.days ?? 7;
58
49
  const from = new Date(now.getFullYear(), now.getMonth(), now.getDate() - (days - 1));
@@ -75,46 +66,50 @@ export function askedKey(payload) {
75
66
  const EMPTY = { asked: null, summary: null, loading: false, failed: false, limits: null };
76
67
  /* Reads the period, metric and currency again, once another window of the same origin wrote them. */
77
68
  export function reloadUsagePreferences() {
78
- useUsageStore.setState(readPreferences());
69
+ useUsageStore.setState(storage.read());
79
70
  }
80
- export const useUsageStore = createChatStore((set, get) => ({
81
- ...DEFAULTS,
82
- byScope: {},
83
- chosen: null,
84
- choose(scopeId) {
85
- set({ chosen: scopeId });
86
- },
87
- setPeriod(period) {
88
- persist({ ...chosen(get()), period });
89
- set({ period });
90
- },
91
- setMetric(metric) {
92
- persist({ ...chosen(get()), metric });
93
- set({ metric });
94
- },
95
- setCurrency(currency) {
96
- persist({ ...chosen(get()), currency });
97
- set({ currency });
98
- },
99
- setLoading(scopeId, loading) {
100
- set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), loading } } });
101
- },
102
- receive(scopeId, asked, summary) {
103
- set({
104
- byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), asked, summary, loading: false, failed: false } }
105
- });
106
- },
107
- fail(scopeId) {
108
- set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), loading: false, failed: true } } });
109
- },
110
- setLimits(scopeId, limits) {
111
- set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), limits } } });
112
- },
113
- forget(scopeId) {
114
- const { [scopeId]: _gone, ...rest } = get().byScope;
115
- set({ byScope: rest });
116
- }
117
- }), (store) => store.setState(readPreferences()));
71
+ export const useUsageStore = createChatStore((set, get) => {
72
+ const choosePreference = (change) => {
73
+ storage.write({ ...chosen(get()), ...change });
74
+ set(change);
75
+ };
76
+ const patchRow = (scopeId, change) => {
77
+ set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), ...change } } });
78
+ };
79
+ return {
80
+ ...DEFAULTS,
81
+ byScope: {},
82
+ chosen: null,
83
+ choose(scopeId) {
84
+ set({ chosen: scopeId });
85
+ },
86
+ setPeriod(period) {
87
+ choosePreference({ period });
88
+ },
89
+ setMetric(metric) {
90
+ choosePreference({ metric });
91
+ },
92
+ setCurrency(currency) {
93
+ choosePreference({ currency });
94
+ },
95
+ setLoading(scopeId, loading) {
96
+ patchRow(scopeId, { loading });
97
+ },
98
+ receive(scopeId, asked, summary) {
99
+ patchRow(scopeId, { asked, summary, loading: false, failed: false });
100
+ },
101
+ fail(scopeId) {
102
+ patchRow(scopeId, { loading: false, failed: true });
103
+ },
104
+ setLimits(scopeId, limits) {
105
+ patchRow(scopeId, { limits });
106
+ },
107
+ forget(scopeId) {
108
+ const { [scopeId]: _gone, ...rest } = get().byScope;
109
+ set({ byScope: rest });
110
+ }
111
+ };
112
+ }, (store) => store.setState(storage.read()));
118
113
  export function useUsage(select) {
119
114
  const { id } = useChatScope();
120
115
  return useUsageStore((s) => select({ period: s.period, metric: s.metric, currency: s.currency, ...(s.byScope[id] ?? EMPTY) }));
package/dist/storage.js CHANGED
@@ -64,25 +64,25 @@ export function createChatStore(initial, hydrate) {
64
64
  hydrate(store);
65
65
  };
66
66
  hydrators.push(hydrateOnce);
67
- const useStore = ((...args) => {
67
+ const ready = () => {
68
68
  hydrateChatStorage();
69
69
  hydrateOnce();
70
+ };
71
+ const useStore = ((...args) => {
72
+ ready();
70
73
  return store(...args);
71
74
  });
72
75
  Object.assign(useStore, store);
73
76
  useStore.getState = () => {
74
- hydrateChatStorage();
75
- hydrateOnce();
77
+ ready();
76
78
  return store.getState();
77
79
  };
78
80
  useStore.subscribe = (...args) => {
79
- hydrateChatStorage();
80
- hydrateOnce();
81
+ ready();
81
82
  return store.subscribe(...args);
82
83
  };
83
84
  useStore.setState = ((...args) => {
84
- hydrateChatStorage();
85
- hydrateOnce();
85
+ ready();
86
86
  store.setState(...args);
87
87
  });
88
88
  return useStore;
package/dist/theme.css CHANGED
@@ -76,8 +76,6 @@
76
76
  }
77
77
 
78
78
  @layer components {
79
- /* ---------------------------------------------------------------- Chat */
80
-
81
79
  /* Text in a chat is there to be read and copied, so the thread selects even though the app does
82
80
  not. Its chrome stays out of it: a drag on a fold header or a chip would select the row.
83
81
  The pretty wrap from `body` would be recomputed on the growing paragraph every frame a reply
@@ -160,18 +158,12 @@
160
158
  }
161
159
 
162
160
  /*
163
- * Markdown, outside every layer on purpose. The typography plugin registers `.prose` as a utility,
164
- * and a later layer beats an earlier one whatever the specificity, so a rule that has to outrank
165
- * prose cannot live in `components`. Unlayered styles win over every layer, which is the one
166
- * place the app's tokens and scale are sure to land on top of the plugin's.
167
- */
168
- /*
169
- * Markdown anywhere in the app: the chat timeline, a note node and the file preview all render
170
- * the same `Markdown` component, which carries `prose prose-sm max-w-none text-sm` next to this
171
- * class. Prose brings the rhythm; everything here is what the app decides instead of the plugin.
172
- * The semantic tokens flip with the theme on their own, so the prose variables read from them
173
- * and `prose-invert` is a second source of truth the app does not need. The size comes from the
174
- * `text-sm` utility, which a node, a view and the file preview each move.
161
+ * Markdown, outside every layer on purpose: the typography plugin registers `.prose` as a utility,
162
+ * and only unlayered styles are sure to land on top of it.
163
+ *
164
+ * Every `Markdown` carries `prose prose-sm max-w-none text-sm` next to this class. Prose brings the
165
+ * rhythm; everything here is what the app decides instead. The semantic tokens flip with the theme on
166
+ * their own, so the prose variables read from them and `prose-invert` is not needed.
175
167
  */
176
168
  .prose.chat-markdown {
177
169
  --tw-prose-body: var(--text);
@@ -9,8 +9,7 @@ import { formatClock, formatWeekdayClock, isSameDay, formatCountdown, formatPerc
9
9
  import { ProviderLogo } from '../agents/ProviderLogo.js';
10
10
  import { PROVIDER_COLORS, PROVIDER_LABELS } from './format.js';
11
11
  import { accountNote, explain, isSignedOut, nextReset } from './limit-groups.js';
12
- /* Red where a window is nearly spent, amber where it is worth knowing. A window with room to spare
13
- is not news, so it takes the text color rather than a hue that competes with the two that are. */
12
+ /* Red where a window is nearly spent, amber where it is worth knowing; room to spare is not news and takes the text color. */
14
13
  function toneOf(used) {
15
14
  return used >= 0.9 ? 'bg-status-error' : used >= 0.7 ? 'bg-status-needs-you' : 'bg-text';
16
15
  }
@@ -12,11 +12,9 @@ import { modelNameFromSlug } from '../agents/model-name.js';
12
12
  import { formatCount, formatTokens, PROVIDER_COLORS, PROVIDER_LABELS, shortPath, slotLabel } from './format.js';
13
13
  import { useMoney } from './money.js';
14
14
  import { deriveDays } from './summary.js';
15
- /* The row of every table, the same height and the same hover as a commit row in the git panel. */
16
15
  const ROW = 'flex h-8 items-center gap-2 rounded-md px-2 text-xs hover:bg-surface-hover';
17
16
  const HEAD = 'flex h-6 items-center gap-2 px-2 text-xs text-text-faint';
18
- /* A row is padded so its hover has room around the text, and the table pulls that padding back off
19
- again. The first column then starts on the same line as every other section of the page. */
17
+ /* The table pulls the padding that gives a row's hover room back off, so its first column lines up with the page. */
20
18
  const TABLE = '-mx-2 flex flex-col';
21
19
  const TABS = ['models', 'projects', 'day'];
22
20
  /* A share of the biggest row, never under one pixel. A row that did something has to be visible. */
@@ -2,10 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import i18next from 'i18next';
3
3
  import { Dialog } from '@adecore/ui';
4
4
  /*
5
- * The frame of the usage page: larger than the settings, since the chart and the breakdown need the
6
- * width. What goes in it (`UsagePage`, in an `ErrorBoundary` of the app's) mounts only while it is
7
- * open, so nothing is scanned behind a closed dialog. It is a dialog and not a view, since none of it
8
- * belongs to one project.
5
+ * The frame of the usage page, wider than the settings for the chart and the breakdown. What goes in
6
+ * it mounts only while it is open, so nothing is scanned behind a closed dialog.
9
7
  */
10
8
  export function UsageDialog({ open, onOpenChange, children }) {
11
9
  return (_jsx(Dialog.Root, { open: open, onOpenChange: onOpenChange, children: _jsxs(Dialog.Popup, { className: "flex h-[820px] w-[1080px] flex-col", children: [children, _jsx(Dialog.Description, { className: "sr-only", children: i18next.t('agent-usage:dialog.description') })] }) }));
@@ -1,7 +1,6 @@
1
1
  import { formatDay, formatHour, formatWeekdayDay, formatMoney, formatNumber, formatTokens } from '@adecore/ui/format';
2
2
  export { formatClock } from '@adecore/ui/format';
3
- /* Every number on this page is read in the region of the machine looking at it, as a bank statement
4
- would be: `$5,480.96` in the US, `$ 5.480,96` in the Netherlands. The labels stay English. */
3
+ /* Read in the region of the machine looking at it: `$5,480.96` in the US, `$ 5.480,96` in the Netherlands. */
5
4
  export function formatCount(value) {
6
5
  return formatNumber(value);
7
6
  }
@@ -23,8 +22,10 @@ export function moneyFormat(currency, rate) {
23
22
  }
24
23
  export const PROVIDER_LABELS = { claude: 'Claude Code', codex: 'Codex' };
25
24
  export const PROVIDER_COLORS = { claude: 'var(--chart-claude)', codex: 'var(--chart-codex)' };
26
- /* A slot is `2026-09-10` or `2026-09-10T14`, already in the viewer's own zone, so it is read as
27
- plain wall clock and never handed to a parser that would shift it back. */
25
+ /*
26
+ * A slot is `2026-09-10` or `2026-09-10T14`, already in the viewer's own zone, so it is read as plain
27
+ * wall clock and never handed to a parser that would shift it back.
28
+ */
28
29
  function dateOfSlot(slot) {
29
30
  return new Date(Number(slot.slice(0, 4)), Number(slot.slice(5, 7)) - 1, Number(slot.slice(8, 10)));
30
31
  }
@@ -47,8 +48,7 @@ export function slotAxisLabel(slot) {
47
48
  export function formatDate(at) {
48
49
  return formatDay(at);
49
50
  }
50
- /* A path under a row that already carries the name: enough of the tail to tell two checkouts of the
51
- same repository apart, without the home directory nobody needs to read again. */
51
+ /* Enough of a path's tail to tell two checkouts of the same repository apart, under a row that already carries the name. */
52
52
  export function shortPath(path) {
53
53
  const parts = path.split('/').filter(Boolean);
54
54
  return parts.length <= 3 ? path : `…/${parts.slice(-3).join('/')}`;
@@ -1,11 +1,7 @@
1
1
  import { useMemo } from 'react';
2
2
  import { useUsage } from '../state/usage.js';
3
3
  import { moneyFormat } from './format.js';
4
- /*
5
- * The formatter every amount on the page goes through: the currency that was chosen, at the rate
6
- * the summary on screen carries. It is a hook rather than a prop because every row wants it and
7
- * none of them wants to know where the rate came from.
8
- */
4
+ /* The formatter every amount on the page goes through: the chosen currency, at the rate the summary on screen carries. */
9
5
  export function useMoney() {
10
6
  const currency = useUsage((s) => s.currency);
11
7
  const rate = useUsage((s) => s.summary?.rate ?? null);
@@ -74,10 +74,8 @@ export function deriveUsage(summary, metric) {
74
74
  };
75
75
  }
76
76
  /*
77
- * The same buckets the chart draws, written out as a table of calendar days, newest first. Today is
78
- * drawn per hour but read per day, so an hour slot is folded onto the date it names. A day nothing
79
- * happened in is left out, since the chart already shows the gap, and a quiet fortnight would otherwise
80
- * be fourteen rows of zeroes between the days worth reading.
77
+ * The chart's buckets as a table of calendar days, newest first; an hour slot folds onto the date it
78
+ * names. A day nothing happened in is left out, since the chart already shows the gap.
81
79
  */
82
80
  export function deriveDays(summary) {
83
81
  const rows = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents-react",
3
- "version": "0.21.1",
3
+ "version": "0.22.0",
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",
@@ -1011,8 +1031,8 @@
1011
1031
  "test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
1012
1032
  },
1013
1033
  "dependencies": {
1014
- "@adecore/agent-contracts": "0.21.1",
1015
- "@adecore/intelligent-ui": "0.21.1",
1034
+ "@adecore/agent-contracts": "0.22.0",
1035
+ "@adecore/intelligent-ui": "0.22.0",
1016
1036
  "@codemirror/commands": "^6.11.1",
1017
1037
  "@codemirror/language": "^6.12.4",
1018
1038
  "@codemirror/state": "^6.7.6",
@@ -1033,7 +1053,7 @@
1033
1053
  "zustand": "^5.0.15"
1034
1054
  },
1035
1055
  "peerDependencies": {
1036
- "@adecore/ui": "^0.21.1",
1056
+ "@adecore/ui": "^0.22.0",
1037
1057
  "i18next": "^26.4.2",
1038
1058
  "react": "^19.3.0",
1039
1059
  "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
+ }
@@ -10,9 +10,8 @@ export interface StatusLook {
10
10
  }
11
11
 
12
12
  /*
13
- * One state, one mark: an entry in the sub-agent flyout, a task on its node (`TaskMark`), what finished
14
- * in the toolbar (`StatusSummary`) and work in progress in a toast. A spinner stops under reduced
15
- * motion with every other animation (`styles.css`).
13
+ * One state, one mark, wherever work is drawn: a sub-agent entry, a task, a summary or a toast. A
14
+ * spinner stops under reduced motion with every other animation.
16
15
  */
17
16
  const LOOKS: Record<StatusWord, StatusLook> = {
18
17
  running: { icon: LoaderCircle, tone: 'text-status-running', spins: true },