@adecore/agents-react 0.36.0 → 0.37.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 (56) hide show
  1. package/dist/agents/provider-paths.js +3 -1
  2. package/dist/chat/chat-client.d.ts +5 -0
  3. package/dist/chat/chat-client.js +91 -20
  4. package/dist/chat/index.d.ts +1 -0
  5. package/dist/chat/index.js +2 -1
  6. package/dist/chat/logic/picker-entries.js +11 -6
  7. package/dist/chat/logic/scrubber.d.ts +4 -2
  8. package/dist/chat/logic/scrubber.js +3 -3
  9. package/dist/chat/pending-prompts.d.ts +15 -0
  10. package/dist/chat/pending-prompts.js +93 -0
  11. package/dist/chat/reading-width.js +6 -2
  12. package/dist/chat/ui/PromptComposer.d.ts +2 -2
  13. package/dist/chat/ui/PromptComposer.js +92 -98
  14. package/dist/chat/ui/SubagentTimeline.js +128 -182
  15. package/dist/chat/ui/Timeline.js +86 -60
  16. package/dist/chat/ui/code-lines.d.ts +5 -0
  17. package/dist/chat/ui/code-lines.js +37 -0
  18. package/dist/chat/ui/composer/use-pending-requests.d.ts +2 -2
  19. package/dist/chat/ui/composer/use-pending-requests.js +29 -37
  20. package/dist/chat/ui/highlighter.d.ts +1 -1
  21. package/dist/chat/ui/highlighter.js +4 -2
  22. package/dist/locales/en/agent-chat.d.ts +1 -0
  23. package/dist/locales/en/agent-chat.js +1 -0
  24. package/dist/locales/nl/agent-chat.d.ts +1 -0
  25. package/dist/locales/nl/agent-chat.js +1 -0
  26. package/dist/prompts/logic/subjects.d.ts +5 -0
  27. package/dist/prompts/ui/PromptCard.js +1 -1
  28. package/dist/prompts/ui/PromptView.js +104 -95
  29. package/dist/providers/parts.js +2 -1
  30. package/dist/state/chats.d.ts +4 -0
  31. package/dist/state/chats.js +84 -48
  32. package/dist/theme.css +6 -2
  33. package/dist/usage/format.js +4 -2
  34. package/package.json +8 -8
  35. package/src/agents/provider-paths.ts +5 -1
  36. package/src/chat/chat-client.ts +92 -21
  37. package/src/chat/index.ts +2 -0
  38. package/src/chat/logic/picker-entries.ts +6 -4
  39. package/src/chat/logic/scrubber.ts +8 -6
  40. package/src/chat/pending-prompts.ts +108 -0
  41. package/src/chat/reading-width.ts +9 -2
  42. package/src/chat/ui/PromptComposer.tsx +10 -7
  43. package/src/chat/ui/SubagentTimeline.tsx +1 -24
  44. package/src/chat/ui/Timeline.tsx +66 -40
  45. package/src/chat/ui/code-lines.ts +37 -0
  46. package/src/chat/ui/composer/use-pending-requests.ts +13 -21
  47. package/src/chat/ui/highlighter.ts +4 -3
  48. package/src/locales/en/agent-chat.ts +1 -0
  49. package/src/locales/nl/agent-chat.ts +1 -0
  50. package/src/prompts/logic/subjects.ts +4 -1
  51. package/src/prompts/ui/PromptCard.tsx +1 -1
  52. package/src/prompts/ui/PromptView.tsx +7 -1
  53. package/src/providers/parts.tsx +2 -1
  54. package/src/state/chats.ts +63 -18
  55. package/src/theme.css +6 -2
  56. package/src/usage/format.ts +4 -2
@@ -1,5 +1,6 @@
1
1
  import { c as _c } from "react/compiler-runtime";
2
2
  import { useTranslation } from 'react-i18next';
3
+ import { Pill } from '@adecore/ui';
3
4
  import { toolSummary } from '../../chat/logic/tools.js';
4
5
  import { useChatHost } from '../../host.js';
5
6
  import { answerValue, promptAnswers, questionAnswer } from '../logic/prompts.js';
