@adecore/agents-react 0.33.1 → 0.33.3

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.
@@ -3,5 +3,6 @@ import { type AccountEntry } from './accounts.js';
3
3
  export declare function limitsAccountId(entry: UsageLimitsProvider): string;
4
4
  export declare function limitsOfAccount(snapshot: UsageLimitsSnapshot | null, id: string): UsageLimitsProvider | null;
5
5
  export declare function sessionWindow(entry: UsageLimitsProvider | null): UsageWindow | null;
6
+ export declare function continueTargets(accounts: ProviderAccounts | null, snapshot: UsageLimitsSnapshot | null, kind: AgentKind, current: string | undefined): AccountEntry[];
6
7
  export declare function continueTarget(accounts: ProviderAccounts | null, snapshot: UsageLimitsSnapshot | null, kind: AgentKind, current: string | undefined): AccountEntry | null;
7
8
  export declare function hasUnreadAccount(accounts: ProviderAccounts | null, snapshot: UsageLimitsSnapshot | null, kind: AgentKind, current: string | undefined): boolean;
@@ -22,11 +22,11 @@ function others(accounts, kind, current) {
22
22
  return accountsOfKind(accounts, kind).filter(entry => entry.id !== (current ?? kind) && entry.account.enabled !== false && entry.status?.state === 'ready');
23
23
  }
24
24
  /*
25
- * The account a chat that stopped on a limit could go on under: another one of its CLI that is on,
26
- * signed in and has room, the one with the least of its session spent. Null when none has room, or
27
- * none was read yet; the choice is only ever offered, never made.
25
+ * The accounts a chat that stopped on a limit could go on under: the others of its CLI that are on,
26
+ * signed in and have room, the one with the least of its session spent first. Empty when none has
27
+ * room, or none was read yet; the choice is only ever offered, never made.
28
28
  */
