@adecore/agents-react 0.21.0 → 0.21.2

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 (137) hide show
  1. package/dist/agents/status-look.js +2 -3
  2. package/dist/chat/chat-client.d.ts +1 -1
  3. package/dist/chat/chat-client.js +2 -2
  4. package/dist/chat/drafts.js +6 -12
  5. package/dist/chat/logic/ansi.js +5 -1
  6. package/dist/chat/logic/fork.js +8 -9
  7. package/dist/chat/logic/limit.js +17 -14
  8. package/dist/chat/logic/resume-compaction.js +2 -11
  9. package/dist/chat/logic/scrubber.js +9 -18
  10. package/dist/chat/logic/timeline.js +15 -10
  11. package/dist/chat/logic/tool-catalog.js +3 -4
  12. package/dist/chat/mentions.js +5 -6
  13. package/dist/chat/runtime-modes.js +2 -4
  14. package/dist/chat/stash.js +1 -3
  15. package/dist/chat/thumbnails.js +3 -4
  16. package/dist/chat/timeline-scroll.js +39 -45
  17. package/dist/chat/ui/BookmarkSubmenu.js +1 -5
  18. package/dist/chat/ui/CodeBlock.js +2 -3
  19. package/dist/chat/ui/Composer.js +57 -69
  20. package/dist/chat/ui/Markdown.js +11 -11
  21. package/dist/chat/ui/Pickers.js +5 -9
  22. package/dist/chat/ui/QuoteButton.js +2 -3
  23. package/dist/chat/ui/RunSettings.js +8 -2
  24. package/dist/chat/ui/Scrubber.js +2 -3
  25. package/dist/chat/ui/SubagentControls.js +1 -3
  26. package/dist/chat/ui/Timeline.js +15 -18
  27. package/dist/chat/ui/TimelineMenu.js +3 -9
  28. package/dist/chat/ui/UnifiedDiff.js +3 -5
  29. package/dist/chat/ui/VisualFrame.js +3 -4
  30. package/dist/chat/ui/composer/keys.js +19 -11
  31. package/dist/chat/ui/intelligent-ui/UiBlockFrame.d.ts +14 -1
  32. package/dist/chat/ui/intelligent-ui/UiBlockFrame.js +47 -5
  33. package/dist/chat/ui/intelligent-ui/UiReply.d.ts +3 -1
  34. package/dist/chat/ui/intelligent-ui/UiReply.js +34 -30
  35. package/dist/chat/ui/intelligent-ui/block-local.d.ts +9 -0
  36. package/dist/chat/ui/intelligent-ui/block-local.js +30 -1
  37. package/dist/chat/ui/intelligent-ui/node-text.d.ts +6 -0
  38. package/dist/chat/ui/intelligent-ui/node-text.js +54 -0
  39. package/dist/chat/ui/intelligent-ui/registry.js +2 -1
  40. package/dist/chat/ui/intelligent-ui/render-context.d.ts +4 -1
  41. package/dist/chat/ui/intelligent-ui/renderers/chart.js +5 -7
  42. package/dist/chat/ui/intelligent-ui/renderers/choices.js +6 -2
  43. package/dist/chat/ui/intelligent-ui/renderers/content.js +6 -13
  44. package/dist/chat/ui/intelligent-ui/renderers/data.js +8 -15
  45. package/dist/chat/ui/intelligent-ui/renderers/inputs.d.ts +1 -0
  46. package/dist/chat/ui/intelligent-ui/renderers/inputs.js +16 -4
  47. package/dist/chat/ui/intelligent-ui/renderers/links.js +2 -1
  48. package/dist/chat/ui/intelligent-ui/use-ui-links.d.ts +4 -4
  49. package/dist/chat/ui/intelligent-ui/use-ui-links.js +29 -10
  50. package/dist/chat/ui/intelligent-ui/use-ui-queries.d.ts +2 -1
  51. package/dist/chat/ui/intelligent-ui/use-ui-queries.js +34 -21
  52. package/dist/chat/ui/rows/MessageRows.js +3 -4
  53. package/dist/chat/ui/rows/WorkRows.d.ts +4 -3
  54. package/dist/chat/ui/rows/WorkRows.js +19 -14
  55. package/dist/chat/visual-viewport.d.ts +1 -1
  56. package/dist/chat/visual-viewport.js +9 -8
  57. package/dist/locales/en/agent-chat.json +23 -4
  58. package/dist/locales/nl/agent-chat.json +23 -4
  59. package/dist/port-transport.js +4 -8
  60. package/dist/state/chats.js +15 -15
  61. package/dist/state/usage.js +44 -49
  62. package/dist/storage.js +7 -7
  63. package/dist/theme.css +6 -14
  64. package/dist/usage/LimitsList.js +1 -2
  65. package/dist/usage/UsageBreakdown.js +1 -3
  66. package/dist/usage/UsageDialog.js +2 -4
  67. package/dist/usage/format.js +6 -6
  68. package/dist/usage/money.js +1 -5
  69. package/dist/usage/summary.js +2 -4
  70. package/package.json +24 -29
  71. package/src/agents/status-look.ts +2 -3
  72. package/src/chat/chat-client.ts +2 -3
  73. package/src/chat/drafts.ts +6 -14
  74. package/src/chat/logic/ansi.ts +5 -1
  75. package/src/chat/logic/fork.ts +8 -9
  76. package/src/chat/logic/limit.ts +17 -16
  77. package/src/chat/logic/resume-compaction.ts +2 -11
  78. package/src/chat/logic/scrubber.ts +13 -17
  79. package/src/chat/logic/timeline.ts +22 -11
  80. package/src/chat/logic/tool-catalog.ts +7 -6
  81. package/src/chat/mentions.ts +5 -6
  82. package/src/chat/runtime-modes.ts +2 -4
  83. package/src/chat/stash.ts +1 -3
  84. package/src/chat/thumbnails.ts +3 -4
  85. package/src/chat/timeline-scroll.ts +43 -52
  86. package/src/chat/ui/BookmarkSubmenu.tsx +1 -5
  87. package/src/chat/ui/CodeBlock.tsx +2 -3
  88. package/src/chat/ui/Composer.tsx +66 -86
  89. package/src/chat/ui/Markdown.tsx +13 -17
  90. package/src/chat/ui/Pickers.tsx +5 -9
  91. package/src/chat/ui/QuoteButton.tsx +2 -3
  92. package/src/chat/ui/RunSettings.tsx +9 -2
  93. package/src/chat/ui/Scrubber.tsx +2 -3
  94. package/src/chat/ui/SubagentControls.tsx +1 -3
  95. package/src/chat/ui/SubagentTimeline.tsx +1 -2
  96. package/src/chat/ui/Timeline.tsx +18 -20
  97. package/src/chat/ui/TimelineMenu.tsx +3 -9
  98. package/src/chat/ui/UnifiedDiff.tsx +3 -5
  99. package/src/chat/ui/VisualFrame.tsx +3 -4
  100. package/src/chat/ui/composer/keys.ts +21 -11
  101. package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +135 -5
  102. package/src/chat/ui/intelligent-ui/UiReply.tsx +48 -38
  103. package/src/chat/ui/intelligent-ui/block-local.ts +36 -1
  104. package/src/chat/ui/intelligent-ui/node-text.ts +60 -0
  105. package/src/chat/ui/intelligent-ui/registry.ts +2 -1
  106. package/src/chat/ui/intelligent-ui/render-context.ts +8 -2
  107. package/src/chat/ui/intelligent-ui/renderers/chart.tsx +5 -7
  108. package/src/chat/ui/intelligent-ui/renderers/choices.tsx +6 -2
  109. package/src/chat/ui/intelligent-ui/renderers/content.tsx +9 -14
  110. package/src/chat/ui/intelligent-ui/renderers/data.tsx +13 -19
  111. package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +28 -5
  112. package/src/chat/ui/intelligent-ui/renderers/links.tsx +2 -1
  113. package/src/chat/ui/intelligent-ui/use-ui-links.ts +31 -11
  114. package/src/chat/ui/intelligent-ui/use-ui-queries.ts +48 -22
  115. package/src/chat/ui/rehype-chips.ts +1 -1
  116. package/src/chat/ui/rehype-fade.ts +1 -1
  117. package/src/chat/ui/remark-html-as-text.ts +1 -1
  118. package/src/chat/ui/rows/MessageRows.tsx +4 -5
  119. package/src/chat/ui/rows/WorkRows.tsx +86 -69
  120. package/src/chat/visual-viewport.ts +10 -8
  121. package/src/host.ts +1 -3
  122. package/src/locales/en/agent-chat.json +23 -4
  123. package/src/locales/nl/agent-chat.json +23 -4
  124. package/src/port-transport.ts +4 -8
  125. package/src/state/chats.ts +16 -15
  126. package/src/state/usage.ts +49 -60
  127. package/src/storage.ts +7 -7
  128. package/src/theme.css +6 -14
  129. package/src/usage/LimitsList.tsx +2 -4
  130. package/src/usage/UsageBreakdown.tsx +2 -5
  131. package/src/usage/UsageDialog.tsx +2 -4
  132. package/src/usage/format.ts +6 -6
  133. package/src/usage/money.ts +1 -5
  134. package/src/usage/summary.ts +2 -4
  135. package/dist/chat/ui/intelligent-ui/UiBlockFooter.d.ts +0 -15
  136. package/dist/chat/ui/intelligent-ui/UiBlockFooter.js +0 -50
  137. package/src/chat/ui/intelligent-ui/UiBlockFooter.tsx +0 -139
