@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
@@ -1,6 +1,17 @@
1
+ import { useContext, useMemo } from 'react';
1
2
  import { create } from 'zustand';
2
- import type { ChatApprovalItem, ChatBookmark, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem, ChatVisual } from '@adecore/agent-contracts';
3
- import { useChatScope } from '../scope';
3
+ import type {
4
+ AgentKind,
5
+ ChatApprovalItem,
6
+ ChatBookmark,
7
+ ChatEvent,
8
+ ChatHistoryResult,
9
+ ChatInfo,
10
+ ChatItem,
11
+ ChatQuestionItem,
12
+ ChatVisual
13
+ } from '@adecore/agent-contracts';
14
+ import { ChatScopeContext, useChatScope } from '../scope';
4
15
 
5
16
  export interface ChatState {
6
17
  info: ChatInfo;
@@ -194,6 +205,15 @@ export function applyEvent(state: ChatState, event: ChatEvent): ChatState {
194
205
  }
195
206
  }
196
207
 
208
+ /* Fields of a held chat that are not part of its thread; a chat this client does not hold stays as it is. */
209
+ function withFields(store: ChatsStore, key: string, fields: Pick<ChatState, 'bookmarks' | 'visuals'>): Partial<ChatsStore> {
210
+ const current = store.byKey[key];
211
+ if (!current) {
212
+ return {};
213
+ }
214
+ return { byKey: { ...store.byKey, [key]: { ...current, ...fields } } };
215
+ }
216
+
197
217
  export const useChats = create<ChatsStore>((set) => ({
198
218
  byKey: {},
199
219
  statusByKey: {},
@@ -247,22 +267,10 @@ export const useChats = create<ChatsStore>((set) => ({
247
267
  });
248
268
  },
249
269
  bookmarks(key, bookmarks) {
250
- set((s) => {
251
- const current = s.byKey[key];
252
- if (!current) {
253
- return {};
254
- }
255
- return { byKey: { ...s.byKey, [key]: { ...current, bookmarks } } };
256
- });
270
+ set((s) => withFields(s, key, { bookmarks }));
257
271
  },
258
272
  visuals(key, visuals) {
259
- set((s) => {
260
- const current = s.byKey[key];
261
- if (!current) {
262
- return {};
263
- }
264
- return { byKey: { ...s.byKey, [key]: { ...current, visuals } } };
265
- });
273
+ set((s) => withFields(s, key, { visuals }));
266
274
  },
267
275
  forget(key) {
268
276
  set((s) => {
@@ -297,6 +305,24 @@ export function useChatRow<T>(chatId: string, select: (row: ChatState | undefine
297
305
  return useChats((s) => select(s.byKey[keyOf(chatId)]));
298
306
  }
299
307
 
308
+ /*
309
+ * The models the CLI of this provider reported running in the chats this client holds of the host.
310
+ * A model it ran is proof the CLI offers it, whatever the version says. Outside a scope there is none.
311
+ */
312
+ export function useReportedModels(provider: AgentKind): ReadonlySet<string> {
313
+ const scope = useContext(ChatScopeContext);
314
+ const joined = useChats((s) => {
315
+ const names = new Set<string>();
316
+ for (const [key, state] of Object.entries(s.statusByKey)) {
317
+ if (scope?.owns(key) === true && state.info.provider === provider && state.info.model !== null) {
318
+ names.add(state.info.model);
319
+ }
320
+ }
321
+ return [...names].sort().join('\n');
322
+ });
323
+ return useMemo(() => new Set(joined === '' ? [] : joined.split('\n')), [joined]);
324
+ }
325
+
300
326
  /* The item as the thread holds it now, for a row derived from the structure, which a delta leaves alone. */
301
327
  export function useCurrentItem<T extends ChatItem>(chatId: string, derived: T): T {
302
328
  return useChatRow(chatId, (row) => {
@@ -51,14 +51,6 @@ function parsePreferences(raw: string | null): Preferences {
51
51
 
52
52
  const storage = persistedJson(storageKey, parsePreferences, DEFAULTS, () => chatStorageLegacyKeys('usage'));
53
53
 
54
- function readPreferences(): Preferences {
55
- return storage.read();
56
- }
57
-
58
- function persist(preferences: Preferences): void {
59
- storage.write(preferences);
60
- }
61
-
62
54
  /* The three stored choices out of the store, so a setter writes the other two back unchanged. */
63
55
  function chosen(state: Preferences): Preferences {
64
56
  return { period: state.period, metric: state.metric, currency: state.currency };
@@ -73,10 +65,7 @@ export function dayOf(date: Date): string {
73
65
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
74
66
  }
75
67
 
76
- /*
77
- * What a period asks for. Today counts from local midnight in hours, the way the commit log already
78
- * groups days, so every other period reads as one bar per calendar day.
79
- */
68
+ /* What a period asks for: today in hours from local midnight, every other period a bar per calendar day. */
80
69
  export function windowFor(period: UsagePeriod, now = new Date()): UsageSummaryPayload {
81
70
  const days = USAGE_PERIODS.find((entry) => entry.id === period)?.days ?? 7;
82
71
  const from = new Date(now.getFullYear(), now.getMonth(), now.getDate() - (days - 1));
@@ -116,10 +105,8 @@ const EMPTY: UsageRow = { asked: null, summary: null, loading: false, failed: fa
116
105
  interface UsageStore extends Preferences {
117
106
  byScope: Record<string, UsageRow>;
118
107
  /*
119
- * The host the usage page is showing, by scope id, or null for the one its surroundings are on.
120
- * Deliberately not in `Preferences`, so it is not written to storage: a remembered host outlives
121
- * this client's knowledge of it, and the numbers worth opening on are those of the host you work
122
- * on. It does survive closing the page, which is what makes browsing two hosts bearable.
108
+ * The host the usage page shows, by scope id, or null for the one around it. Not stored: a
109
+ * remembered host outlives this client's knowledge of it. It does survive closing the page.
123
110
  */
124
111
  chosen: string | null;
125
112
  /* Which host's numbers the page shows; null hands it back to the one around it. */
@@ -137,56 +124,58 @@ interface UsageStore extends Preferences {
137
124
 
138
125
  /* Reads the period, metric and currency again, once another window of the same origin wrote them. */
139
126
  export function reloadUsagePreferences(): void {
140
- useUsageStore.setState(readPreferences());
127
+ useUsageStore.setState(storage.read());
141
128
  }
142
129
 
143
130
  export const useUsageStore = createChatStore<UsageStore>(
144
- (set, get) => ({
145
- ...DEFAULTS,
146
- byScope: {},
147
- chosen: null,
148
- choose(scopeId) {
149
- set({ chosen: scopeId });
150
- },
151
- setPeriod(period) {
152
- persist({ ...chosen(get()), period });
153
- set({ period });
154
- },
155
- setMetric(metric) {
156
- persist({ ...chosen(get()), metric });
157
- set({ metric });
158
- },
159
- setCurrency(currency) {
160
- persist({ ...chosen(get()), currency });
161
- set({ currency });
162
- },
163
- setLoading(scopeId, loading) {
164
- set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), loading } } });
165
- },
166
- receive(scopeId, asked, summary) {
167
- set({
168
- byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), asked, summary, loading: false, failed: false } }
169
- });
170
- },
171
- fail(scopeId) {
172
- set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), loading: false, failed: true } } });
173
- },
174
- setLimits(scopeId, limits) {
175
- set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), limits } } });
176
- },
177
- forget(scopeId) {
178
- const { [scopeId]: _gone, ...rest } = get().byScope;
179
- set({ byScope: rest });
180
- }
181
- }),
182
- (store) => store.setState(readPreferences())
131
+ (set, get) => {
132
+ const choosePreference = (change: Partial<Preferences>): void => {
133
+ storage.write({ ...chosen(get()), ...change });
134
+ set(change);
135
+ };
136
+ const patchRow = (scopeId: string, change: Partial<UsageRow>): void => {
137
+ set({ byScope: { ...get().byScope, [scopeId]: { ...(get().byScope[scopeId] ?? EMPTY), ...change } } });
138
+ };
139
+ return {
140
+ ...DEFAULTS,
141
+ byScope: {},
142
+ chosen: null,
143
+ choose(scopeId) {
144
+ set({ chosen: scopeId });
145
+ },
146
+ setPeriod(period) {
147
+ choosePreference({ period });
148
+ },
149
+ setMetric(metric) {
150
+ choosePreference({ metric });
151
+ },
152
+ setCurrency(currency) {
153
+ choosePreference({ currency });
154
+ },
155
+ setLoading(scopeId, loading) {
156
+ patchRow(scopeId, { loading });
157
+ },
158
+ receive(scopeId, asked, summary) {
159
+ patchRow(scopeId, { asked, summary, loading: false, failed: false });
160
+ },
161
+ fail(scopeId) {
162
+ patchRow(scopeId, { loading: false, failed: true });
163
+ },
164
+ setLimits(scopeId, limits) {
165
+ patchRow(scopeId, { limits });
166
+ },
167
+ forget(scopeId) {
168
+ const { [scopeId]: _gone, ...rest } = get().byScope;
169
+ set({ byScope: rest });
170
+ }
171
+ };
172
+ },
173
+ (store) => store.setState(storage.read())
183
174
  );