29
- export function continueTarget(accounts, snapshot, kind, current) {
29
+ export function continueTargets(accounts, snapshot, kind, current) {
30
30
  const roomy = others(accounts, kind, current).flatMap(entry => {
31
31
  const limits = limitsOfAccount(snapshot, entry.id);
32
32
  return hasRoom(limits) ? [{
@@ -35,7 +35,11 @@ export function continueTarget(accounts, snapshot, kind, current) {
35
35
  }] : [];
36
36
  });
37
37
  roomy.sort((a, b) => a.pressure - b.pressure);
38
- return roomy[0]?.entry ?? null;
38
+ return roomy.map(target => target.entry);
39
+ }
40
+ /* The one `continueTargets` suggests: the account with the most room. */
41
+ export function continueTarget(accounts, snapshot, kind, current) {
42
+ return continueTargets(accounts, snapshot, kind, current)[0] ?? null;
39
43
  }
40
44
  /* Whether another account a chat could go on under has no numbers yet, since the machine only reads one in use by itself. */
41
45
  export function hasUnreadAccount(accounts, snapshot, kind, current) {
@@ -1,19 +1,22 @@
1
1
  import { c as _c } from "react/compiler-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { Hourglass } from 'lucide-react';
4
+ import clsx from 'clsx';
5
+ import { ChevronDown, Hourglass } from 'lucide-react';
5
6
  import { AccountDot } from '../../agents/AccountDot.js';
6
- import { continueTarget, hasUnreadAccount } from '../../agents/account-limits.js';
7
+ import { continueTargets, hasUnreadAccount, limitsOfAccount, sessionWindow } from '../../agents/account-limits.js';
8
+ import { canContinueOn } from '../../agents/accounts.js';
7
9
  import { useAccountChoice } from '../account-choice.js';
8
10
  import { limitView } from '../logic/limit.js';
9
11
  import { useFormatLocale } from '@adecore/ui/format';
10
- import { Switch, Button, Icon, Pill, Tooltip, useNow } from '@adecore/ui';
12
+ import { Switch, Button, ButtonGroup, Icon, Menu, Pill, Tooltip, useNow } from '@adecore/ui';
11
13
  import { useUsageLimits } from '../../usage/limits.js';
12
14
  import { useChats } from '../../state/chats.js';
13
15
  import { useChatHost } from '../../host.js';
14
16
  import { useChatScope } from '../../scope.js';
15
17
  import { useChatActions } from '../actions.js';
16
18
  import { useUsageStore } from '../../state/usage.js';
19
+ import { SessionLine } from './SessionLine.js';
17
20
  import { useHostResumeAtReset } from '../../host-hooks.js';
18
21
  // A time a limit names reads as a clock today and with its day after that, so the words follow the minute.
19
22
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
@@ -118,13 +121,15 @@ function ResumeAtResetToggle(t0) {
118
121
  return t7;
119
122
  }
120
123
  /*
121
- * Another account of the chat's CLI that has room: the one with the least of its session spent. Only
122
- * ever offered, since going on there spends someone's other plan. An account that reads the chat's
123
- * conversation takes it over; any other goes on in a fork, which then opens the way a fork does.
124
+ * Another account of the chat's CLI that has room: the one with the least of its session spent, and
125
+ * beside it a menu of every account with room, for a person whose companies each pay for their own
126
+ * work. Only ever offered, since going on there spends someone's other plan, and nothing is
127
+ * remembered: the next limit suggests the one with the most room again. An account that reads the
128
+ * chat's conversation takes it over; any other goes on in a fork, which then opens the way a fork does.
124
129
  */
125
130
  function _temp() {}
126
131
  function ContinueOn(t0) {
127
- const $ = _c(25);
132
+ const $ = _c(65);
128
133
  const {
129
134
  chatId,
130
135
  info,
@@ -138,7 +143,7 @@ function ContinueOn(t0) {
138
143
  const actions = useChatActions();
139
144
  const limits = useUsageLimits();
140
145
  const [busy, setBusy] = useState(false);
141
- const target = continueTarget(choice.accounts, limits, info.provider, info.account);
146
+ const targets = continueTargets(choice.accounts, limits, info.provider, info.account);
142
147
  let t1;
143
148
  if ($[0] !== choice.accounts || $[1] !== info.account || $[2] !== info.provider || $[3] !== limits) {
144
149
  t1 = limits !== null && hasUnreadAccount(choice.accounts, limits, info.provider, info.account);
@@ -169,86 +174,320 @@ function ContinueOn(t0) {
169
174
  t3 = $[8];
170
175
  }
171
176
  useEffect(t2, t3);
172
- if (target === null) {
177
+ const target = targets[0];
178
+ if (target === undefined) {
173
179
  return null;
174
180
  }
181
+ let t4;
182
+ if ($[9] !== actions || $[10] !== chatId || $[11] !== choice || $[12] !== host || $[13] !== t) {
183
+ t4 = entry => {
184
+ setBusy(true);
185
+ actions.continueOn(chatId, entry.id).catch(e => host.notify({
186
+ kind: "error",
187
+ title: t("limit.continueFailed", {
188
+ account: choice.nameOf(entry)
189
+ }),
190
+ description: e instanceof Error ? e.message : String(e)
191
+ })).finally(() => setBusy(false));
192
+ };
193
+ $[9] = actions;
194
+ $[10] = chatId;
195
+ $[11] = choice;
196
+ $[12] = host;
197
+ $[13] = t;
198
+ $[14] = t4;
199
+ } else {
200
+ t4 = $[14];
201
+ }
202
+ const go = t4;
175
203
  const name = choice.nameOf(target);
176
- const go = () => {
177
- setBusy(true);
178
- actions.continueOn(chatId, target.id).catch(e => host.notify({
179
- kind: "error",
180
- title: t("limit.continueFailed", {
181
- account: name
182
- }),
183
- description: e instanceof Error ? e.message : String(e)
184
- })).finally(() => setBusy(false));
185
- };
204
+ const split = targets.length > 1;
186
205
  const T0 = Tooltip;
187
- const t4 = t("limit.continueOnTip", {
206
+ const t5 = t("limit.continueOnTip", {
188
207
  account: name
189
208
  });
190
209
  const T1 = Button;
191
- const t5 = "secondary";
192
- const t6 = "sm";
193
- const t7 = "shrink-0 gap-1.5 whitespace-nowrap";
194
- const t8 = t("limit.continueOnAria", {
210
+ const t6 = "secondary";
211
+ const t7 = "sm";
212
+ const t8 = split && "rounded-r-none";
213
+ let t9;
214
+ if ($[15] !== t8) {
215
+ t9 = clsx("shrink-0 gap-1.5 whitespace-nowrap", t8);
216
+ $[15] = t8;
217
+ $[16] = t9;
218
+ } else {
219
+ t9 = $[16];
220
+ }
221
+ const t10 = t("limit.continueOnAria", {
195
222
  account: name
196
223
  });
197
- let t9;
198
- if ($[9] !== t) {
199
- t9 = t("limit.continueOn");
200
- $[9] = t;
201
- $[10] = t9;
224
+ let t11;
225
+ if ($[17] !== go || $[18] !== target) {
226
+ t11 = () => go(target);
227
+ $[17] = go;
228
+ $[18] = target;
229
+ $[19] = t11;
202
230
  } else {
203
- t9 = $[10];
231
+ t11 = $[19];
204
232
  }
205
- let t10;
206
- if ($[11] !== target.account.color) {
207
- t10 = /*#__PURE__*/_jsx(AccountDot, {
233
+ let t12;
234
+ if ($[20] !== t) {
235
+ t12 = t("limit.continueOn");
236
+ $[20] = t;
237
+ $[21] = t12;
238
+ } else {
239
+ t12 = $[21];
240
+ }
241
+ let t13;
242
+ if ($[22] !== target.account.color) {
243
+ t13 = /*#__PURE__*/_jsx(AccountDot, {
208
244
  color: target.account.color,
209
245
  className: "size-1.75"
210
246
  });
211
- $[11] = target.account.color;
212
- $[12] = t10;
247
+ $[22] = target.account.color;
248
+ $[23] = t13;
213
249
  } else {
214
- t10 = $[12];
215
- }
216
- let t11;
217
- if ($[13] !== T1 || $[14] !== busy || $[15] !== go || $[16] !== name || $[17] !== t10 || $[18] !== t8 || $[19] !== t9) {
218
- t11 = /*#__PURE__*/_jsxs(T1, {
219
- variant: t5,
220
- size: t6,
221
- className: t7,
250
+ t13 = $[23];
251
+ }
252
+ let t14;
253
+ if ($[24] !== T1 || $[25] !== busy || $[26] !== name || $[27] !== t10 || $[28] !== t11 || $[29] !== t12 || $[30] !== t13 || $[31] !== t9) {
254
+ t14 = /*#__PURE__*/_jsxs(T1, {
255
+ variant: t6,
256
+ size: t7,
257
+ className: t9,
222
258
  disabled: busy,
223
- "aria-label": t8,
224
- onClick: go,
225
- children: [t9, t10, name]
259
+ "aria-label": t10,
260
+ onClick: t11,
261
+ children: [t12, t13, name]
226
262
  });
227
- $[13] = T1;
228
- $[14] = busy;
229
- $[15] = go;
230
- $[16] = name;
231
- $[17] = t10;
232
- $[18] = t8;
233
- $[19] = t9;
234
- $[20] = t11;
263
+ $[24] = T1;
264
+ $[25] = busy;
265
+ $[26] = name;
266
+ $[27] = t10;
267
+ $[28] = t11;
268
+ $[29] = t12;
269
+ $[30] = t13;
270
+ $[31] = t9;
271
+ $[32] = t14;
235
272
  } else {
236
- t11 = $[20];
273
+ t14 = $[32];
237
274
  }
238
- let t12;
239
- if ($[21] !== T0 || $[22] !== t11 || $[23] !== t4) {
240
- t12 = /*#__PURE__*/_jsx(T0, {
241
- label: t4,
242
- children: t11
275
+ let t15;
276
+ if ($[33] !== T0 || $[34] !== t14 || $[35] !== t5) {
277
+ t15 = /*#__PURE__*/_jsx(T0, {
278
+ label: t5,
279
+ children: t14
280
+ });
281
+ $[33] = T0;
282
+ $[34] = t14;
283
+ $[35] = t5;
284
+ $[36] = t15;
285
+ } else {
286
+ t15 = $[36];
287
+ }
288
+ const suggestion = t15;
289
+ if (!split) {
290
+ return suggestion;
291
+ }
292
+ let t16;
293
+ if ($[37] !== t) {
294
+ t16 = t("limit.chooseAccount");
295
+ $[37] = t;
296
+ $[38] = t16;
297
+ } else {
298
+ t16 = $[38];
299
+ }
300
+ let t17;
301
+ if ($[39] !== busy) {
302
+ t17 = /*#__PURE__*/_jsx(Button, {
303
+ variant: "secondary",
304
+ size: "sm",
305
+ className: "-ml-px rounded-l-none border-l-0 px-1.5",
306
+ disabled: busy
307
+ });
308
+ $[39] = busy;
309
+ $[40] = t17;
310
+ } else {
311
+ t17 = $[40];
312
+ }
313
+ let t18;
314
+ if ($[41] === Symbol.for("react.memo_cache_sentinel")) {
315
+ t18 = /*#__PURE__*/_jsx(Icon, {
316
+ icon: ChevronDown,
317
+ size: 14
318
+ });
319
+ $[41] = t18;
320
+ } else {
321
+ t18 = $[41];
322
+ }
323
+ let t19;
324
+ if ($[42] !== t17) {
325
+ t19 = /*#__PURE__*/_jsx(Menu.Trigger, {
326
+ render: t17,
327
+ children: t18
328
+ });
329
+ $[42] = t17;
330
+ $[43] = t19;
331
+ } else {
332
+ t19 = $[43];
333
+ }
334
+ let t20;
335
+ if ($[44] !== t16 || $[45] !== t19) {
336
+ t20 = /*#__PURE__*/_jsx(Tooltip, {
337
+ label: t16,
338
+ name: true,
339
+ children: t19
340
+ });
341
+ $[44] = t16;
342
+ $[45] = t19;
343
+ $[46] = t20;
344
+ } else {
345
+ t20 = $[46];
346
+ }
347
+ let t21;
348
+ if ($[47] !== t) {
349
+ t21 = t("limit.continueOn");
350
+ $[47] = t;
351
+ $[48] = t21;
352
+ } else {
353
+ t21 = $[48];
354
+ }
355
+ let t22;
356
+ if ($[49] !== t21) {
357
+ t22 = /*#__PURE__*/_jsx(Menu.GroupLabel, {
358
+ children: t21
359
+ });
360
+ $[49] = t21;
361
+ $[50] = t22;
362
+ } else {
363
+ t22 = $[50];
364
+ }
365
+ let t23;
366
+ if ($[51] !== choice || $[52] !== go || $[53] !== info || $[54] !== targets) {
367
+ t23 = /*#__PURE__*/_jsx(ContinueOnRows, {
368
+ targets: targets,
369
+ info: info,
370
+ choice: choice,
371
+ onPick: go
372
+ });
373
+ $[51] = choice;
374
+ $[52] = go;
375
+ $[53] = info;
376
+ $[54] = targets;
377
+ $[55] = t23;
378
+ } else {
379
+ t23 = $[55];
380
+ }
381
+ let t24;
382
+ if ($[56] !== t22 || $[57] !== t23) {
383
+ t24 = /*#__PURE__*/_jsx(Menu.Popup, {
384
+ align: "end",
385
+ className: "min-w-64",
386
+ children: /*#__PURE__*/_jsxs(Menu.Group, {
387
+ children: [t22, t23]
388
+ })
389
+ });
390
+ $[56] = t22;
391
+ $[57] = t23;
392
+ $[58] = t24;
393
+ } else {
394
+ t24 = $[58];
395
+ }
396
+ let t25;
397
+ if ($[59] !== t20 || $[60] !== t24) {
398
+ t25 = /*#__PURE__*/_jsxs(Menu.Root, {
399
+ children: [t20, t24]
400
+ });
401
+ $[59] = t20;
402
+ $[60] = t24;
403
+ $[61] = t25;
404
+ } else {
405
+ t25 = $[61];
406
+ }
407
+ let t26;
408
+ if ($[62] !== suggestion || $[63] !== t25) {
409
+ t26 = /*#__PURE__*/_jsxs(ButtonGroup, {
410
+ className: "shrink-0",
411
+ children: [suggestion, t25]
243
412
  });
244
- $[21] = T0;
245
- $[22] = t11;
246
- $[23] = t4;
247
- $[24] = t12;
413
+ $[62] = suggestion;
414
+ $[63] = t25;
415
+ $[64] = t26;
416
+ } else {
417
+ t26 = $[64];
418
+ }
419
+ return t26;
420
+ }
421
+ /* Mounted only while the menu is open, so the clock of the session lines only ticks while someone reads them. */
422
+ function ContinueOnRows(t0) {
423
+ const $ = _c(15);
424
+ const {
425
+ targets,
426
+ info,
427
+ choice,
428
+ onPick
429
+ } = t0;
430
+ const {
431
+ t
432
+ } = useTranslation("agent-chat");
433
+ const limits = useUsageLimits();
434
+ const now = useNow(MINUTE_MS);
435
+ let t1;
436
+ if ($[0] !== choice || $[1] !== info || $[2] !== limits || $[3] !== now || $[4] !== onPick || $[5] !== t || $[6] !== targets) {
437
+ let t2;
438
+ if ($[8] !== choice || $[9] !== info || $[10] !== limits || $[11] !== now || $[12] !== onPick || $[13] !== t) {
439
+ t2 = entry => {
440
+ const name = choice.nameOf(entry);
441
+ const session = sessionWindow(limitsOfAccount(limits, entry.id));
442
+ const fork = !canContinueOn(choice.accounts, info.provider, info.account, entry.id);
443
+ return /*#__PURE__*/_jsx(Menu.Item, {
444
+ className: "items-start",
445
+ "aria-label": t(fork ? "limit.continueInForkAria" : "limit.continueOnAria", {
446
+ account: name
447
+ }),
448
+ onClick: () => onPick(entry),
449
+ children: /*#__PURE__*/_jsxs("span", {
450
+ className: "flex min-w-0 grow flex-col",
451
+ children: [/*#__PURE__*/_jsxs("span", {
452
+ className: "flex min-w-0 items-center gap-2",
453
+ children: [/*#__PURE__*/_jsx(AccountDot, {
454
+ color: entry.account.color
455
+ }), /*#__PURE__*/_jsx("span", {
456
+ className: "truncate",
457
+ children: name
458
+ }), fork && /*#__PURE__*/_jsx(Menu.Hint, {
459
+ children: t("limit.inFork")
460
+ })]
461
+ }), session !== null && /*#__PURE__*/_jsx(SessionLine, {
462
+ window: session,
463
+ now: now
464
+ })]
465
+ })
466
+ }, entry.id);
467
+ };
468
+ $[8] = choice;
469
+ $[9] = info;
470
+ $[10] = limits;
471
+ $[11] = now;
472
+ $[12] = onPick;
473
+ $[13] = t;
474
+ $[14] = t2;
475
+ } else {
476
+ t2 = $[14];
477
+ }
478
+ t1 = targets.map(t2);
479
+ $[0] = choice;
480
+ $[1] = info;
481
+ $[2] = limits;
482
+ $[3] = now;
483
+ $[4] = onPick;
484
+ $[5] = t;
485
+ $[6] = targets;
486
+ $[7] = t1;
248
487
  } else {
249
- t12 = $[24];
488
+ t1 = $[7];
250
489
  }
251
- return t12;
490
+ return t1;
252
491
  }
253
492
  /* At the top of the composer while the last turn stopped on a limit: what stopped it, when it goes on, and the switch. */
254
493
  export function LimitDock(t0) {
@@ -9,15 +9,15 @@ import { canContinueOn } from '../../agents/accounts.js';
9
9
  import { AgentIcon } from '../../agents/AgentIcon.js';
10
10
  import { modelName, shortModelName } from '../../agents/model-name.js';
11
11
  import { ModelMarks } from './ModelMarks.js';
12
+ import { SessionLine } from './SessionLine.js';
12
13
  import { favoritesFirst, searchModels, shortlistOf, wantsModelSearch } from '../logic/picker-entries.js';
13
14
  import { favoriteModelsOf, toggleFavoriteModel, useChatPreferences } from '../preferences.js';
14
15
  import { CONTEXT_OPTION, contextFraction, contextSegments, orderOptions } from '../logic/context-usage.js';
15
16
  import { RUNTIME_MODES } from '../runtime-modes.js';
16
17
  import { localizedOption } from '../../agents/option-words.js';
17
- import { formatClock, formatWeekdayClock, isSameDay, formatPercent, formatTokens } from '@adecore/ui/format';
18
+ import { formatPercent, formatTokens } from '@adecore/ui/format';
18
19
  import { Icon, Menu, Tooltip, useNow } from '@adecore/ui';
19
20
  import { useChatHost } from '../../host.js';
20
- import { evenPace, paceText } from '../../usage/limit-groups.js';
21
21
  import { useUsageLimits } from '../../usage/limits.js';
22
22
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
23
23
  const MINUTE_MS = 60_000;
@@ -803,7 +803,11 @@ export function RunSettings(t0) {
803
803
  }), t49]
804
804
  });
805
805
  }
806
- /* Red where a window is nearly spent, amber where it is worth knowing, as the usage bars have it. */
806
+ /*
807
+ * The account the next turn runs under, for the chat's own CLI. Before the first turn any of them;
808
+ * after it one that reads the conversation, since the others would start it over, and a fork is how
809
+ * the conversation goes along to those. A switch applies from the next turn.
810
+ */
807
811
  function _temp3(count, group) {
808
812
  return count + group.models.length;
809
813
  }
@@ -813,23 +817,6 @@ function _temp2(preferences) {
813
817
  function _temp(option_0) {
814
818
  return option_0.id === CONTEXT_OPTION;
815
819
  }
816
- function sessionTone(used) {
817
- return used >= 0.9 ? {
818
- bar: 'bg-status-error',
819
- text: 'text-status-error'
820
- } : used >= 0.7 ? {
821
- bar: 'bg-status-needs-you',
822
- text: 'text-status-needs-you'
823
- } : {
824
- bar: 'bg-text-muted',
825
- text: 'text-text-muted'
826
- };
827
- }
828
- /*
829
- * The account the next turn runs under, for the chat's own CLI. Before the first turn any of them;
830
- * after it one that reads the conversation, since the others would start it over, and a fork is how
831
- * the conversation goes along to those. A switch applies from the next turn.
832
- */
833
820
  function AccountSubmenu(t0) {
834
821
  const $ = _c(51);
835
822
  const {
@@ -1119,158 +1106,6 @@ function AccountRows(t0) {
1119
1106
  }
1120
1107
  return t1;
1121
1108
  }
1122
- function SessionLine(t0) {
1123
- const $ = _c(37);
1124
- const {
1125
- window,
1126
- now
1127
- } = t0;
1128
- const {
1129
- t
1130
- } = useTranslation("agent-chat");
1131
- let resetsAt;
1132
- let t1;
1133
- let t2;
1134
- let t3;
1135
- let t4;
1136
- let t5;
1137
- if ($[0] !== now || $[1] !== window) {
1138
- const percent = Math.round(window.used * 100);
1139
- const tone = sessionTone(window.used);
1140
- const mark = evenPace(window, now)?.percent ?? null;
1141
- resetsAt = window.resetsAt;
1142
- t4 = "flex items-center gap-2 text-xs text-text-faint tabular-nums";
1143
- const t6 = true;
1144
- const t7 = "relative h-1 w-14 shrink-0 rounded-full bg-surface-sunken";
1145
- const t8 = clsx("absolute inset-y-0 left-0 rounded-full", tone.bar);
1146
- const t9 = `${Math.min(100, percent)}%`;
1147
- let t10;
1148
- if ($[8] !== t9) {
1149
- t10 = {
1150
- width: t9
1151
- };
1152
- $[8] = t9;
1153
- $[9] = t10;
1154
- } else {
1155
- t10 = $[9];
1156
- }
1157
- let t11;
1158
- if ($[10] !== t10 || $[11] !== t8) {
1159
- t11 = /*#__PURE__*/_jsx("span", {
1160
- className: t8,
1161
- style: t10
1162
- });
1163
- $[10] = t10;
1164
- $[11] = t8;
1165
- $[12] = t11;
1166
- } else {
1167
- t11 = $[12];
1168
- }
1169
- const t12 = mark !== null && /*#__PURE__*/_jsx("span", {
1170
- className: clsx("absolute -inset-y-0.5 w-px -translate-x-1/2", mark <= percent ? "bg-surface-raised" : "bg-text-muted"),
1171
- style: {
1172
- left: `${mark}%`
1173
- }
1174
- });
1175
- if ($[13] !== t11 || $[14] !== t12 || $[15] !== t6 || $[16] !== t7) {
1176
- t5 = /*#__PURE__*/_jsxs("span", {
1177
- "aria-hidden": t6,
1178
- className: t7,
1179
- children: [t11, t12]
1180
- });
1181
- $[13] = t11;
1182
- $[14] = t12;
1183
- $[15] = t6;
1184
- $[16] = t7;
1185
- $[17] = t5;
1186
- } else {
1187
- t5 = $[17];
1188
- }
1189
- t1 = true;
1190
- t2 = tone.text;
1191
- t3 = formatPercent(percent);
1192
- $[0] = now;
1193
- $[1] = window;
1194
- $[2] = resetsAt;
1195
- $[3] = t1;
1196
- $[4] = t2;
1197
- $[5] = t3;
1198
- $[6] = t4;
1199
- $[7] = t5;
1200
- } else {
1201
- resetsAt = $[2];
1202
- t1 = $[3];
1203
- t2 = $[4];
1204
- t3 = $[5];
1205
- t4 = $[6];
1206
- t5 = $[7];
1207
- }
1208
- let t6;
1209
- if ($[18] !== t1 || $[19] !== t2 || $[20] !== t3) {
1210
- t6 = /*#__PURE__*/_jsx("span", {
1211
- "aria-hidden": t1,
1212
- className: t2,
1213
- children: t3
1214
- });
1215
- $[18] = t1;
1216
- $[19] = t2;
1217
- $[20] = t3;
1218
- $[21] = t6;
1219
- } else {
1220
- t6 = $[21];
1221
- }
1222
- let t7;
1223
- if ($[22] !== now || $[23] !== window) {
1224
- t7 = paceText(window, now);
1225
- $[22] = now;
1226
- $[23] = window;
1227
- $[24] = t7;
1228
- } else {
1229
- t7 = $[24];
1230
- }
1231
- let t8;
1232
- if ($[25] !== t7) {
1233
- t8 = /*#__PURE__*/_jsx("span", {
1234
- className: "sr-only",
1235
- children: t7
1236
- });
1237
- $[25] = t7;
1238
- $[26] = t8;
1239
- } else {
1240
- t8 = $[26];
1241
- }
1242
- let t9;
1243
- if ($[27] !== now || $[28] !== resetsAt || $[29] !== t) {
1244
- t9 = resetsAt !== null && /*#__PURE__*/_jsxs("span", {
1245
- className: "whitespace-nowrap",
1246
- children: ["\xB7 ", t("pickers.account.resets", {
1247
- time: isSameDay(resetsAt, now) ? formatClock(resetsAt) : formatWeekdayClock(resetsAt)
1248
- })]
1249
- });
1250
- $[27] = now;
1251
- $[28] = resetsAt;
1252
- $[29] = t;
1253
- $[30] = t9;
1254
- } else {
1255
- t9 = $[30];
1256
- }
1257
- let t10;
1258
- if ($[31] !== t4 || $[32] !== t5 || $[33] !== t6 || $[34] !== t8 || $[35] !== t9) {
1259
- t10 = /*#__PURE__*/_jsxs("span", {
1260
- className: t4,
1261
- children: [t5, t6, t8, t9]
1262
- });
1263
- $[31] = t4;
1264
- $[32] = t5;
1265
- $[33] = t6;
1266
- $[34] = t8;
1267
- $[35] = t9;
1268
- $[36] = t10;
1269
- } else {
1270
- t10 = $[36];
1271
- }
1272
- return t10;
1273
- }
1274
1109
  /* How full the context is, drawn small enough to sit in the pill beside the window's size. */
1275
1110
  function ContextRing(t0) {
1276
1111
  const $ = _c(15);
@@ -0,0 +1,5 @@
1
+ import type { UsageWindow } from '@adecore/agent-contracts';
2
+ export declare function SessionLine({ window, now }: {
3
+ window: UsageWindow;
4
+ now: number;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,172 @@
1
+ import { c as _c } from "react/compiler-runtime";
2
+ import { useTranslation } from 'react-i18next';
3
+ import clsx from 'clsx';
4
+ import { formatClock, formatPercent, formatWeekdayClock, isSameDay } from '@adecore/ui/format';
5
+ import { evenPace, paceText } from '../../usage/limit-groups.js';
6
+ /* Red where a window is nearly spent, amber where it is worth knowing, as the usage bars have it. */
7
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
+ function sessionTone(used) {
9
+ return used >= 0.9 ? {
10
+ bar: 'bg-status-error',
11
+ text: 'text-status-error'
12
+ } : used >= 0.7 ? {
13
+ bar: 'bg-status-needs-you',
14
+ text: 'text-status-needs-you'
15
+ } : {
16
+ bar: 'bg-text-muted',
17
+ text: 'text-text-muted'
18
+ };
19
+ }
20
+ /* How much of an account's session is spent and when it resets, on the second line of a row that names the account. */
21
+ export function SessionLine(t0) {
22
+ const $ = _c(37);
23
+ const {
24
+ window,
25
+ now
26
+ } = t0;
27
+ const {
28
+ t
29
+ } = useTranslation("agent-chat");
30
+ let resetsAt;
31
+ let t1;
32
+ let t2;
33
+ let t3;
34
+ let t4;
35
+ let t5;
36
+ if ($[0] !== now || $[1] !== window) {
37
+ const percent = Math.round(window.used * 100);
38
+ const tone = sessionTone(window.used);
39
+ const mark = evenPace(window, now)?.percent ?? null;
40
+ resetsAt = window.resetsAt;
41
+ t4 = "flex items-center gap-2 text-xs text-text-faint tabular-nums";
42
+ const t6 = true;
43
+ const t7 = "relative h-1 w-14 shrink-0 rounded-full bg-surface-sunken";
44
+ const t8 = clsx("absolute inset-y-0 left-0 rounded-full", tone.bar);
45
+ const t9 = `${Math.min(100, percent)}%`;
46
+ let t10;
47
+ if ($[8] !== t9) {
48
+ t10 = {
49
+ width: t9
50
+ };
51
+ $[8] = t9;
52
+ $[9] = t10;
53
+ } else {
54
+ t10 = $[9];
55
+ }
56
+ let t11;
57
+ if ($[10] !== t10 || $[11] !== t8) {
58
+ t11 = /*#__PURE__*/_jsx("span", {
59
+ className: t8,
60
+ style: t10
61
+ });
62
+ $[10] = t10;
63
+ $[11] = t8;
64
+ $[12] = t11;
65
+ } else {
66
+ t11 = $[12];
67
+ }
68
+ const t12 = mark !== null && /*#__PURE__*/_jsx("span", {
69
+ className: clsx("absolute -inset-y-0.5 w-px -translate-x-1/2", mark <= percent ? "bg-surface-raised" : "bg-text-muted"),
70
+ style: {
71
+ left: `${mark}%`
72
+ }
73
+ });
74
+ if ($[13] !== t11 || $[14] !== t12 || $[15] !== t6 || $[16] !== t7) {
75
+ t5 = /*#__PURE__*/_jsxs("span", {
76
+ "aria-hidden": t6,
77
+ className: t7,
78
+ children: [t11, t12]
79
+ });
80
+ $[13] = t11;
81
+ $[14] = t12;
82
+ $[15] = t6;
83
+ $[16] = t7;
84
+ $[17] = t5;
85
+ } else {
86
+ t5 = $[17];
87
+ }
88
+ t1 = true;
89
+ t2 = tone.text;
90
+ t3 = formatPercent(percent);
91
+ $[0] = now;
92
+ $[1] = window;
93
+ $[2] = resetsAt;
94
+ $[3] = t1;
95
+ $[4] = t2;
96
+ $[5] = t3;
97
+ $[6] = t4;
98
+ $[7] = t5;
99
+ } else {
100
+ resetsAt = $[2];
101
+ t1 = $[3];
102
+ t2 = $[4];
103
+ t3 = $[5];
104
+ t4 = $[6];
105
+ t5 = $[7];
106
+ }
107
+ let t6;
108
+ if ($[18] !== t1 || $[19] !== t2 || $[20] !== t3) {
109
+ t6 = /*#__PURE__*/_jsx("span", {
110
+ "aria-hidden": t1,
111
+ className: t2,
112
+ children: t3
113
+ });
114
+ $[18] = t1;
115
+ $[19] = t2;
116
+ $[20] = t3;
117
+ $[21] = t6;
118
+ } else {
119
+ t6 = $[21];
120
+ }
121
+ let t7;
122
+ if ($[22] !== now || $[23] !== window) {
123
+ t7 = paceText(window, now);
124
+ $[22] = now;
125
+ $[23] = window;
126
+ $[24] = t7;
127
+ } else {
128
+ t7 = $[24];
129
+ }
130
+ let t8;
131
+ if ($[25] !== t7) {
132
+ t8 = /*#__PURE__*/_jsx("span", {
133
+ className: "sr-only",
134
+ children: t7
135
+ });
136
+ $[25] = t7;
137
+ $[26] = t8;
138
+ } else {
139
+ t8 = $[26];
140
+ }
141
+ let t9;
142
+ if ($[27] !== now || $[28] !== resetsAt || $[29] !== t) {
143
+ t9 = resetsAt !== null && /*#__PURE__*/_jsxs("span", {
144
+ className: "whitespace-nowrap",
145
+ children: ["\xB7 ", t("pickers.account.resets", {
146
+ time: isSameDay(resetsAt, now) ? formatClock(resetsAt) : formatWeekdayClock(resetsAt)
147
+ })]
148
+ });
149
+ $[27] = now;
150
+ $[28] = resetsAt;
151
+ $[29] = t;
152
+ $[30] = t9;
153
+ } else {
154
+ t9 = $[30];
155
+ }
156
+ let t10;
157
+ if ($[31] !== t4 || $[32] !== t5 || $[33] !== t6 || $[34] !== t8 || $[35] !== t9) {
158
+ t10 = /*#__PURE__*/_jsxs("span", {
159
+ className: t4,
160
+ children: [t5, t6, t8, t9]
161
+ });
162
+ $[31] = t4;
163
+ $[32] = t5;
164
+ $[33] = t6;
165
+ $[34] = t8;
166
+ $[35] = t9;
167
+ $[36] = t10;
168
+ } else {
169
+ t10 = $[36];
170
+ }
171
+ return t10;
172
+ }
@@ -95,6 +95,9 @@ declare const _default: {
95
95
  continueOnAria: string;
96
96
  continueOnTip: string;
97
97
  continueFailed: string;
98
+ chooseAccount: string;
99
+ continueInForkAria: string;
100
+ inFork: string;
98
101
  };
99
102
  contextMeter: {
100
103
  aria: string;
@@ -94,7 +94,10 @@ export default {
94
94
  continueOn: 'Continue on',
95
95
  continueOnAria: 'Continue on {{account}}',
96
96
  continueOnTip: 'Switches to {{account}}, which has the most room left, and resends the turn.',
97
- continueFailed: 'Could not continue on {{account}}'
97
+ continueFailed: 'Could not continue on {{account}}',
98
+ chooseAccount: 'Choose another account',
99
+ continueInForkAria: 'Continue on {{account}} in a fork',
100
+ inFork: 'in a fork'
98
101
  },
99
102
  contextMeter: {
100
103
  aria: '{{percent}}% of context used',
@@ -95,6 +95,9 @@ declare const _default: {
95
95
  continueOnAria: string;
96
96
  continueOnTip: string;
97
97
  continueFailed: string;
98
+ chooseAccount: string;
99
+ continueInForkAria: string;
100
+ inFork: string;
98
101
  };
99
102
  contextMeter: {
100
103
  aria: string;
@@ -94,7 +94,10 @@ export default {
94
94
  continueOn: 'Verder op',
95
95
  continueOnAria: 'Verder op {{account}}',
96
96
  continueOnTip: 'Wisselt naar {{account}}, dat de meeste ruimte over heeft, en verstuurt de turn opnieuw.',
97
- continueFailed: 'Kon niet verder op {{account}}'
97
+ continueFailed: 'Kon niet verder op {{account}}',
98
+ chooseAccount: 'Kies een ander account',
99
+ continueInForkAria: 'Verder op {{account}} in een fork',
100
+ inFork: 'in een fork'
98
101
  },
99
102
  contextMeter: {
100
103
  aria: '{{percent}}% van de context gebruikt',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents-react",
3
- "version": "0.33.1",
3
+ "version": "0.33.3",
4
4
  "description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -67,11 +67,11 @@
67
67
  "test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
68
68
  },
69
69
  "dependencies": {
70
- "@adecore/agent-contracts": "0.33.1",
71
- "@adecore/editor": "0.33.1",
72
- "@adecore/editor-react": "0.33.1",
73
- "@adecore/intelligent-ui": "0.33.1",
74
- "@adecore/merge": "0.33.1",
70
+ "@adecore/agent-contracts": "0.33.3",
71
+ "@adecore/editor": "0.33.3",
72
+ "@adecore/editor-react": "0.33.3",
73
+ "@adecore/intelligent-ui": "0.33.3",
74
+ "@adecore/merge": "0.33.3",
75
75
  "@codemirror/commands": "^6.11.1",
76
76
  "@codemirror/language": "^6.12.4",
77
77
  "@codemirror/state": "^6.7.6",
@@ -94,14 +94,14 @@
94
94
  "zustand": "^5.0.15"
95
95
  },
96
96
  "peerDependencies": {
97
- "@adecore/ui": "^0.33.1",
97
+ "@adecore/ui": "^0.33.3",
98
98
  "i18next": "^26.4.2",
99
99
  "react": "^19.3.0",
100
100
  "react-dom": "^19.3.0",
101
101
  "react-i18next": "^17.0.15"
102
102
  },
103
103
  "devDependencies": {
104
- "@adecore/ui": "0.33.1",
104
+ "@adecore/ui": "0.33.3",
105
105
  "@types/bun": "^1.4.2",
106
106
  "@types/react": "^19.3.0",
107
107
  "@types/react-dom": "^19.3.0",
@@ -32,22 +32,32 @@ function others(accounts: ProviderAccounts | null, kind: AgentKind, current: str
32
32
  }
33
33
 
34
34
  /*
35
- * The account a chat that stopped on a limit could go on under: another one of its CLI that is on,
36
- * signed in and has room, the one with the least of its session spent. Null when none has room, or
37
- * none was read yet; the choice is only ever offered, never made.
35
+ * The accounts a chat that stopped on a limit could go on under: the others of its CLI that are on,
36
+ * signed in and have room, the one with the least of its session spent first. Empty when none has
37
+ * room, or none was read yet; the choice is only ever offered, never made.
38
38
  */
39
- export function continueTarget(
39
+ export function continueTargets(
40
40
  accounts: ProviderAccounts | null,
41
41
  snapshot: UsageLimitsSnapshot | null,
42
42
  kind: AgentKind,
43
43
  current: string | undefined
44
- ): AccountEntry | null {
44
+ ): AccountEntry[] {
45
45
  const roomy = others(accounts, kind, current).flatMap((entry) => {
46
46
  const limits = limitsOfAccount(snapshot, entry.id);
47
47
  return hasRoom(limits) ? [{ entry, pressure: pressure(limits) }] : [];
48
48
  });
49
49
  roomy.sort((a, b) => a.pressure - b.pressure);
50
- return roomy[0]?.entry ?? null;
50
+ return roomy.map((target) => target.entry);
51
+ }
52
+
53
+ /* The one `continueTargets` suggests: the account with the most room. */
54
+ export function continueTarget(
55
+ accounts: ProviderAccounts | null,
56
+ snapshot: UsageLimitsSnapshot | null,
57
+ kind: AgentKind,
58
+ current: string | undefined
59
+ ): AccountEntry | null {
60
+ return continueTargets(accounts, snapshot, kind, current)[0] ?? null;
51
61
  }
52
62
 
53
63
  /* Whether another account a chat could go on under has no numbers yet, since the machine only reads one in use by itself. */
@@ -1,19 +1,22 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { Hourglass } from 'lucide-react';
3
+ import clsx from 'clsx';
4
+ import { ChevronDown, Hourglass } from 'lucide-react';
4
5
  import type { ChatInfo } from '@adecore/agent-contracts';
5
6
  import { AccountDot } from '../../agents/AccountDot';
6
- import { continueTarget, hasUnreadAccount } from '../../agents/account-limits';
7
+ import { continueTargets, hasUnreadAccount, limitsOfAccount, sessionWindow } from '../../agents/account-limits';
8
+ import { canContinueOn, type AccountEntry } from '../../agents/accounts';
7
9
  import { useAccountChoice, type AccountChoice } from '../account-choice';
8
10
  import { limitView } from '../logic/limit';
9
11
  import { useFormatLocale } from '@adecore/ui/format';
10
- import { Switch, Button, Icon, Pill, Tooltip, useNow } from '@adecore/ui';
12
+ import { Switch, Button, ButtonGroup, Icon, Menu, Pill, Tooltip, useNow } from '@adecore/ui';
11
13
  import { useUsageLimits } from '../../usage/limits';
12
14
  import { useChats } from '../../state/chats';
13
15
  import { useChatHost } from '../../host';
14
16
  import { useChatScope, type ChatScope } from '../../scope';
15
17
  import { useChatActions } from '../actions';
16
18
  import { useUsageStore } from '../../state/usage';
19
+ import { SessionLine } from './SessionLine';
17
20
  import { useHostResumeAtReset } from '../../host-hooks';
18
21
 
19
22
  // A time a limit names reads as a clock today and with its day after that, so the words follow the minute.
@@ -62,9 +65,11 @@ function ResumeAtResetToggle({ chatId, info }: { chatId: string; info: ChatInfo
62
65
  }
63
66
 
64
67
  /*
65
- * Another account of the chat's CLI that has room: the one with the least of its session spent. Only
66
- * ever offered, since going on there spends someone's other plan. An account that reads the chat's
67
- * conversation takes it over; any other goes on in a fork, which then opens the way a fork does.
68
+ * Another account of the chat's CLI that has room: the one with the least of its session spent, and
69
+ * beside it a menu of every account with room, for a person whose companies each pay for their own
70
+ * work. Only ever offered, since going on there spends someone's other plan, and nothing is
71
+ * remembered: the next limit suggests the one with the most room again. An account that reads the
72
+ * chat's conversation takes it over; any other goes on in a fork, which then opens the way a fork does.
68
73
  */
69
74
  function ContinueOn({ chatId, info, choice }: { chatId: string; info: ChatInfo; choice: AccountChoice }) {
70
75
  const host = useChatHost();
@@ -73,7 +78,7 @@ function ContinueOn({ chatId, info, choice }: { chatId: string; info: ChatInfo;
73
78
  const actions = useChatActions();
74
79
  const limits = useUsageLimits();
75
80
  const [busy, setBusy] = useState(false);
76
- const target = continueTarget(choice.accounts, limits, info.provider, info.account);
81
+ const targets = continueTargets(choice.accounts, limits, info.provider, info.account);
77
82
  const unread = limits !== null && hasUnreadAccount(choice.accounts, limits, info.provider, info.account);
78
83
 
79
84
  useEffect(() => {
@@ -82,32 +87,34 @@ function ContinueOn({ chatId, info, choice }: { chatId: string; info: ChatInfo;
82
87
  }
83
88
  }, [scope, unread]);
84
89
 
85
- if (target === null) {
90
+ const target = targets[0];
91
+ if (target === undefined) {
86
92
  return null;
87
93
  }
88
- const name = choice.nameOf(target);
89
- const go = (): void => {
94
+ const go = (entry: AccountEntry): void => {
90
95
  setBusy(true);
91
96
  actions
92
- .continueOn(chatId, target.id)
97
+ .continueOn(chatId, entry.id)
93
98
  .catch((e: unknown) =>
94
99
  host.notify({
95
100
  kind: 'error',
96
- title: t('limit.continueFailed', { account: name }),
101
+ title: t('limit.continueFailed', { account: choice.nameOf(entry) }),
97
102
  description: e instanceof Error ? e.message : String(e)
98
103
  })
99
104
  )
100
105
  .finally(() => setBusy(false));
101
106
  };
102
- return (
107
+ const name = choice.nameOf(target);
108
+ const split = targets.length > 1;
109
+ const suggestion = (
103
110
  <Tooltip label={t('limit.continueOnTip', { account: name })}>
104
111
  <Button
105
112
  variant="secondary"
106
113
  size="sm"
107
- className="shrink-0 gap-1.5 whitespace-nowrap"
114
+ className={clsx('shrink-0 gap-1.5 whitespace-nowrap', split && 'rounded-r-none')}
108
115
  disabled={busy}
109
116
  aria-label={t('limit.continueOnAria', { account: name })}
110
- onClick={go}
117
+ onClick={() => go(target)}
111
118
  >
112
119
  {t('limit.continueOn')}
113
120
  <AccountDot color={target.account.color} className="size-1.75" />
@@ -115,6 +122,66 @@ function ContinueOn({ chatId, info, choice }: { chatId: string; info: ChatInfo;
115
122
  </Button>
116
123
  </Tooltip>
117
124
  );
125
+ if (!split) {
126
+ return suggestion;
127
+ }
128
+ return (
129
+ <ButtonGroup className="shrink-0">
130
+ {suggestion}
131
+ <Menu.Root>
132
+ <Tooltip label={t('limit.chooseAccount')} name>
133
+ <Menu.Trigger render={<Button variant="secondary" size="sm" className="-ml-px rounded-l-none border-l-0 px-1.5" disabled={busy} />}>
134
+ <Icon icon={ChevronDown} size={14} />
135
+ </Menu.Trigger>
136
+ </Tooltip>
137
+ <Menu.Popup align="end" className="min-w-64">
138
+ <Menu.Group>
139
+ <Menu.GroupLabel>{t('limit.continueOn')}</Menu.GroupLabel>
140
+ <ContinueOnRows targets={targets} info={info} choice={choice} onPick={go} />
141
+ </Menu.Group>
142
+ </Menu.Popup>
143
+ </Menu.Root>
144
+ </ButtonGroup>
145
+ );
146
+ }
147
+
148
+ /* Mounted only while the menu is open, so the clock of the session lines only ticks while someone reads them. */
149
+ function ContinueOnRows({
150
+ targets,
151
+ info,
152
+ choice,
153
+ onPick
154
+ }: {
155
+ targets: AccountEntry[];
156
+ info: ChatInfo;
157
+ choice: AccountChoice;
158
+ onPick(entry: AccountEntry): void;
159
+ }) {
160
+ const { t } = useTranslation('agent-chat');
161
+ const limits = useUsageLimits();
162
+ const now = useNow(MINUTE_MS);
163
+ return targets.map((entry) => {
164
+ const name = choice.nameOf(entry);
165
+ const session = sessionWindow(limitsOfAccount(limits, entry.id));
166
+ const fork = !canContinueOn(choice.accounts, info.provider, info.account, entry.id);
167
+ return (
168
+ <Menu.Item
169
+ key={entry.id}
170
+ className="items-start"
171
+ aria-label={t(fork ? 'limit.continueInForkAria' : 'limit.continueOnAria', { account: name })}
172
+ onClick={() => onPick(entry)}
173
+ >
174
+ <span className="flex min-w-0 grow flex-col">
175
+ <span className="flex min-w-0 items-center gap-2">
176
+ <AccountDot color={entry.account.color} />
177
+ <span className="truncate">{name}</span>
178
+ {fork && <Menu.Hint>{t('limit.inFork')}</Menu.Hint>}
179
+ </span>
180
+ {session !== null && <SessionLine window={session} now={now} />}
181
+ </span>
182
+ </Menu.Item>
183
+ );
184
+ });
118
185
  }
119
186
 
120
187
  /* At the top of the composer while the last turn stopped on a limit: what stopped it, when it goes on, and the switch. */
@@ -18,7 +18,7 @@ import {
18
18
  Star,
19
19
  type LucideIcon
20
20
  } from 'lucide-react';
21
- import type { AgentKind, ChatUsage, ModelInfo, ModelOptionDescriptor, ModelSelection, ProviderInfo, RuntimeMode, UsageWindow } from '@adecore/agent-contracts';
21
+ import type { AgentKind, ChatUsage, ModelInfo, ModelOptionDescriptor, ModelSelection, ProviderInfo, RuntimeMode } from '@adecore/agent-contracts';
22
22
  import { AccountDot } from '../../agents/AccountDot';
23
23
  import { limitsOfAccount, sessionWindow } from '../../agents/account-limits';
24
24
  import { canContinueOn } from '../../agents/accounts';
@@ -26,15 +26,15 @@ import { AgentIcon } from '../../agents/AgentIcon';
26
26
  import type { AccountChoice } from '../account-choice';
27
27
  import { modelName, shortModelName } from '../../agents/model-name';
28
28
  import { ModelMarks } from './ModelMarks';
29
+ import { SessionLine } from './SessionLine';
29
30
  import { favoritesFirst, searchModels, shortlistOf, wantsModelSearch } from '../logic/picker-entries';
30
31
  import { favoriteModelsOf, toggleFavoriteModel, useChatPreferences } from '../preferences';
31
32
  import { CONTEXT_OPTION, contextFraction, contextSegments, orderOptions, type ContextPart } from '../logic/context-usage';
32
33
  import { RUNTIME_MODES } from '../runtime-modes';
33
34
  import { localizedOption } from '../../agents/option-words';
34
- import { formatClock, formatWeekdayClock, isSameDay, formatPercent, formatTokens } from '@adecore/ui/format';
35
+ import { formatPercent, formatTokens } from '@adecore/ui/format';
35
36
  import { Icon, Menu, Tooltip, useNow } from '@adecore/ui';
36
37
  import { useChatHost } from '../../host';
37
- import { evenPace, paceText } from '../../usage/limit-groups';
38
38
  import { useUsageLimits } from '../../usage/limits';
39
39
 
40
40
  const MINUTE_MS = 60_000;
@@ -453,15 +453,6 @@ export function RunSettings({
453
453
  );
454
454
  }
455
455
 
456
- /* Red where a window is nearly spent, amber where it is worth knowing, as the usage bars have it. */
457
- function sessionTone(used: number): { bar: string; text: string } {
458
- return used >= 0.9
459
- ? { bar: 'bg-status-error', text: 'text-status-error' }
460
- : used >= 0.7
461
- ? { bar: 'bg-status-needs-you', text: 'text-status-needs-you' }
462
- : { bar: 'bg-text-muted', text: 'text-text-muted' };
463
- }
464
-
465
456
  /*
466
457
  * The account the next turn runs under, for the chat's own CLI. Before the first turn any of them;
467
458
  * after it one that reads the conversation, since the others would start it over, and a fork is how
@@ -529,37 +520,6 @@ function AccountRows({ choice, locked }: { choice: AccountChoice; locked(id: str
529
520
  });
530
521
  }
531
522
 
532
- function SessionLine({ window, now }: { window: UsageWindow; now: number }) {
533
- const { t } = useTranslation('agent-chat');
534
- const percent = Math.round(window.used * 100);
535
- const tone = sessionTone(window.used);
536
- const mark = evenPace(window, now)?.percent ?? null;
537
- const resetsAt = window.resetsAt;
538
- return (
539
- <span className="flex items-center gap-2 text-xs text-text-faint tabular-nums">
540
- <span aria-hidden className="relative h-1 w-14 shrink-0 rounded-full bg-surface-sunken">
541
- <span className={clsx('absolute inset-y-0 left-0 rounded-full', tone.bar)} style={{ width: `${Math.min(100, percent)}%` }} />
542
- {/* Where an even spread over the window would stand, as the usage bars mark it. */}
543
- {mark !== null && (
544
- <span
545
- className={clsx('absolute -inset-y-0.5 w-px -translate-x-1/2', mark <= percent ? 'bg-surface-raised' : 'bg-text-muted')}
546
- style={{ left: `${mark}%` }}
547
- />
548
- )}
549
- </span>
550
- <span aria-hidden className={tone.text}>
551
- {formatPercent(percent)}
552
- </span>
553
- <span className="sr-only">{paceText(window, now)}</span>
554
- {resetsAt !== null && (
555
- <span className="whitespace-nowrap">
556
- · {t('pickers.account.resets', { time: isSameDay(resetsAt, now) ? formatClock(resetsAt) : formatWeekdayClock(resetsAt) })}
557
- </span>
558
- )}
559
- </span>
560
- );
561
- }
562
-
563
523
  /* How full the context is, drawn small enough to sit in the pill beside the window's size. */
564
524
  function ContextRing({ usage }: { usage: ChatUsage }) {
565
525
  const fraction = contextFraction(usage);
@@ -0,0 +1,46 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import clsx from 'clsx';
3
+ import type { UsageWindow } from '@adecore/agent-contracts';
4
+ import { formatClock, formatPercent, formatWeekdayClock, isSameDay } from '@adecore/ui/format';
5
+ import { evenPace, paceText } from '../../usage/limit-groups';
6
+
7
+ /* Red where a window is nearly spent, amber where it is worth knowing, as the usage bars have it. */
8
+ function sessionTone(used: number): { bar: string; text: string } {
9
+ return used >= 0.9
10
+ ? { bar: 'bg-status-error', text: 'text-status-error' }
11
+ : used >= 0.7
12
+ ? { bar: 'bg-status-needs-you', text: 'text-status-needs-you' }
13
+ : { bar: 'bg-text-muted', text: 'text-text-muted' };
14
+ }
15
+
16
+ /* How much of an account's session is spent and when it resets, on the second line of a row that names the account. */
17
+ export function SessionLine({ window, now }: { window: UsageWindow; now: number }) {
18
+ const { t } = useTranslation('agent-chat');
19
+ const percent = Math.round(window.used * 100);
20
+ const tone = sessionTone(window.used);
21
+ const mark = evenPace(window, now)?.percent ?? null;
22
+ const resetsAt = window.resetsAt;
23
+ return (
24
+ <span className="flex items-center gap-2 text-xs text-text-faint tabular-nums">
25
+ <span aria-hidden className="relative h-1 w-14 shrink-0 rounded-full bg-surface-sunken">
26
+ <span className={clsx('absolute inset-y-0 left-0 rounded-full', tone.bar)} style={{ width: `${Math.min(100, percent)}%` }} />
27
+ {/* Where an even spread over the window would stand, as the usage bars mark it. */}
28
+ {mark !== null && (
29
+ <span
30
+ className={clsx('absolute -inset-y-0.5 w-px -translate-x-1/2', mark <= percent ? 'bg-surface-raised' : 'bg-text-muted')}
31
+ style={{ left: `${mark}%` }}
32
+ />
33
+ )}
34
+ </span>
35
+ <span aria-hidden className={tone.text}>
36
+ {formatPercent(percent)}
37
+ </span>
38
+ <span className="sr-only">{paceText(window, now)}</span>
39
+ {resetsAt !== null && (
40
+ <span className="whitespace-nowrap">
41
+ · {t('pickers.account.resets', { time: isSameDay(resetsAt, now) ? formatClock(resetsAt) : formatWeekdayClock(resetsAt) })}
42
+ </span>
43
+ )}
44
+ </span>
45
+ );
46
+ }
@@ -94,7 +94,10 @@ export default {
94
94
  continueOn: 'Continue on',
95
95
  continueOnAria: 'Continue on {{account}}',
96
96
  continueOnTip: 'Switches to {{account}}, which has the most room left, and resends the turn.',
97
- continueFailed: 'Could not continue on {{account}}'
97
+ continueFailed: 'Could not continue on {{account}}',
98
+ chooseAccount: 'Choose another account',
99
+ continueInForkAria: 'Continue on {{account}} in a fork',
100
+ inFork: 'in a fork'
98
101
  },
99
102
  contextMeter: {
100
103
  aria: '{{percent}}% of context used',
@@ -94,7 +94,10 @@ export default {
94
94
  continueOn: 'Verder op',
95
95
  continueOnAria: 'Verder op {{account}}',
96
96
  continueOnTip: 'Wisselt naar {{account}}, dat de meeste ruimte over heeft, en verstuurt de turn opnieuw.',
97
- continueFailed: 'Kon niet verder op {{account}}'
97
+ continueFailed: 'Kon niet verder op {{account}}',
98
+ chooseAccount: 'Kies een ander account',
99
+ continueInForkAria: 'Verder op {{account}} in een fork',
100
+ inFork: 'in een fork'
98
101
  },
99
102
  contextMeter: {
100
103
  aria: '{{percent}}% van de context gebruikt',