@@ -28,14 +28,6 @@ const storage = persistedJson<Record<string, DraftRecord>>(
28
28
  () => chatStorageLegacyKeys('drafts')
29
29
  );
30
30
 
31
- function readAll(): Record<string, DraftRecord> {
32
- return storage.read();
33
- }
34
-
35
- function store(drafts: Record<string, DraftRecord>): boolean {
36
- return storage.write(drafts);
37
- }
38
-
39
31
  export function isEmptyDraft(draft: ChatDraft): boolean {
40
32
  return draft.text.trim() === '' && draft.attachments.length === 0 && draft.quote === '';
41
33
  }
@@ -46,7 +38,7 @@ export function isEmptyDraft(draft: ChatDraft): boolean {
46
38
  */
47
39
  export const useDrafts = createChatStore<{ ids: string[] }>(
48
40
  () => ({ ids: [] }),
49
- (store) => store.setState({ ids: Object.keys(readAll()) })
41
+ (store) => store.setState({ ids: Object.keys(storage.read()) })
50
42
  );
51
43
 
52
44
  export function useHasDraft(chatId: string): boolean {
@@ -63,7 +55,7 @@ function trackDraft(chatId: string, held: boolean): void {
63
55
 
64
56
  /* An unsent prompt per chat node, kept across reloads so a half-written message is never lost. */
65
57
  export function readDraft(chatId: string): ChatDraft {
66
- const record = readAll()[chatId];
58
+ const record = storage.read()[chatId];
67
59
  return record
68
60
  ? {
69
61
  text: record.text,
@@ -77,11 +69,11 @@ export function readDraft(chatId: string): ChatDraft {
77
69
  }
78
70
 
79
71
  export function writeDraft(chatId: string, draft: ChatDraft): void {
80
- const drafts = readAll();
72
+ const drafts = storage.read();
81
73
  trackDraft(chatId, !isEmptyDraft(draft));
82
74
  if (isEmptyDraft(draft)) {
83
75
  delete drafts[chatId];
84
- store(drafts);
76
+ storage.write(drafts);
85
77
  return;
86
78
  }
87
79
  drafts[chatId] = {
@@ -92,12 +84,12 @@ export function writeDraft(chatId: string, draft: ChatDraft): void {
92
84
  attachments: draft.attachments,
93
85
  quote: draft.quote
94
86
  };
95
- if (store(drafts)) {
87
+ if (storage.write(drafts)) {
96
88
  return;
97
89
  }
98
90
  // A file can outgrow the storage quota; the text is the part worth keeping then.
99
91
  drafts[chatId] = { text: draft.text, mentions: draft.mentions, skills: draft.skills, chats: draft.chats, quote: draft.quote };
100
- store(drafts);
92
+ storage.write(drafts);
101
93
  }
102
94
 
103
95
  /* Text handed to a draft from outside the composer goes under what was already typed, never over it. */
@@ -164,7 +164,7 @@ function styleOf(state: AnsiState): CSSProperties {
164
164
  return style;
165
165
  }
166
166
 
167
- export function parseAnsi(text: string, options: { limit?: number; tailLines?: number } = {}): { tokens: AnsiToken[]; omitted: number } {
167
+ function styledTokens(text: string): AnsiToken[] {
168
168
  const tokens: AnsiToken[] = [];
169
169
  let state: AnsiState = {};
170
170
  let position = 0;
@@ -180,7 +180,11 @@ export function parseAnsi(text: string, options: { limit?: number; tailLines?: n
180
180
  if (position < text.length) {
181
181
  tokens.push({ content: text.slice(position), style: styleOf(state) });
182
182
  }
183
+ return tokens;
184
+ }
183
185
 
186
+ export function parseAnsi(text: string, options: { limit?: number; tailLines?: number } = {}): { tokens: AnsiToken[]; omitted: number } {
187
+ const tokens = styledTokens(text);
184
188
  const plain = tokens.map((token) => token.content).join('');
185
189
  let start = 0;
186
190
  let end = plain.length;
@@ -172,20 +172,19 @@ export function forkPayload(input: {
172
172
  worktree?: ForkWorktreeChoice | null;
173
173
  cli?: { original: ForkCliChoice; chosen: ForkCliChoice };
174
174
  }): ChatForkPayload {
175
+ const { cli, worktree } = input;
176
+ const provider = cli !== undefined && cli.chosen.provider !== cli.original.provider ? cli.chosen.provider : null;
177
+ const selection = cli !== undefined && (provider !== null || cli.chosen.selection.model !== cli.original.selection.model) ? cli.chosen.selection : null;
178
+ const account = cli?.chosen.account !== undefined && (provider !== null || cli.chosen.account !== cli.original.account) ? cli.chosen.account : null;
175
179
  return {
176
180
  chatId: input.chatId,
177
181
  turnId: input.turnId,
178
182
  title: input.title,
179
183
  ...(input.shape === 'view' ? { asView: true } : {}),
180
- ...(input.cli && input.cli.chosen.provider !== input.cli.original.provider ? { provider: input.cli.chosen.provider } : {}),
181
- ...(input.cli && (input.cli.chosen.provider !== input.cli.original.provider || input.cli.chosen.selection.model !== input.cli.original.selection.model)
182
- ? { selection: input.cli.chosen.selection }
183
- : {}),
184
- ...(input.cli?.chosen.account !== undefined &&
185
- (input.cli.chosen.provider !== input.cli.original.provider || input.cli.chosen.account !== input.cli.original.account)
186
- ? { account: input.cli.chosen.account }
187
- : {}),
188
- ...(input.worktree ? { worktree: { branch: input.worktree.branch }, ...(input.worktree.filesAfterTurn ? { filesAfterTurn: true } : {}) } : {})
184
+ ...(provider === null ? {} : { provider }),
185
+ ...(selection === null ? {} : { selection }),
186
+ ...(account === null ? {} : { account }),
187
+ ...(worktree ? { worktree: { branch: worktree.branch }, ...(worktree.filesAfterTurn ? { filesAfterTurn: true } : {}) } : {})
189
188
  };
190
189
  }
191
190
 
@@ -27,20 +27,21 @@ export function limitView(info: Pick<ChatInfo, 'limit' | 'resumeAt' | 'activeTur
27
27
  detail: resumes === null ? i18next.t('agent-chat:limit.overload.sendAgain') : i18next.t('agent-chat:limit.overload.retries', { time: resumes })
28
28
  };
29
29
  }
30
- const resets = limit.resetsAt === undefined ? null : formatMoment(limit.resetsAt, now);
31
- return {
32
- pill:
33
- resumes !== null
34
- ? i18next.t('agent-chat:limit.usage.pillResumes', { time: resumes })
35
- : resets !== null
36
- ? i18next.t('agent-chat:limit.usage.pillUntil', { time: resets })
37
- : i18next.t('agent-chat:limit.usage.pill'),
38
- title: i18next.t('agent-chat:limit.usage.title'),
39
- detail:
40
- resumes !== null
41
- ? i18next.t('agent-chat:limit.usage.resumes', { time: resumes })
42
- : resets !== null
43
- ? i18next.t('agent-chat:limit.usage.resets', { time: resets })
44
- : null
45
- };
30
+ const title = i18next.t('agent-chat:limit.usage.title');
31
+ if (resumes !== null) {
32
+ return {
33
+ pill: i18next.t('agent-chat:limit.usage.pillResumes', { time: resumes }),
34
+ title,
35
+ detail: i18next.t('agent-chat:limit.usage.resumes', { time: resumes })
36
+ };
37
+ }
38
+ if (limit.resetsAt !== undefined) {
39
+ const resets = formatMoment(limit.resetsAt, now);
40
+ return {
41
+ pill: i18next.t('agent-chat:limit.usage.pillUntil', { time: resets }),
42
+ title,
43
+ detail: i18next.t('agent-chat:limit.usage.resets', { time: resets })
44
+ };
45
+ }
46
+ return { pill: i18next.t('agent-chat:limit.usage.pill'), title, detail: null };
46
47
  }
@@ -16,19 +16,10 @@ export interface ResumeCompactionOffer {
16
16
  }
17
17
 
18
18
  function lastTurn(items: readonly ChatItem[]): Extract<ChatItem, { kind: 'turn' }> | null {
19
- for (let i = items.length - 1; i >= 0; i--) {
20
- const item = items[i]!;
21
- if (item.kind === 'turn') {
22
- return item;
23
- }
24
- }
25
- return null;
19
+ return items.findLast((item): item is Extract<ChatItem, { kind: 'turn' }> => item.kind === 'turn') ?? null;
26
20
  }
27
21
 
28
- /*
29
- * Whether to offer compacting before the next message. The caller passes `now`, so this stays pure
30
- * and the test needs no clock.
31
- */
22
+ /* Whether to offer compacting before the next message. */
32
23
  export function resumeCompactionOffer({
33
24
  info,
34
25
  compaction,
@@ -127,19 +127,10 @@ export function tickOfRow(ticks: readonly ScrubberTick[], rowIndex: number): num
127
127
  if (ticks.length === 0) {
128
128
  return null;
129
129
  }
130
- let low = 0;
131
- let high = ticks.length - 1;
132
- let found = 0;
133
- while (low <= high) {
134
- const middle = (low + high) >> 1;
135
- if (ticks[middle]!.rowIndex <= rowIndex) {
136
- found = middle;
137
- low = middle + 1;
138
- } else {
139
- high = middle - 1;
140
- }
141
- }
142
- return found;
130
+ return Math.max(
131
+ 0,
132
+ lastIndexAtOrBelow(ticks.length, (index) => ticks[index]!.rowIndex, rowIndex)
133
+ );
143
134
  }
144
135
 
145
136
  /* Per tick, whether any of the rows has a find hit in its stretch. */
@@ -199,14 +190,14 @@ export function tickWidth(kind: TickKind, distanceFromPointer: number | null): n
199
190
  return kind === 'wake' ? WIDTH_WAKE_PX : WIDTH_PERSON_PX;
200
191
  }
201
192
 
202
- /* The last index whose start lies at or above a line, or -1. The starts only grow. */
203
- function lastAtOrAbove(starts: readonly number[], line: number): number {
193
+ /* The last index whose value is at most `limit`, or -1. The values only grow. */
194
+ function lastIndexAtOrBelow(length: number, valueAt: (index: number) => number, limit: number): number {
204
195
  let low = 0;
205
- let high = starts.length - 1;
196
+ let high = length - 1;
206
197
  let found = -1;
207
198
  while (low <= high) {
208
199
  const middle = (low + high) >> 1;
209
- if (starts[middle]! <= line) {
200
+ if (valueAt(middle) <= limit) {
210
201
  found = middle;
211
202
  low = middle + 1;
212
203
  } else {
@@ -216,6 +207,11 @@ function lastAtOrAbove(starts: readonly number[], line: number): number {
216
207
  return found;
217
208
  }
218
209
 
210
+ /* The last index whose start lies at or above a line, or -1. */
211
+ function lastAtOrAbove(starts: readonly number[], line: number): number {
212
+ return lastIndexAtOrBelow(starts.length, (index) => starts[index]!, line);
213
+ }
214
+
219
215
  export interface ReadingPosition {
220
216
  /* Where each message's row starts in the thread, in the virtualizer's measurements. */
221
217
  starts: readonly number[];
@@ -88,10 +88,9 @@ export function summarizeGroup(tools: ChatToolItem[]): string {
88
88
  if (only !== null && toolEntry(only)?.grouped === true) {
89
89
  return i18next.t(`agent-chat:group.tools.${only}`, { count: tools.length });
90
90
  }
91
- /* A run that only touches files says so once, whether it took one tool or three, and counts the
92
- files rather than the calls: three edits to one file are one file edited. */
91
+ // Counts the files rather than the calls: three edits to one file are one file edited.
93
92
  if (tools.every((tool) => isFileChange(tool.name))) {
94
- const paths = new Set(tools.map((tool) => (tool.input as { file_path?: string })?.file_path ?? tool.id));
93
+ const paths = new Set(tools.map(editedPath));
95
94
  return i18next.t('agent-chat:group.edited', { count: paths.size });
96
95
  }
97
96
  if (only !== null) {
@@ -100,6 +99,10 @@ export function summarizeGroup(tools: ChatToolItem[]): string {
100
99
  return i18next.t('agent-chat:group.toolCalls', { count: tools.length });
101
100
  }
102
101
 
102
+ function editedPath(tool: ChatToolItem): string {
103
+ return (tool.input as { file_path?: string })?.file_path ?? tool.id;
104
+ }
105
+
103
106
  /*
104
107
  * What a folded turn did, a sentence per kind of call in the order it first came: "Read 11 files",
105
108
  * "Searched 2 patterns". Every file change reads as one sentence, and calls without a sentence of
@@ -112,7 +115,7 @@ export function summarizeTurn(tools: readonly ChatToolItem[]): string[] {
112
115
  let other = 0;
113
116
  for (const tool of tools) {
114
117
  if (isFileChange(tool.name)) {
115
- edited.add((tool.input as { file_path?: string })?.file_path ?? tool.id);
118
+ edited.add(editedPath(tool));
116
119
  counts.set('agent-chat:group.edited', edited.size);
117
120
  } else if (toolEntry(tool.name)?.grouped === true) {
118
121
  const key = `agent-chat:group.tools.${tool.name}`;
@@ -345,12 +348,14 @@ function lastAssistantRow(rows: TimelineRow[]): TimelineRow | null {
345
348
  return null;
346
349
  }
347
350
 
348
- /* The whole thread as rows; items without a turn (older records) are shown as they are. */
349
- export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions): TimelineRow[] {
350
- const rows: TimelineRow[] = [];
351
- const children = groupChildren(items);
352
- // Items are grouped by turn, in order of first appearance; runs of turnless items keep their place.
353
- const chunks: Array<{ turnId: string | null; items: ChatItem[] }> = [];
351
+ interface TurnChunk {
352
+ turnId: string | null;
353
+ items: ChatItem[];
354
+ }
355
+
356
+ /* Items grouped by turn, in order of first appearance; runs of turnless items keep their place. */
357
+ function chunksByTurn(items: ChatItem[]): TurnChunk[] {
358
+ const chunks: TurnChunk[] = [];
354
359
  const byTurn = new Map<string, ChatItem[]>();
355
360
  for (const item of items) {
356
361
  if (item.turnId === null) {
@@ -370,8 +375,14 @@ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions):
370
375
  }
371
376
  bucket.push(item);
372
377
  }
378
+ return chunks;
379
+ }
373
380
 
374
- for (const chunk of chunks) {
381
+ /* The whole thread as rows; items without a turn (older records) are shown as they are. */
382
+ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions): TimelineRow[] {
383
+ const rows: TimelineRow[] = [];
384
+ const children = groupChildren(items);
385
+ for (const chunk of chunksByTurn(items)) {
375
386
  if (chunk.turnId === null) {
376
387
  rows.push(...rowsForItems(chunk.items, options, children));
377
388
  continue;
@@ -6,8 +6,10 @@ export interface ToolEntry {
6
6
  icon: LucideIcon;
7
7
  /* The input keys that hold the one line saying what the call is about, best first. */
8
8
  summary: readonly string[];
9
- /* Whether a run of calls of this name reads as a sentence of its own (`agent-chat:group.tools.<name>`).
10
- One that changes files without a sentence falls to `agent-chat:group.edited`, which counts files rather than calls. */
9
+ /*
10
+ * Whether a run of calls of this name reads as a sentence of its own (`agent-chat:group.tools.<name>`).
11
+ * One that changes files without a sentence falls to `agent-chat:group.edited`, which counts files.
12
+ */
11
13
  grouped: boolean;
12
14
  /* Whether the call writes to a file, which is what a mixed run of calls is named after. */
13
15
  changesFiles: boolean;
@@ -27,10 +29,9 @@ function entry(icon: LucideIcon, summary: readonly string[], rest: Partial<ToolE
27
29
  }
28
30
 
29
31
  /*
30
- * The icon, the summary line, the group sentence and the file-changing flag of every tool the thread
31
- * has a word for, in one table, because they used to sit in four and a name added to one of them was
32
- * missing from the rest. A name that is not here still draws, reads and groups; it falls back to the
33
- * wrench and to the first string of its input.
32
+ * Every tool the thread has a word for, in one table so a new name is never missing from a lookup. A
33
+ * name that is not here still draws, reads and groups; it falls back to the wrench and to the first
34
+ * string of its input.
34
35
  */
35
36
  export const TOOL_CATALOG: Record<string, ToolEntry> = {
36
37
  Read: entry(Eye, ['file_path'], { readsImage: true }),
@@ -1,5 +1,7 @@
1
- /* What a file dragged out of an app's own list of files carries: the paths, space separated, relative
2
- to the folder. The composer takes it next to the image files it already accepts. */
1
+ /*
2
+ * What a file dragged out of an app's own list of files carries: the paths, space separated, relative
3
+ * to the folder. The composer takes it next to the image files it already accepts.
4
+ */
3
5
  export const MENTION_DRAG_TYPE = 'application/x-adecore-mention';
4
6
  export const LEGACY_MENTION_DRAG_TYPE = 'application/x-ruimte-mention';
5
7
  export const MENTION_DRAG_TYPES = [MENTION_DRAG_TYPE, LEGACY_MENTION_DRAG_TYPE] as const;
@@ -145,10 +147,7 @@ export function presentSkills(text: string, chosen: string[]): string[] {
145
147
  return present(text, '$', chosen);
146
148
  }
147
149
 
148
- /*
149
- * The characters a segment stands for, sigil included. Joined, the segments spell the text they
150
- * were cut from.
151
- */
150
+ /* The characters a segment stands for, sigil included; joined, the segments spell the text they were cut from. */
152
151
  export function chipText(segment: ChipSegment): string {
153
152
  if (segment.kind === 'mention') {
154
153
  return `@${segment.path}`;
@@ -2,10 +2,8 @@ import i18next from 'i18next';
2
2
  import { RuntimeModeSchema, type RuntimeMode } from '@adecore/agent-contracts';
3
3
 
4
4
  /*
5
- * The runtime modes in the order they are offered, shared by the composer's mode picker, the
6
- * settings dialog and the agent menus. That order is the schema's, which the host reads as the
7
- * rank from narrowest to widest. Only the ids live here: the words are in `modes.<id>` of the `chat`
8
- * namespace, read when a surface draws one rather than when this module loads.
5
+ * The runtime modes in the order they are offered: the schema's, which the host reads as the rank
6
+ * from narrowest to widest. Their words are read when a surface draws one, not when this module loads.
9
7
  */
10
8
  export const RUNTIME_MODES: readonly RuntimeMode[] = RuntimeModeSchema.options;
11
9
 
package/src/chat/stash.ts CHANGED
@@ -94,9 +94,7 @@ export function stashedFrom(draft: ChatDraft, id: string, now: number): StashedP
94
94
 
95
95
  const storage = persistedJson<StashedPrompt[]>(storageKey, parseStash, [], () => chatStorageLegacyKeys('stash'));
96
96
 
97
- /*
98
- * Prompts put aside for later can move to another chat, so the shelf is shared across chats.
99
- */
97
+ /* Prompts put aside for later can move to another chat, so the shelf is shared across chats. */
100
98
  export const useStash = createChatStore<{ prompts: StashedPrompt[] }>(
101
99
  () => ({ prompts: [] }),
102
100
  (store) => store.setState({ prompts: storage.read() })
@@ -45,10 +45,9 @@ function dataUrlOf(blob: Blob): Promise<string> {
45
45
  }
46
46
 
47
47
  /*
48
- * A still of the middle of an image, decoded once. The composer used to draw the full data URL in a
49
- * 56 px box, which a browser decodes again for every paint, so a tall screenshot made typing stutter.
50
- * An animated image gives its first frame. Whatever cannot be decoded this way (an SVG, a browser
51
- * without `OffscreenCanvas`) falls back to the image itself, which is what it was before.
48
+ * A still of the middle of an image, decoded once: a browser decodes a full data URL again on every
49
+ * paint, so a tall screenshot made typing stutter. An animated image gives its first frame; what
50
+ * cannot be decoded this way (an SVG, a browser without `OffscreenCanvas`) falls back to the image.
52
51
  */
53
52
  async function makeThumbnail(upload: ChatAttachmentUpload): Promise<string> {
54
53
  try {
@@ -10,6 +10,10 @@ type Scroller = {
10
10
 
11
11
  const scrollers = new Map<string, Scroller>();
12
12
 
13
+ /* The timeline owns following; the composer subscribes here to show its jump-to-end button. */
14
+ const ends = new Map<string, boolean>();
15
+ const listeners = new Set<() => void>();
16
+
13
17
  /* The timeline hands its scroller over so the composer, which never sees it, can page through it. */
14
18
  export function registerTimeline(
15
19
  chatId: string,
@@ -29,10 +33,6 @@ export function registerTimeline(
29
33
  };
30
34
  }
31
35
 
32
- /* The timeline owns following; the composer subscribes here to show its jump-to-end button. */
33
- const ends = new Map<string, boolean>();
34
- const listeners = new Set<() => void>();
35
-
36
36
  export function setTimelineAtEnd(chatId: string, atEnd: boolean): void {
37
37
  if (ends.get(chatId) === atEnd) {
38
38
  return;
@@ -97,72 +97,63 @@ export function stepTimelineMessage(key: string, direction: -1 | 1): boolean {
97
97
  }
98
98
 
99
99
  /*
100
- * A jump to one message of a chat, asked from outside its thread (the chat's menu). A thread that is
101
- * not on screen yet gets it when it registers, since the menu may have to show the chat first.
102
- * Keyed with the scope's `keyOf`, like the steppers.
100
+ * Jumps asked from outside a thread (the chat's menu, a card that names a turn), keyed with the
101
+ * scope's `keyOf` like the steppers. A thread that is not on screen yet gets its jump when it
102
+ * registers, since the asker may have to show the chat first.
103
103
  */
104
- const jumpers = new Map<string, (itemId: string) => void>();
105
- const waitingJumps = new Map<string, string>();
106
-
107
- export function registerItemJumper(key: string, jump: (itemId: string) => void): () => void {
108
- jumpers.set(key, jump);
109
- const waiting = waitingJumps.get(key);
110
- if (waiting !== undefined) {
111
- waitingJumps.delete(key);
112
- jump(waiting);
113
- }
114
- return () => {
115
- if (jumpers.get(key) === jump) {
116
- jumpers.delete(key);
104
+ function waitingJumps<T>(): { register(key: string, jump: (target: T) => void): () => void; jump(key: string, target: T): boolean } {
105
+ const jumpers = new Map<string, (target: T) => void>();
106
+ const waiting = new Map<string, T>();
107
+ return {
108
+ register(key, jump) {
109
+ jumpers.set(key, jump);
110
+ if (waiting.has(key)) {
111
+ const target = waiting.get(key)!;
112
+ waiting.delete(key);
113
+ jump(target);
114
+ }
115
+ return () => {
116
+ if (jumpers.get(key) === jump) {
117
+ jumpers.delete(key);
118
+ }
119
+ };
120
+ },
121
+ jump(key, target) {
122
+ const jump = jumpers.get(key);
123
+ if (jump === undefined) {
124
+ waiting.set(key, target);
125
+ return false;
126
+ }
127
+ jump(target);
128
+ return true;
117
129
  }
118
130
  };
119
131
  }
120
132
 
133
+ const itemJumps = waitingJumps<string>();
134
+
135
+ export function registerItemJumper(key: string, jump: (itemId: string) => void): () => void {
136
+ return itemJumps.register(key, jump);
137
+ }
138
+
121
139
  /* False when that chat has no thread on screen; the jump then waits for the next one that registers. */
122
140
  export function jumpToTimelineItem(key: string, itemId: string): boolean {
123
- const jump = jumpers.get(key);
124
- if (jump === undefined) {
125
- waitingJumps.set(key, itemId);
126
- return false;
127
- }
128
- jump(itemId);
129
- return true;
141
+ return itemJumps.jump(key, itemId);
130
142
  }
131
143
 
132
- /*
133
- * A jump to the turn of a chat, for a card that says which turn wrote something: the message that opened
134
- * it, or the files it changed. Kept like the jump to an item, so a chat that is not on screen yet gets it
135
- * when its thread registers.
136
- */
144
+ /* Which part of a turn a jump lands on: the message that opened it, or the files it changed. */
137
145
  export type TurnTarget = 'prompt' | 'changes';
138
146
 
139
147
  type TurnJump = (turnId: string, target: TurnTarget) => void;
140
- const turnJumpers = new Map<string, TurnJump>();
141
- const waitingTurnJumps = new Map<string, { turnId: string; target: TurnTarget }>();
148
+ const turnJumps = waitingJumps<{ turnId: string; target: TurnTarget }>();
142
149
 
143
150
  export function registerTurnJumper(key: string, jump: TurnJump): () => void {
144
- turnJumpers.set(key, jump);
145
- const waiting = waitingTurnJumps.get(key);
146
- if (waiting !== undefined) {
147
- waitingTurnJumps.delete(key);
148
- jump(waiting.turnId, waiting.target);
149
- }
150
- return () => {
151
- if (turnJumpers.get(key) === jump) {
152
- turnJumpers.delete(key);
153
- }
154
- };
151
+ return turnJumps.register(key, ({ turnId, target }) => jump(turnId, target));
155
152
  }
156
153
 
157
154
  /* False when that chat has no thread on screen; the jump then waits for the next one that registers. */
158
155
  export function jumpToTimelineTurn(key: string, turnId: string, target: TurnTarget): boolean {
159
- const jump = turnJumpers.get(key);
160
- if (jump === undefined) {
161
- waitingTurnJumps.set(key, { turnId, target });
162
- return false;
163
- }
164
- jump(turnId, target);
165
- return true;
156
+ return turnJumps.jump(key, { turnId, target });
166
157
  }
167
158
 
168
159
  /* How near the top, in screens, the page before is asked for, so it is there before a reader reaches the edge. */
@@ -9,11 +9,7 @@ import { useChatScope } from '../../scope';
9
9
  import { useChatRow } from '../../state/chats';
10
10
  import { Icon, Menu } from '@adecore/ui';
11
11
 
12
- /*
13
- * "Bookmarks" in the menu of a chat node or a chat view: the chat's bookmarks in the order of the
14
- * thread, each a jump to its message, with rename and remove on the row. A chat without bookmarks
15
- * draws nothing.
16
- */
12
+ /* A chat's bookmarks in thread order, each a jump to its message with rename and remove; nothing without bookmarks. */
17
13
  export function BookmarkSubmenu({ chatId }: { chatId: string }) {
18
14
  const { t } = useTranslation('agent-chat');
19
15
  const scope = useChatScope();
@@ -123,9 +123,8 @@ const CodeLine = memo(function CodeLine({ tokens }: { tokens: CodeToken[] }) {
123
123
 
124
124
  /*
125
125
  * A fenced block, highlighted a line at a time. Until shiki and the grammar are there it holds its
126
- * place invisibly in the same shape, so there is never a frame of bare code that turns colored. A
127
- * fence that is still open is tokenized as it grows, and the same component carries on once it
128
- * closes, so closing it does not draw it again.
126
+ * place invisibly, so bare code never turns colored. An open fence is tokenized as it grows, and
127
+ * closing it does not draw it again.
129
128
  */
130
129
  export function CodeBlock({ code, lang, actions }: { code: string; lang: string; actions?: ReactNode }) {
131
130
  const { t } = useTranslation('agent-chat');