184
175
 
185
176
  /*
186
- * The preferences of the viewer over the numbers of the host in scope, as one thing to select from.
187
- * The usage page renders the host its picker is on as a scope of its own; everything outside it (the
188
- * limit bars beside the work) reads the host around it, so browsing another host's spend never
189
- * moves the bars that say how much of the plan the agents beside you have left.
177
+ * The viewer's preferences over the numbers of the host in scope. The usage page renders its picked
178
+ * host as a scope of its own, so browsing another host's spend never moves the limit bars outside it.
190
179
  */
191
180
  export type UsageView = Preferences & UsageRow;
192
181
 
package/src/storage.ts CHANGED
@@ -88,25 +88,25 @@ export function createChatStore<T>(initial: StateCreator<T>, hydrate: (store: St
88
88
  hydrate(store);
89
89
  };
90
90
  hydrators.push(hydrateOnce);
91
- const useStore = ((...args: Parameters<typeof store>) => {
91
+ const ready = (): void => {
92
92
  hydrateChatStorage();
93
93
  hydrateOnce();
94
+ };
95
+ const useStore = ((...args: Parameters<typeof store>) => {
96
+ ready();
94
97
  return store(...args);
95
98
  }) as typeof store;
96
99
  Object.assign(useStore, store);
97
100
  useStore.getState = () => {
98
- hydrateChatStorage();
99
- hydrateOnce();
101
+ ready();
100
102
  return store.getState();
101
103
  };
102
104
  useStore.subscribe = (...args) => {
103
- hydrateChatStorage();
104
- hydrateOnce();
105
+ ready();
105
106
  return store.subscribe(...args);
106
107
  };
107
108
  useStore.setState = ((...args: Parameters<typeof store.setState>) => {
108
- hydrateChatStorage();
109
- hydrateOnce();
109
+ ready();
110
110
  store.setState(...args);
111
111
  }) as typeof store.setState;
112
112
  return useStore;
package/src/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);
@@ -10,8 +10,7 @@ import { ProviderLogo } from '../agents/ProviderLogo';
10
10
  import { PROVIDER_COLORS, PROVIDER_LABELS } from './format';
11
11
  import { accountNote, explain, isSignedOut, nextReset, type LimitAccount, type LimitGroup } from './limit-groups';
12
12
 
13
- /* Red where a window is nearly spent, amber where it is worth knowing. A window with room to spare
14
- is not news, so it takes the text color rather than a hue that competes with the two that are. */
13
+ /* Red where a window is nearly spent, amber where it is worth knowing; room to spare is not news and takes the text color. */
15
14
  function toneOf(used: number): string {
16
15
  return used >= 0.9 ? 'bg-status-error' : used >= 0.7 ? 'bg-status-needs-you' : 'bg-text';
17
16
  }
@@ -93,8 +92,7 @@ export function WindowBar({ window, now, compact }: { window: UsageWindow; now:
93
92
  {bar}
94
93
  </Tooltip>
95
94
  )}
96
- {/* Under the bar rather than beside the label. A weekday and a countdown are too long to
97
- share that line, and a reset that wrapped would read as two of them. */}
95
+ {/* Under the bar: a weekday and a countdown are too long to share the label's line. */}
98
96
  {resetsAt !== null && (
99
97
  <p className="text-xs whitespace-nowrap text-text-faint">
100
98
  {t('limits.resets', { at: resetsAt })}
@@ -16,13 +16,11 @@ import { deriveDays } from './summary';
16
16
 
17
17
  type Breakdown = 'models' | 'projects' | 'day';
18
18
 
19
- /* The row of every table, the same height and the same hover as a commit row in the git panel. */
20
19
  const ROW = 'flex h-8 items-center gap-2 rounded-md px-2 text-xs hover:bg-surface-hover';
21
20
 
22
21
  const HEAD = 'flex h-6 items-center gap-2 px-2 text-xs text-text-faint';
23
22
 
24
- /* A row is padded so its hover has room around the text, and the table pulls that padding back off
25
- again. The first column then starts on the same line as every other section of the page. */
23
+ /* The table pulls the padding that gives a row's hover room back off, so its first column lines up with the page. */
26
24
  const TABLE = '-mx-2 flex flex-col';
27
25
 
28
26
  const TABS: readonly Breakdown[] = ['models', 'projects', 'day'];
@@ -102,8 +100,7 @@ function ProjectRow({ project, top, money }: { project: UsageProject; top: numbe
102
100
  // The usage on screen is one host's, so the look of a project is that host's too.
103
101
  const look = chatHost().useProjectLook(useChatScope().id, project.projectId);
104
102
  return (
105
- /* The mark sits on the title rather than between the two lines, so a column of icons lines
106
- up with the names beside it and not with the paths under them. */
103
+ /* The mark sits on the title rather than between the two lines, so the icons line up with the names. */
107
104
  <div className={`${ROW} h-auto items-start py-1.5`}>
108
105
  <span className="mt-px flex">
109
106
  {look?.mark ?? (
@@ -3,10 +3,8 @@ import i18next from 'i18next';
3
3
  import { Dialog } from '@adecore/ui';
4
4
 
5
5
  /*
6
- * The frame of the usage page: larger than the settings, since the chart and the breakdown need the
7
- * width. What goes in it (`UsagePage`, in an `ErrorBoundary` of the app's) mounts only while it is
8
- * open, so nothing is scanned behind a closed dialog. It is a dialog and not a view, since none of it
9
- * belongs to one project.
6
+ * The frame of the usage page, wider than the settings for the chart and the breakdown. What goes in
7
+ * it mounts only while it is open, so nothing is scanned behind a closed dialog.
10
8
  */
11
9
  export function UsageDialog({ open, onOpenChange, children }: { open: boolean; onOpenChange(open: boolean): void; children: ReactNode }) {
12
10
  return (
@@ -5,8 +5,7 @@ export { formatClock } from '@adecore/ui/format';
5
5
  /* Dollars are what a price table is in; euros are what the page can be set to. */
6
6
  export type UsageCurrency = 'USD' | 'EUR';
7
7
 
8
- /* Every number on this page is read in the region of the machine looking at it, as a bank statement
9
- would be: `$5,480.96` in the US, `$ 5.480,96` in the Netherlands. The labels stay English. */
8
+ /* Read in the region of the machine looking at it: `$5,480.96` in the US, `$ 5.480,96` in the Netherlands. */
10
9
  export function formatCount(value: number): string {
11
10
  return formatNumber(value);
12
11
  }
@@ -33,8 +32,10 @@ export const PROVIDER_LABELS: Record<UsageProvider, string> = { claude: 'Claude
33
32
 
34
33
  export const PROVIDER_COLORS: Record<UsageProvider, string> = { claude: 'var(--chart-claude)', codex: 'var(--chart-codex)' };
35
34
 
36
- /* A slot is `2026-09-10` or `2026-09-10T14`, already in the viewer's own zone, so it is read as
37
- plain wall clock and never handed to a parser that would shift it back. */
35
+ /*
36
+ * A slot is `2026-09-10` or `2026-09-10T14`, already in the viewer's own zone, so it is read as plain
37
+ * wall clock and never handed to a parser that would shift it back.
38
+ */
38
39
  function dateOfSlot(slot: string): Date {
39
40
  return new Date(Number(slot.slice(0, 4)), Number(slot.slice(5, 7)) - 1, Number(slot.slice(8, 10)));
40
41
  }
@@ -63,8 +64,7 @@ export function formatDate(at: number): string {
63
64
  return formatDay(at);
64
65
  }
65
66
 
66
- /* A path under a row that already carries the name: enough of the tail to tell two checkouts of the
67
- same repository apart, without the home directory nobody needs to read again. */
67
+ /* Enough of a path's tail to tell two checkouts of the same repository apart, under a row that already carries the name. */
68
68
  export function shortPath(path: string): string {
69
69
  const parts = path.split('/').filter(Boolean);
70
70
  return parts.length <= 3 ? path : `…/${parts.slice(-3).join('/')}`;
@@ -2,11 +2,7 @@ import { useMemo } from 'react';
2
2
  import { useUsage } from '../state/usage';
3
3
  import { moneyFormat } from './format';
4
4
 
5
- /*
6
- * The formatter every amount on the page goes through: the currency that was chosen, at the rate
7
- * the summary on screen carries. It is a hook rather than a prop because every row wants it and
8
- * none of them wants to know where the rate came from.
9
- */
5
+ /* The formatter every amount on the page goes through: the chosen currency, at the rate the summary on screen carries. */
10
6
  export function useMoney(): (usd: number) => string {
11
7
  const currency = useUsage((s) => s.currency);
12
8
  const rate = useUsage((s) => s.summary?.rate ?? null);
@@ -117,10 +117,8 @@ export interface UsageDay {
117
117
  }
118
118
 
119
119
  /*
120
- * The same buckets the chart draws, written out as a table of calendar days, newest first. Today is
121
- * drawn per hour but read per day, so an hour slot is folded onto the date it names. A day nothing
122
- * happened in is left out, since the chart already shows the gap, and a quiet fortnight would otherwise
123
- * be fourteen rows of zeroes between the days worth reading.
120
+ * The chart's buckets as a table of calendar days, newest first; an hour slot folds onto the date it
121
+ * names. A day nothing happened in is left out, since the chart already shows the gap.
124
122
  */
125
123
  export function deriveDays(summary: UsageSummaryResult): UsageDay[] {
126
124
  const rows = new Map<string, UsageDay>();