@@ -11,7 +12,7 @@ import { QuestionBody } from './QuestionBody.js';
11
12
  /* A permission request or question, answered in place: a chat's, or one of the app's own that it draws itself. */
12
13
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
13
14
  export function PromptView(props) {
14
- const $ = _c(98);
15
+ const $ = _c(99);
15
16
  const {
16
17
  subject,
17
18
  draft,
@@ -143,9 +144,16 @@ export function PromptView(props) {
143
144
  const commit = t4;
144
145
  const multiple = item.kind === "question" && item.questions.length > 1;
145
146
  let t5;
146
- if ($[27] !== draft.index || $[28] !== hasDraft || $[29] !== item.questions || $[30] !== more || $[31] !== multiple || $[32] !== t) {
147
- t5 = more > 0 || hasDraft || multiple ? /*#__PURE__*/_jsxs(_Fragment, {
148
- children: [more > 0 && /*#__PURE__*/_jsx("span", {
147
+ if ($[27] !== draft.index || $[28] !== hasDraft || $[29] !== item.questions || $[30] !== more || $[31] !== multiple || $[32] !== subject.source || $[33] !== t) {
148
+ t5 = subject.source !== undefined || more > 0 || hasDraft || multiple ? /*#__PURE__*/_jsxs(_Fragment, {
149
+ children: [subject.source && /*#__PURE__*/_jsx(Pill, {
150
+ size: "sm",
151
+ className: "max-w-full",
152
+ children: /*#__PURE__*/_jsx("span", {
153
+ className: "truncate",
154
+ children: subject.source.label
155
+ })
156
+ }), more > 0 && /*#__PURE__*/_jsx("span", {
149
157
  className: "tabular-nums",
150
158
  children: t("meta.waiting", {
151
159
  count: more + 1
@@ -165,63 +173,64 @@ export function PromptView(props) {
165
173
  $[29] = item.questions;
166
174
  $[30] = more;
167
175
  $[31] = multiple;
168
- $[32] = t;
169
- $[33] = t5;
176
+ $[32] = subject.source;
177
+ $[33] = t;
178
+ $[34] = t5;
170
179
  } else {
171
- t5 = $[33];
180
+ t5 = $[34];
172
181
  }
173
182
  const meta = t5;
174
183
  if (item.kind === "approval") {
175
184
  let t6;
176
- if ($[34] !== item.toolName || $[35] !== t) {
185
+ if ($[35] !== item.toolName || $[36] !== t) {
177
186
  t6 = item.toolName === "Bash" ? t("approval.runCommand") : item.toolName;
178
- $[34] = item.toolName;
179
- $[35] = t;
180
- $[36] = t6;
187
+ $[35] = item.toolName;
188
+ $[36] = t;
189
+ $[37] = t6;
181
190
  } else {
182
- t6 = $[36];
191
+ t6 = $[37];
183
192
  }
184
193
  let t7;
185
- if ($[37] !== item.input || $[38] !== item.toolName) {
194
+ if ($[38] !== item.input || $[39] !== item.toolName) {
186
195
  t7 = item.toolName === "Bash" ? "" : toolSummary(item.toolName, item.input);
187
- $[37] = item.input;
188
- $[38] = item.toolName;
189
- $[39] = t7;
196
+ $[38] = item.input;
197
+ $[39] = item.toolName;
198
+ $[40] = t7;
190
199
  } else {
191
- t7 = $[39];
200
+ t7 = $[40];
192
201
  }
193
202
  const t8 = `${t6} ${t7}`;
194
203
  let t9;
195
- if ($[40] !== denyReason || $[41] !== draft || $[42] !== onDraft) {
204
+ if ($[41] !== denyReason || $[42] !== draft || $[43] !== onDraft) {
196
205
  t9 = denyReason && !draft.showReason ? () => onDraft({
197
206
  ...draft,
198
207
  showReason: true
199
208
  }) : undefined;
200
- $[40] = denyReason;
201
- $[41] = draft;
202
- $[42] = onDraft;
203
- $[43] = t9;
209
+ $[41] = denyReason;
210
+ $[42] = draft;
211
+ $[43] = onDraft;
212
+ $[44] = t9;
204
213
  } else {
205
- t9 = $[43];
214
+ t9 = $[44];
206
215
  }
207
216
  let t10;
208
- if ($[44] !== buttons || $[45] !== locked || $[46] !== sending || $[47] !== t9) {
217
+ if ($[45] !== buttons || $[46] !== locked || $[47] !== sending || $[48] !== t9) {
209
218
  t10 = /*#__PURE__*/_jsx(ApprovalActions, {
210
219
  buttons: buttons,
211
220
  onAddReason: t9,
212
221
  locked: locked,
213
222
  sending: sending
214
223
  });
215
- $[44] = buttons;
216
- $[45] = locked;
217
- $[46] = sending;
218
- $[47] = t9;
219
- $[48] = t10;
224
+ $[45] = buttons;
225
+ $[46] = locked;
226
+ $[47] = sending;
227
+ $[48] = t9;
228
+ $[49] = t10;
220
229
  } else {
221
- t10 = $[48];
230
+ t10 = $[49];
222
231
  }
223
232
  let t11;
224
- if ($[49] !== denyReason || $[50] !== draft || $[51] !== item || $[52] !== locked || $[53] !== onDraft) {
233
+ if ($[50] !== denyReason || $[51] !== draft || $[52] !== item || $[53] !== locked || $[54] !== onDraft) {
225
234
  t11 = /*#__PURE__*/_jsx(ApprovalBody, {
226
235
  item: item,
227
236
  draft: draft,
@@ -229,17 +238,17 @@ export function PromptView(props) {
229
238
  denyReason: denyReason,
230
239
  locked: locked
231
240
  });
232
- $[49] = denyReason;
233
- $[50] = draft;
234
- $[51] = item;
235
- $[52] = locked;
236
- $[53] = onDraft;
237
- $[54] = t11;
241
+ $[50] = denyReason;
242
+ $[51] = draft;
243
+ $[52] = item;
244
+ $[53] = locked;
245
+ $[54] = onDraft;
246
+ $[55] = t11;
238
247
  } else {
239
- t11 = $[54];
248
+ t11 = $[55];
240
249
  }
241
250
  let t12;
242
- if ($[55] !== error || $[56] !== meta || $[57] !== notice || $[58] !== sending || $[59] !== t10 || $[60] !== t11 || $[61] !== t8 || $[62] !== top) {
251
+ if ($[56] !== error || $[57] !== meta || $[58] !== notice || $[59] !== sending || $[60] !== t10 || $[61] !== t11 || $[62] !== t8 || $[63] !== top) {
243
252
  t12 = /*#__PURE__*/_jsx(PromptCard, {
244
253
  kind: "approval",
245
254
  heading: t8,
@@ -251,17 +260,17 @@ export function PromptView(props) {
251
260
  actions: t10,
252
261
  children: t11
253
262
  });
254
- $[55] = error;
255
- $[56] = meta;
256
- $[57] = notice;
257
- $[58] = sending;
258
- $[59] = t10;
259
- $[60] = t11;
260
- $[61] = t8;
261
- $[62] = top;
262
- $[63] = t12;
263
+ $[56] = error;
264
+ $[57] = meta;
265
+ $[58] = notice;
266
+ $[59] = sending;
267
+ $[60] = t10;
268
+ $[61] = t11;
269
+ $[62] = t8;
270
+ $[63] = top;
271
+ $[64] = t12;
263
272
  } else {
264
- t12 = $[63];
273
+ t12 = $[64];
265
274
  }
266
275
  return t12;
267
276
  }
@@ -270,38 +279,38 @@ export function PromptView(props) {
270
279
  const t8 = item.async === true;
271
280
  const t9 = readyWith(draft);
272
281
  let t10;
273
- if ($[64] !== draft || $[65] !== onDraft) {
282
+ if ($[65] !== draft || $[66] !== onDraft) {
274
283
  t10 = () => onDraft({
275
284
  ...draft,
276
285
  index: draft.index - 1
277
286
  });
278
- $[64] = draft;
279
- $[65] = onDraft;
280
- $[66] = t10;
287
+ $[65] = draft;
288
+ $[66] = onDraft;
289
+ $[67] = t10;
281
290
  } else {
282
- t10 = $[66];
291
+ t10 = $[67];
283
292
  }
284
293
  let t11;
285
- if ($[67] !== onAction) {
294
+ if ($[68] !== onAction) {
286
295
  t11 = () => onAction({
287
296
  kind: "dismiss"
288
297
  });
289
- $[67] = onAction;
290
- $[68] = t11;
298
+ $[68] = onAction;
299
+ $[69] = t11;
291
300
  } else {
292
- t11 = $[68];
301
+ t11 = $[69];
293
302
  }
294
303
  let t12;
295
- if ($[69] !== commit || $[70] !== draft) {
304
+ if ($[70] !== commit || $[71] !== draft) {
296
305
  t12 = () => commit(draft);
297
- $[69] = commit;
298
- $[70] = draft;
299
- $[71] = t12;
306
+ $[70] = commit;
307
+ $[71] = draft;
308
+ $[72] = t12;
300
309
  } else {
301
- t12 = $[71];
310
+ t12 = $[72];
302
311
  }
303
312
  let t13;
304
- if ($[72] !== draft.index || $[73] !== last || $[74] !== locked || $[75] !== sending || $[76] !== t10 || $[77] !== t11 || $[78] !== t12 || $[79] !== t8 || $[80] !== t9) {
313
+ if ($[73] !== draft.index || $[74] !== last || $[75] !== locked || $[76] !== sending || $[77] !== t10 || $[78] !== t11 || $[79] !== t12 || $[80] !== t8 || $[81] !== t9) {
305
314
  t13 = /*#__PURE__*/_jsx(QuestionActions, {
306
315
  index: t7,
307
316
  dismissable: t8,
@@ -313,21 +322,21 @@ export function PromptView(props) {
313
322
  onDismiss: t11,
314
323
  onCommit: t12
315
324
  });
316
- $[72] = draft.index;
317
- $[73] = last;
318
- $[74] = locked;
319
- $[75] = sending;
320
- $[76] = t10;
321
- $[77] = t11;
322
- $[78] = t12;
323
- $[79] = t8;
324
- $[80] = t9;
325
- $[81] = t13;
325
+ $[73] = draft.index;
326
+ $[74] = last;
327
+ $[75] = locked;
328
+ $[76] = sending;
329
+ $[77] = t10;
330
+ $[78] = t11;
331
+ $[79] = t12;
332
+ $[80] = t8;
333
+ $[81] = t9;
334
+ $[82] = t13;
326
335
  } else {
327
- t13 = $[81];
336
+ t13 = $[82];
328
337
  }
329
338
  let t14;
330
- if ($[82] !== answer || $[83] !== commit || $[84] !== draft || $[85] !== locked || $[86] !== onDraft || $[87] !== question) {
339
+ if ($[83] !== answer || $[84] !== commit || $[85] !== draft || $[86] !== locked || $[87] !== onDraft || $[88] !== question) {
331
340
  t14 = question && answer && /*#__PURE__*/_jsx(QuestionBody, {
332
341
  question: question,
333
342
  answer: answer,
@@ -347,18 +356,18 @@ export function PromptView(props) {
347
356
  }),
348
357
  locked: locked
349
358
  });
350
- $[82] = answer;
351
- $[83] = commit;
352
- $[84] = draft;
353
- $[85] = locked;
354
- $[86] = onDraft;
355
- $[87] = question;
356
- $[88] = t14;
359
+ $[83] = answer;
360
+ $[84] = commit;
361
+ $[85] = draft;
362
+ $[86] = locked;
363
+ $[87] = onDraft;
364
+ $[88] = question;
365
+ $[89] = t14;
357
366
  } else {
358
- t14 = $[88];
367
+ t14 = $[89];
359
368
  }
360
369
  let t15;
361
- if ($[89] !== error || $[90] !== meta || $[91] !== notice || $[92] !== sending || $[93] !== t13 || $[94] !== t14 || $[95] !== t6 || $[96] !== top) {
370
+ if ($[90] !== error || $[91] !== meta || $[92] !== notice || $[93] !== sending || $[94] !== t13 || $[95] !== t14 || $[96] !== t6 || $[97] !== top) {
362
371
  t15 = /*#__PURE__*/_jsx(PromptCard, {
363
372
  kind: "question",
364
373
  heading: t6,
@@ -370,17 +379,17 @@ export function PromptView(props) {
370
379
  actions: t13,
371
380
  children: t14
372
381
  });
373
- $[89] = error;
374
- $[90] = meta;
375
- $[91] = notice;
376
- $[92] = sending;
377
- $[93] = t13;
378
- $[94] = t14;
379
- $[95] = t6;
380
- $[96] = top;
381
- $[97] = t15;
382
+ $[90] = error;
383
+ $[91] = meta;
384
+ $[92] = notice;
385
+ $[93] = sending;
386
+ $[94] = t13;
387
+ $[95] = t14;
388
+ $[96] = t6;
389
+ $[97] = top;
390
+ $[98] = t15;
382
391
  } else {
383
- t15 = $[97];
392
+ t15 = $[98];
384
393
  }
385
394
  return t15;
386
395
  }
@@ -10,7 +10,8 @@ const CLI_COLORS = {
10
10
  gemini: 'text-chart-gemini',
11
11
  copilot: 'text-chart-copilot',
12
12
  nanogpt: 'text-chart-nanogpt',
13
- grok: 'text-chart-grok'
13
+ grok: 'text-chart-grok',
14
+ 'anthropic-claude-api': 'text-chart-claude-api'
14
15
  };
15
16
  export function CliMark(t0) {
16
17
  const $ = _c(7);
@@ -1,6 +1,7 @@
1
1
  import type { AgentKind, ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem, ChatSubagentItem, ChatVisual } from '@adecore/agent-contracts';
2
2
  export interface ChatState {
3
3
  info: ChatInfo;
4
+ historyPending?: boolean;
4
5
  items: Record<string, ChatItem>;
5
6
  structure: Record<string, ChatItem>;
6
7
  order: string[];
@@ -19,9 +20,11 @@ export interface ChatPage {
19
20
  export type ChatsById = Record<string, ChatState>;
20
21
  export type ChatStatuses = Record<string, Pick<ChatState, 'info'>>;
21
22
  export interface ChatSink {
23
+ historyIdentity?(chatId: string): object | undefined;
22
24
  reset(chatId: string, info: ChatInfo, items: ChatItem[], page?: ChatPage): void;
23
25
  prepend(chatId: string, cursor: string, page: ChatHistoryResult): void;
24
26
  apply(chatId: string, event: ChatEvent): void;
27
+ replay?(chatId: string, events: readonly ChatEvent[]): void;
25
28
  status(chatId: string, info: ChatInfo): void;
26
29
  bookmarks(chatId: string, bookmarks: ChatBookmark[]): void;
27
30
  visuals(chatId: string, visuals: ChatVisual[]): void;
@@ -33,6 +36,7 @@ interface ChatsStore {
33
36
  reset(key: string, info: ChatInfo, items: ChatItem[], page?: ChatPage): void;
34
37
  prepend(key: string, cursor: string, page: ChatHistoryResult): void;
35
38
  apply(key: string, event: ChatEvent): void;
39
+ replay(key: string, events: readonly ChatEvent[]): void;
36
40
  status(key: string, info: ChatInfo): void;
37
41
  bookmarks(key: string, bookmarks: ChatBookmark[]): void;
38
42
  visuals(key: string, visuals: ChatVisual[]): void;
@@ -37,11 +37,11 @@ function isBeforePage(state, historyIndex) {
37
37
  return state.history !== undefined && historyIndex !== undefined && historyIndex < state.history.start;
38
38
  }
39
39
  /* An item from before the page this client holds matters only as a sub-agent, a request that waits, or one that stopped waiting. */
40
- function withEarlierItem(state, item) {
40
+ function withEarlierItem(state, item, write) {
41
41
  if (item.kind === 'subagent') {
42
42
  const subagents = state.subagentsBefore ?? [];
43
43
  return {
44
- ...withItem(state, item),
44
+ ...write(state, item),
45
45
  subagentsBefore: subagents.includes(item.id) ? subagents : [...subagents, item.id]
46
46
  };
47
47
  }
@@ -49,12 +49,12 @@ function withEarlierItem(state, item) {
49
49
  const held = waiting.includes(item.id);
50
50
  if (isWaiting(item)) {
51
51
  return {
52
- ...withItem(state, item),
52
+ ...write(state, item),
53
53
  waitingBefore: held ? waiting : [...waiting, item.id]
54
54
  };
55
55
  }
56
56
  return held ? {
57
- ...withItem(state, item),
57
+ ...write(state, item),
58
58
  waitingBefore: waiting.filter(id => id !== item.id)
59
59
  } : state;
60
60
  }
@@ -63,6 +63,10 @@ export function prependPage(state, cursor, page) {
63
63
  if (state.history?.cursor !== cursor) {
64
64
  return state;
65
65
  }
66
+ const {
67
+ historyPending: _historyPending,
68
+ ...loaded
69
+ } = state;
66
70
  const held = new Set(state.order);
67
71
  const older = page.items.filter(item => !held.has(item.id));
68
72
  const olderIds = new Set(older.map(item => item.id));
@@ -80,7 +84,7 @@ export function prependPage(state, cursor, page) {
80
84
  waitingBefore: _waitingBefore,
81
85
  subagentsBefore: _subagentsBefore,
82
86
  ...rest
83
- } = state;
87
+ } = loaded;
84
88
  return {
85
89
  ...rest,
86
90
  items,
@@ -91,7 +95,7 @@ export function prependPage(state, cursor, page) {
91
95
  const waitingBefore = (state.waitingBefore ?? []).filter(id => !olderIds.has(id));
92
96
  const subagentsBefore = (state.subagentsBefore ?? []).filter(id => !olderIds.has(id));
93
97
  return {
94
- ...state,
98
+ ...loaded,
95
99
  items,
96
100
  structure: items,
97
101
  order,
@@ -104,16 +108,18 @@ export function prependPage(state, cursor, page) {
104
108
  export function waitingRequestsOf(chat) {
105
109
  return [...(chat?.waitingBefore ?? []), ...(chat?.order ?? [])].map(id => chat?.structure[id]).filter(isWaiting);
106
110
  }
107
- /* The state with one item replaced, in the thread and in the structure the rows come from. */
108
- function withItem(state, item) {
111
+ function withItem(state, item, structural = true) {
109
112
  const items = {
110
113
  ...state.items,
111
114
  [item.id]: item
112
115
  };
113
- return {
116
+ return structural ? {
114
117
  ...state,
115
118
  items,
116
119
  structure: items
120
+ } : {
121
+ ...state,
122
+ items
117
123
  };
118
124
  }
119
125
  /* Cheap enough, since a status only arrives when the host saw one change, never on a streamed word. */
@@ -133,15 +139,48 @@ function without(rows, matches) {
133
139
  return Object.fromEntries(Object.entries(rows).filter(([key]) => !matches(key)));
134
140
  }
135
141
  export function applyEvent(state, event) {
142
+ return applyUpdate(state, event, withItem);
143
+ }
144
+ function replayEvents(state, events) {
145
+ let draft;
146
+ const write = (current, item, structural = true) => {
147
+ // A text delta must leave the last structural snapshot intact, including within this replay.
148
+ if (draft !== current.items || !structural && draft === current.structure) {
149
+ draft = {
150
+ ...current.items
151
+ };
152
+ }
153
+ // Item ids may be __proto__, so this must define an own data property.
154
+ Object.defineProperty(draft, item.id, {
155
+ value: item,
156
+ enumerable: true,
157
+ configurable: true,
158
+ writable: true
159
+ });
160
+ return structural ? {
161
+ ...current,
162
+ items: draft,
163
+ structure: draft
164
+ } : {
165
+ ...current,
166
+ items: draft
167
+ };
168
+ };
169
+ for (const event of events) {
170
+ state = applyUpdate(state, event, write);
171
+ }
172
+ return state;
173
+ }
174
+ function applyUpdate(state, event, write) {
136
175
  switch (event.type) {
137
176
  case 'item':
138
177
  {
139
178
  if (isBeforePage(state, event.historyIndex)) {
140
- return withEarlierItem(state, event.item);
179
+ return withEarlierItem(state, event.item, write);
141
180
  }
142
181
  const known = event.item.id in state.items;
143
182
  return {
144
- ...withItem(state, event.item),
183
+ ...write(state, event.item),
145
184
  order: known ? state.order : [...state.order, event.item.id]
146
185
  };
147
186
  }
@@ -154,31 +193,19 @@ export function applyEvent(state, event) {
154
193
  uiQueries,
155
194
  ...text
156
195
  } = event;
157
- const grown = event.text === '' ? state : applyEvent(state, text);
196
+ const grown = event.text === '' ? state : applyUpdate(state, text, write);
158
197
  const reply = grown.items[event.itemId] ?? item;
159
- return {
160
- ...grown,
161
- items: {
162
- ...grown.items,
163
- [event.itemId]: {
164
- ...reply,
165
- uiQueries
166
- }
167
- }
168
- };
198
+ return write(grown, {
199
+ ...reply,
200
+ uiQueries
201
+ }, false);
169
202
  }
170
203
  if (event.ui !== undefined && event.text === '') {
171
204
  if (item?.kind === 'assistant' && item.streaming && event.textLength === item.text.length) {
172
- return {
173
- ...state,
174
- items: {
175
- ...state.items,
176
- [event.itemId]: {
177
- ...item,
178
- ui: event.ui
179
- }
180
- }
181
- };
205
+ return write(state, {
206
+ ...item,
207
+ ui: event.ui
208
+ }, false);
182
209
  }
183
210
  return state;
184
211
  }
@@ -189,15 +216,9 @@ export function applyEvent(state, event) {
189
216
  };
190
217
  // The first text is structure after all, since an empty reply that is not streaming has no row.
191
218
  if (item.text === '') {
192
- return withItem(state, grown);
219
+ return write(state, grown);
193
220
  }
194
- return {
195
- ...state,
196
- items: {
197
- ...state.items,
198
- [event.itemId]: grown
199
- }
200
- };
221
+ return write(state, grown, false);
201
222
  }
202
223
  if (item?.kind === 'tool' && item.state === 'running') {
203
224
  const progress = item.progress ?? {
@@ -212,13 +233,7 @@ export function applyEvent(state, event) {
212
233
  output: appendToolOutput(progress.output, event.text)
213
234
  }
214
235
  };
215
- return {
216
- ...state,
217
- items: {
218
- ...state.items,
219
- [event.itemId]: grown
220
- }
221
- };
236
+ return write(state, grown, false);
222
237
  }
223
238
  return state;
224
239
  }
@@ -306,6 +321,22 @@ export const useChats = create(set => ({
306
321
  };
307
322
  });
308
323
  },
324
+ replay(key, events) {
325
+ set(store => {
326
+ const current = store.byKey[key];
327
+ if (!current || events.length === 0) {
328
+ return store;
329
+ }
330
+ const next = replayEvents(current, events);
331
+ return next === current ? store : {
332
+ byKey: {
333
+ ...store.byKey,
334
+ [key]: next
335
+ },
336
+ statusByKey: withStatus(store.statusByKey, key, next.info)
337
+ };
338
+ });
339
+ },
309
340
  /*
310
341
  * The status of a chat this window is not reading, which the host sends for every chat it has
311
342
  * loaded. The thread stays empty until somebody attaches, since this is what a header, a list and
@@ -318,7 +349,10 @@ export const useChats = create(set => ({
318
349
  return {
319
350
  byKey: {
320
351
  ...s.byKey,
321
- [key]: stateOf(info, [])
352
+ [key]: {
353
+ ...stateOf(info, []),
354
+ historyPending: true
355
+ }
322
356
  },
323
357
  statusByKey: withStatus(s.statusByKey, key, info)
324
358
  };
@@ -375,9 +409,11 @@ export const useChats = create(set => ({
375
409
  /* The sink of one host's chat client: it hands over chat ids, this puts them under the keys of its scope. */
376
410
  export function chatSink(keyOf) {
377
411
  return {
412
+ historyIdentity: chatId => useChats.getState().byKey[keyOf(chatId)]?.order,
378
413
  reset: (chatId, info, items, page) => useChats.getState().reset(keyOf(chatId), info, items, page),
379
414
  prepend: (chatId, cursor, page) => useChats.getState().prepend(keyOf(chatId), cursor, page),
380
415
  apply: (chatId, event) => useChats.getState().apply(keyOf(chatId), event),
416
+ replay: (chatId, events) => useChats.getState().replay(keyOf(chatId), events),
381
417
  status: (chatId, info) => useChats.getState().status(keyOf(chatId), info),
382
418
  bookmarks: (chatId, bookmarks) => useChats.getState().bookmarks(keyOf(chatId), bookmarks),
383
419
  visuals: (chatId, visuals) => useChats.getState().visuals(keyOf(chatId), visuals),
package/dist/theme.css CHANGED
@@ -8,8 +8,10 @@
8
8
  --skill: #9333ea;
9
9
  --skill-soft: #f7eeff;
10
10
  /* One color per agent CLI, so a bar, a dot and a legend row all name the same provider. Apple's
11
- system orange and teal; Gemini and Copilot are reserved for the day their transcripts are read. */
11
+ system orange and teal, and purple for Claude through an API key so it reads apart from Claude Code;
12
+ Gemini and Copilot are reserved for the day their transcripts are read. */
12
13
  --chart-claude: #ff9500;
14
+ --chart-claude-api: #af52de;
13
15
  --chart-codex: #30b0c7;
14
16
  --chart-gemini: #3b82f6;
15
17
  --chart-copilot: #64748b;
@@ -37,6 +39,7 @@
37
39
  --skill: #c084fc;
38
40
  --skill-soft: #2f2350;
39
41
  --chart-claude: #ff9f0a;
42
+ --chart-claude-api: #bf5af2;
40
43
  --chart-codex: #40c8e0;
41
44
  --chart-gemini: #60a5fa;
42
45
  --chart-copilot: #94a3b8;
@@ -70,6 +73,7 @@
70
73
  --color-skill: var(--skill);
71
74
  --color-skill-soft: var(--skill-soft);
72
75
  --color-chart-claude: var(--chart-claude);
76
+ --color-chart-claude-api: var(--chart-claude-api);
73
77
  --color-chart-codex: var(--chart-codex);
74
78
  --color-chart-gemini: var(--chart-gemini);
75
79
  --color-chart-copilot: var(--chart-copilot);
@@ -110,7 +114,7 @@
110
114
  */
111
115
  .chat-column .chat-column-content { margin-inline: auto; max-width: var(--chat-measure, 768px); }
112
116
  /* Wide pages clear the scrubber even when the reply column needs no extra padding. */
113
- .chat-wide-content { max-width: calc(100% - 2 * var(--chat-wide-inset, 0px)); }
117
+ .chat-wide-content { max-width: min(calc(100% - 2 * var(--chat-wide-inset, 0px)), var(--chat-wide-measure, 100%)); }
114
118
  /* Wider by the input's inset (`.composer-input .cm-content` padding plus the card's border), so what is typed lines up with the thread. */
115
119
  .chat-column .chat-composer-content { margin-inline: auto; max-width: calc(var(--chat-measure, 768px) + 2 * 21px); }
116
120
 
@@ -25,14 +25,16 @@ export const PROVIDER_LABELS = {
25
25
  codex: 'Codex',
26
26
  nanogpt: 'NanoGPT',
27
27
  grok: 'Grok',
28
- copilot: 'GitHub Copilot'
28
+ copilot: 'GitHub Copilot',
29
+ 'anthropic-claude-api': 'Claude API'
29
30
  };
30
31
  export const PROVIDER_COLORS = {
31
32
  claude: 'var(--chart-claude)',
32
33
  codex: 'var(--chart-codex)',
33
34
  nanogpt: 'var(--chart-nanogpt)',
34
35
  grok: 'var(--chart-grok)',
35
- copilot: 'var(--chart-copilot)'
36
+ copilot: 'var(--chart-copilot)',
37
+ 'anthropic-claude-api': 'var(--chart-claude-api)'
36
38
  };
37
39
  /*
38
40
  * A slot is `2026-09-10` or `2026-09-10T14`, already in the viewer's own zone, so it is read as plain