@contraagent/ui 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +1 -1
  2. package/custom-elements.json +104 -78
  3. package/dist/adapters/journal-columns.d.ts +2 -0
  4. package/dist/adapters/journal-columns.d.ts.map +1 -1
  5. package/dist/adapters/journal-columns.js +18 -5
  6. package/dist/adapters/journal-columns.js.map +1 -1
  7. package/dist/adapters/kpi-tiles.d.ts +8 -3
  8. package/dist/adapters/kpi-tiles.d.ts.map +1 -1
  9. package/dist/adapters/kpi-tiles.js +35 -15
  10. package/dist/adapters/kpi-tiles.js.map +1 -1
  11. package/dist/adapters/transcript/sections.d.ts +5 -0
  12. package/dist/adapters/transcript/sections.d.ts.map +1 -1
  13. package/dist/adapters/transcript/sections.js +7 -1
  14. package/dist/adapters/transcript/sections.js.map +1 -1
  15. package/dist/contraagent-ui.js +689 -700
  16. package/dist/elements/lists/ca-contracts.d.ts +3 -4
  17. package/dist/elements/lists/ca-contracts.d.ts.map +1 -1
  18. package/dist/elements/lists/ca-contracts.js +25 -19
  19. package/dist/elements/lists/ca-contracts.js.map +1 -1
  20. package/dist/elements/overview/ca-kpis.d.ts +3 -1
  21. package/dist/elements/overview/ca-kpis.d.ts.map +1 -1
  22. package/dist/elements/overview/ca-kpis.js +7 -2
  23. package/dist/elements/overview/ca-kpis.js.map +1 -1
  24. package/dist/elements/overview/ca-model-usage.d.ts +2 -1
  25. package/dist/elements/overview/ca-model-usage.d.ts.map +1 -1
  26. package/dist/elements/overview/ca-model-usage.js +3 -2
  27. package/dist/elements/overview/ca-model-usage.js.map +1 -1
  28. package/dist/elements/overview/ca-overview.d.ts +1 -5
  29. package/dist/elements/overview/ca-overview.d.ts.map +1 -1
  30. package/dist/elements/overview/ca-overview.js +53 -77
  31. package/dist/elements/overview/ca-overview.js.map +1 -1
  32. package/dist/elements/overview/ca-pipeline-graph.d.ts +8 -1
  33. package/dist/elements/overview/ca-pipeline-graph.d.ts.map +1 -1
  34. package/dist/elements/overview/ca-pipeline-graph.js +11 -6
  35. package/dist/elements/overview/ca-pipeline-graph.js.map +1 -1
  36. package/dist/elements/overview/ca-run-driver.d.ts.map +1 -1
  37. package/dist/elements/overview/ca-run-driver.js +6 -5
  38. package/dist/elements/overview/ca-run-driver.js.map +1 -1
  39. package/dist/elements/overview/pipeline-graph/strings.js +3 -3
  40. package/dist/elements/overview/pipeline-graph/strings.js.map +1 -1
  41. package/dist/elements/shared/ca-definition.d.ts.map +1 -1
  42. package/dist/elements/shared/ca-definition.js +1 -2
  43. package/dist/elements/shared/ca-definition.js.map +1 -1
  44. package/dist/elements/shell/ca-hosts.d.ts.map +1 -1
  45. package/dist/elements/shell/ca-hosts.js +1 -2
  46. package/dist/elements/shell/ca-hosts.js.map +1 -1
  47. package/dist/elements/shell/ca-monitor.d.ts +3 -1
  48. package/dist/elements/shell/ca-monitor.d.ts.map +1 -1
  49. package/dist/elements/shell/ca-monitor.js +20 -9
  50. package/dist/elements/shell/ca-monitor.js.map +1 -1
  51. package/dist/elements/shell/ca-run-picker.js +1 -1
  52. package/dist/elements/shell/ca-run-picker.js.map +1 -1
  53. package/dist/elements/shell/ca-theme-switch.d.ts.map +1 -1
  54. package/dist/elements/shell/ca-theme-switch.js +6 -1
  55. package/dist/elements/shell/ca-theme-switch.js.map +1 -1
  56. package/dist/elements/task/ca-task-view.js +2 -2
  57. package/dist/elements/task/ca-task-view.js.map +1 -1
  58. package/dist/elements/task/ca-transcript.d.ts +3 -0
  59. package/dist/elements/task/ca-transcript.d.ts.map +1 -1
  60. package/dist/elements/task/ca-transcript.js +9 -2
  61. package/dist/elements/task/ca-transcript.js.map +1 -1
  62. package/dist/elements/task/header.d.ts +3 -0
  63. package/dist/elements/task/header.d.ts.map +1 -1
  64. package/dist/elements/task/header.js +21 -12
  65. package/dist/elements/task/header.js.map +1 -1
  66. package/dist/elements/task/panes/io.d.ts +1 -1
  67. package/dist/elements/task/panes/io.d.ts.map +1 -1
  68. package/dist/elements/task/panes/io.js +14 -21
  69. package/dist/elements/task/panes/io.js.map +1 -1
  70. package/dist/elements/trends/ca-trends.d.ts.map +1 -1
  71. package/dist/elements/trends/ca-trends.js +2 -1
  72. package/dist/elements/trends/ca-trends.js.map +1 -1
  73. package/dist/locales/ja.d.ts +14 -27
  74. package/dist/locales/ja.d.ts.map +1 -1
  75. package/dist/locales/ja.js +14 -27
  76. package/dist/locales/ja.js.map +1 -1
  77. package/dist/words/quota.d.ts +2 -0
  78. package/dist/words/quota.d.ts.map +1 -1
  79. package/dist/words/quota.js +4 -0
  80. package/dist/words/quota.js.map +1 -1
  81. package/dist/words/run-line.d.ts +5 -3
  82. package/dist/words/run-line.d.ts.map +1 -1
  83. package/dist/words/run-line.js +6 -4
  84. package/dist/words/run-line.js.map +1 -1
  85. package/dist/words/trends.d.ts +2 -2
  86. package/dist/words/trends.d.ts.map +1 -1
  87. package/dist/words/trends.js +5 -3
  88. package/dist/words/trends.js.map +1 -1
  89. package/package.json +3 -3
  90. package/src/adapters/journal-columns.ts +19 -5
  91. package/src/adapters/kpi-tiles.ts +37 -16
  92. package/src/adapters/transcript/sections.ts +10 -2
  93. package/src/elements/lists/ca-contracts.ts +25 -19
  94. package/src/elements/overview/ca-kpis.ts +4 -2
  95. package/src/elements/overview/ca-model-usage.ts +3 -2
  96. package/src/elements/overview/ca-overview.ts +49 -79
  97. package/src/elements/overview/ca-pipeline-graph.ts +11 -6
  98. package/src/elements/overview/ca-run-driver.ts +5 -6
  99. package/src/elements/overview/pipeline-graph/strings.ts +3 -3
  100. package/src/elements/shared/ca-definition.ts +1 -2
  101. package/src/elements/shell/ca-hosts.ts +1 -2
  102. package/src/elements/shell/ca-monitor.ts +19 -8
  103. package/src/elements/shell/ca-run-picker.ts +1 -1
  104. package/src/elements/shell/ca-theme-switch.ts +6 -1
  105. package/src/elements/task/ca-task-view.ts +2 -2
  106. package/src/elements/task/ca-transcript.ts +7 -2
  107. package/src/elements/task/header.ts +23 -13
  108. package/src/elements/task/panes/io.ts +14 -21
  109. package/src/elements/trends/ca-trends.ts +2 -1
  110. package/src/locales/ja.ts +14 -27
  111. package/src/words/quota.ts +6 -0
  112. package/src/words/run-line.ts +8 -5
  113. package/src/words/trends.ts +5 -3
@@ -102,7 +102,9 @@ const EMPTY_LAYER: LayerGraph = {
102
102
  * of passes or by status (`<cae-mark-matrix>`). The breadcrumb, shown on every layer
103
103
  * below the top (a loop named by its file and line), and Escape go back up. On a
104
104
  * narrow screen the graph runs top to bottom. "Graph | Table" changes the view; in a
105
- * loop's passes, "Graph" opens the layer the loop is in, with its box selected.
105
+ * loop's passes, "Graph" opens the layer the loop is in, with its box selected. With
106
+ * `controls` false, the graph is only shown: no outline, no "Show all inside" and
107
+ * "Hide all inside", no zoom buttons and no "Graph | Table".
106
108
  * A height set on the element (or given it by a flex or grid container) is the
107
109
  * graph's, less the outline's line above it: the drawing fits in it.
108
110
  *
@@ -119,6 +121,7 @@ export class CaPipelineGraph extends LitElement {
119
121
  journal: { attribute: false },
120
122
  run: {},
121
123
  live: { type: Boolean },
124
+ controls: { type: Boolean },
122
125
  layer: {},
123
126
  unit: {},
124
127
  sel: {},
@@ -139,6 +142,8 @@ export class CaPipelineGraph extends LitElement {
139
142
  run = '';
140
143
  /** The run is running: tasks still pending are waiting; once it is over, they did not run. */
141
144
  live = false;
145
+ /** The outline, "Show all inside" and "Hide all inside", the zoom buttons and "Graph | Table"; false for a graph that is only shown. */
146
+ controls = true;
142
147
  /** The layer shown: its segments (function names, then `loop:<line>` for a loop's passes) joined by `/`; '' for the layer the graph opens on. */
143
148
  layer = '';
144
149
  /** The unit highlighted while nothing is hovered or focused. */
@@ -436,8 +441,8 @@ export class CaPipelineGraph extends LitElement {
436
441
  // The graph keeps its place in one template whichever it is, and whether there is an
437
442
  // outline: a change between them (the first size report, a resize, a plan that gains a
438
443
  // function) keeps the graph's element, and with it the focus, the zoom and the pan.
439
- const side = this.wide && this.tree.length > 0;
440
- const part = !this.tree.length
444
+ const side = this.controls && this.wide && this.tree.length > 0;
445
+ const part = !this.controls || !this.tree.length
441
446
  ? nothing
442
447
  : side
443
448
  ? outline(this.oh, this.tree)
@@ -451,10 +456,10 @@ export class CaPipelineGraph extends LitElement {
451
456
  return html`<cae-layered-graph .graph=${this.input} .states=${this.states} .selected=${this.sel || null} .highlight=${this.unit || null}
452
457
  .order=${this.prevOrder} key=${this.segments.join('/')} direction=${this.down || this.tall ? 'down' : 'across'} .matrix=${this.matrixIn}
453
458
  .related=${this.lights} .strings=${graphStrings(!!parentLayer(this.segments, this.auto), functions)} .matrixStrings=${matrixStrings()}
454
- .renderIcon=${renderIcon} .iconStyles=${iconStyles}
459
+ .renderIcon=${renderIcon} .iconStyles=${iconStyles} .controls=${this.controls}
455
460
  @cae-select=${this.onSelect} @cae-activate=${this.onActivate} @cae-drill=${this.onDrill} @cae-view-change=${this.onView} @cae-toggle=${this.onToggle}
456
461
  @cae-sort=${(e: CustomEvent<{ id: string }>) => (this.sort = e.detail.id as MatrixSort)}>
457
- <div slot="crumbs">${crumbs(this.h, splitLayer(this.segments).loop ? null : openControls(this.openMode(), (m) => this.setMode(m)))}${missingLine(this.missing)}${this.segments.join('/') === this.auto.join('/') ? html`${forecastNote(this.plan, this.forecast)}${unawaitedLine(this.h)}` : nothing}</div>
462
+ <div slot="crumbs">${crumbs(this.h, splitLayer(this.segments).loop || !this.controls ? null : openControls(this.openMode(), (m) => this.setMode(m)))}${missingLine(this.missing)}${this.segments.join('/') === this.auto.join('/') ? html`${forecastNote(this.plan, this.forecast)}${unawaitedLine(this.h)}` : nothing}</div>
458
463
  ${s ? inspector(this.h, s) : nothing}
459
464
  </cae-layered-graph>`;
460
465
  }
@@ -462,7 +467,7 @@ export class CaPipelineGraph extends LitElement {
462
467
  /** The graph's own width: the element's, less the outline's column beside it. */
463
468
  private graphWidth(): number {
464
469
  const w = this.clientWidth || 1200;
465
- return this.tree.length && w >= OUTLINE_WIDE ? w - OUTLINE_COLUMN : w;
470
+ return this.controls && this.tree.length && w >= OUTLINE_WIDE ? w - OUTLINE_COLUMN : w;
466
471
  }
467
472
 
468
473
  /** The place keys on the way to the layer shown (the functions of its path). */
@@ -1,9 +1,9 @@
1
- import { elapsedS, result, runActivity, type HostSummary, type Plan, type Procs, type RunSummary } from '@contraagent/api';
1
+ import { result, runActivity, runIdOf, type HostSummary, type Plan, type Procs, type RunSummary } from '@contraagent/api';
2
2
  import { localized, msg, str } from '@lit/localize';
3
3
  import { LitElement, css, html, nothing } from 'lit';
4
4
  import { customElement, property } from 'lit/decorators.js';
5
5
  import { starts } from '@contraagent/elements';
6
- import { facts, fmtDur, localTime, sentence } from '../../words/format.js';
6
+ import { facts, localTime, sentence } from '../../words/format.js';
7
7
  import { hostFact, lastReport, noReportLabel } from '../../words/hosts.js';
8
8
  import { preflightLabel, preflightStoppedCause, preflightStoppedTitle, preflightTip } from '../../words/preflight.js';
9
9
  import { preflightRows } from '../../adapters/preflight.js';
@@ -80,7 +80,6 @@ export class CaRunDriver extends LitElement {
80
80
  // Its host stopped reporting (a hub): not known to run, not known to have ended.
81
81
  const quiet = runActivity(r) === 'unresponsive';
82
82
  const [title, tone]: [string, Tone] = quiet ? [sentence(noReportLabel()), 'attention'] : this.stateTitle();
83
- const took = quiet ? null : elapsedS(r);
84
83
  return html`<div class="drv off">
85
84
  <div class="drv-ic" data-tone=${toneAttr(tone)} aria-hidden="true">${MARK[tone]}</div>
86
85
  <div>
@@ -89,8 +88,8 @@ export class CaRunDriver extends LitElement {
89
88
  this.plan.preflight?.stopped?.length ? preflightStoppedCause(this.plan.preflight.stopped) : '',
90
89
  quiet ? lastReport(this.host) : '',
91
90
  !r?.last_ts ? msg('No activity') : quiet ? msg(str`Last activity ${localTime(r.last_ts, true)}`) : msg(str`Ended ${localTime(r.last_ts, true)}`),
92
- took == null ? '' : msg(str`Took ${fmtDur(took)}`),
93
- r?.run_group ? msg(str`Run group ${r.run_group}`) : '',
91
+ // How long it took is in the header's run line; a run group named as the run is no fact of its own.
92
+ r?.run_group && r.run_group !== runIdOf(r) ? msg(str`Run group ${r.run_group}`) : '',
94
93
  quiet ? hostFact(r?.host_id ?? '') : '',
95
94
  )}</div>
96
95
  ${this.preflight()}
@@ -101,7 +100,7 @@ export class CaRunDriver extends LitElement {
101
100
  const since = starts.at(`drv:${p.pid ?? ''}`, Number.isNaN(started) ? 1 : (Date.now() - started) / 1000);
102
101
  const rows: [string, string | number][] = [
103
102
  [msg('PID'), p.pid ?? '—'],
104
- [msg('Run group'), p.run_group],
103
+ ...(r && p.run_group === runIdOf(r) ? [] : [[msg('Run group'), p.run_group] as [string, string]]),
105
104
  [msg('Running at once (limit)'), p.max_workers || '—'],
106
105
  [msg('Started'), localTime(p.started_at, true)],
107
106
  ];
@@ -29,12 +29,12 @@ export function graphStrings(up: boolean, functions: boolean): Omit<LayeredGraph
29
29
  inspector: msg('Selected item'),
30
30
  stubTip: msg('Outside this layer'),
31
31
  more: (n) => msg(str`+${n}`),
32
- badge: msg('number of passes'),
32
+ badge: msg('Passes'),
33
33
  solidEdge: lineageWord('given_from'),
34
34
  dottedEdge: lineageWord('after'),
35
35
  dashedEdge: lineageWord('created_from'),
36
- cycle: msg('next pass'),
37
- drill: msg('has a level below'),
36
+ cycle: msg('Next pass'),
37
+ drill: msg('Has a level below'),
38
38
  open: msg('Show inside'),
39
39
  close: msg('Hide inside'),
40
40
  iconNames: { agent: msg('agent task'), python: msg('Python task') },
@@ -182,9 +182,8 @@ export class CaDefinition extends LitElement {
182
182
  };
183
183
  return html`<div class="df">
184
184
  <div class="df-hd">
185
- <button type="button" class="df-sum" aria-expanded=${isOpen ? 'true' : 'false'} @click=${toggle}>
185
+ <button type="button" class="df-sum" aria-expanded=${isOpen ? 'true' : 'false'} title=${isRole ? msg('Role of this kind') : nothing} @click=${toggle}>
186
186
  <span class="tag">${f.part}</span><span class="jmono">${f.path}</span>
187
- ${isRole ? html`<span class="ct-chip">${msg('Role of this kind')}</span>` : nothing}
188
187
  </button>
189
188
  ${isMd
190
189
  ? html`<button type="button" class="df-tog" @click=${flip}>${showSource ? msg('Show rendered') : msg('Show source')}</button>`
@@ -77,8 +77,7 @@ export class CaHosts extends LitElement {
77
77
  </section>
78
78
  ${quota.length
79
79
  ? html`<section class="panel c12 enter">
80
- <h3 title=${msg('Account-wide | read at each agent session start')}>
81
- ${msg('Subscription usage')}<span class="r">${msg('Per account, first → last reading')}</span></h3>
80
+ <h3 title=${msg('Account-wide | first and last reading per account | read at each agent session start')}>${msg('Subscription usage')}</h3>
82
81
  <ca-quota .accounts=${quota} .rests=${quota.flatMap((a) => a.rests ?? [])} live></ca-quota>
83
82
  </section>`
84
83
  : nothing}
@@ -9,6 +9,7 @@ import {
9
9
  runActivity,
10
10
  runIdOf,
11
11
  runRow,
12
+ tryCounts,
12
13
  type MonitorSnapshot,
13
14
  type RunDerived,
14
15
  } from '@contraagent/api';
@@ -24,7 +25,8 @@ import { ViewRouter, type ViewRouteCause, type ViewRouterMode } from '../../core
24
25
  import { LocaleController } from '../../controllers/locale.js';
25
26
  import { ShortcutsController, isShortcutScope, type ShortcutScope } from '../../controllers/shortcuts.js';
26
27
  import { ThemeController } from '../../controllers/theme.js';
27
- import { runLine, runLineAccounts, triesLine } from '../../words/index.js';
28
+ import { runLine, runLineAccounts, shownTasksLine, shownTriesLine } from '../../words/index.js';
29
+ import { filterJournal, filterTasks } from '../../adapters/filters.js';
28
30
  import { runMark } from '../../words/run-line.js';
29
31
  import { exportTip, exportedLabel } from '../../words/export.js';
30
32
  import { apiErrorLine } from '../../words/api-error.js';
@@ -477,9 +479,9 @@ export class CaMonitor extends LitElement {
477
479
  <span class="rs" title=${summary ? accountsTip(runLineAccounts(snap.state, summary)) : ''}>${summary ? runLine(snap.state, summary, derived) : ''}</span>`;
478
480
  }
479
481
 
480
- /** The run button's label on the trends page: its name, and what it shows. */
482
+ /** The run button's label on the trends page: its name, what it shows in its tooltip. */
481
483
  private trendsLabel() {
482
- return html`<span class="dot" data-tone="neutral" aria-hidden="true"></span><span class="rn">${trendsTitle()}</span><span class="rs">${trendsTip()}</span>`;
484
+ return html`<span class="dot" data-tone="neutral" aria-hidden="true"></span><span class="rn" title=${trendsTip()}>${trendsTitle()}</span>`;
483
485
  }
484
486
 
485
487
  /** The run button's label on the hosts page: the hosts in one line, attention while a host has no report. */
@@ -514,8 +516,7 @@ export class CaMonitor extends LitElement {
514
516
  private renderBar(d: RunDerived) {
515
517
  const { viewRoute } = this;
516
518
  if (viewRoute.tab !== 'tasks' && viewRoute.tab !== 'journal') return nothing;
517
- const { counts, kinds } = d;
518
- const total = counts.total;
519
+ const { kinds } = d;
519
520
  return html`<div class="bar">
520
521
  <select title=${msg('Kind')} aria-label=${msg('Kind')} @change=${(e: Event) => this.navigate({ kind: (e.target as HTMLSelectElement).value })}>
521
522
  <option value="" ?selected=${!viewRoute.kind}>${msg('All kinds')}</option>
@@ -523,11 +524,21 @@ export class CaMonitor extends LitElement {
523
524
  </select>
524
525
  <input placeholder=${msg('Filter')} aria-label=${msg('Text filter')} size="22" title=${this.shortcutScope !== 'off' ? msg('Text filter (/)') : msg('Text filter')} .value=${this.qDraft ?? viewRoute.q}
525
526
  @input=${(e: Event) => this.setQ((e.target as HTMLInputElement).value)} />
526
- <span class="meta">${viewRoute.tab === 'journal'
527
- ? triesLine(d.tries)
528
- : total === 1 ? msg('1 task') : msg(str`${total} tasks`)}</span>
527
+ <span class="meta" aria-live="polite">${this.shownLine(d)}</span>
529
528
  </div>`;
530
529
  }
530
+
531
+ /** While a filter is on, how many of the tab's rows it shows; the tab's badge counts them all. */
532
+ private shownLine(d: RunDerived): string {
533
+ const { viewRoute } = this;
534
+ const q = this.qDraft ?? viewRoute.q;
535
+ if (viewRoute.tab === 'journal') {
536
+ if (!viewRoute.kind && !q && !viewRoute.stop) return '';
537
+ return shownTriesLine(tryCounts(filterJournal(this.snap.state.journal, viewRoute.kind, viewRoute.stop, q)), d.tries.tries);
538
+ }
539
+ if (!viewRoute.kind && !q) return '';
540
+ return shownTasksLine(filterTasks(d.rows, viewRoute.kind, q).length, d.counts.total);
541
+ }
531
542
  }
532
543
 
533
544
  declare global {
@@ -270,7 +270,7 @@ export class CaRunPicker extends LitElement {
270
270
  </span>`
271
271
  : page === 'hosts'
272
272
  ? html`<span aria-hidden="true">⊞</span><span class="nm">${msg('Hosts')}</span><span class="when page">${hostsLine(this.hosts)}</span>`
273
- : html`<span aria-hidden="true">▥</span><span class="nm">${trendsTitle()}</span><span class="when page">${trendsTip()}</span>`}
273
+ : html`<span aria-hidden="true">▥</span><span class="nm" title=${trendsTip()}>${trendsTitle()}</span>`}
274
274
  </div>`;
275
275
  }
276
276
  }
@@ -38,6 +38,11 @@ export class CaThemeSwitch extends LitElement {
38
38
  padding: 2px 10px 2px 7px; font-size: 11px; color: var(--text-muted); cursor: pointer; }
39
39
  .trig:hover, .trig[aria-expanded='true'] { color: var(--text); border-color: var(--line2); }
40
40
  .trig svg { width: 13px; height: 13px; }
41
+ /* on a phone the icon alone: the word stays the button's accessible name */
42
+ @media (max-width: 480px) {
43
+ .trig { padding: 4px 6px; }
44
+ .trig .lbl { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
45
+ }
41
46
  .menu { position: absolute; right: 0; top: calc(100% + 8px); min-width: 236px; padding: 6px; z-index: 50;
42
47
  background: var(--panel); border: 1px solid var(--line2); border-radius: 12px;
43
48
  box-shadow: 0 18px 48px color-mix(in srgb, var(--shadow) 65%, transparent); animation: enter 0.16s var(--ease) both; }
@@ -148,7 +153,7 @@ export class CaThemeSwitch extends LitElement {
148
153
  return html`<button type="button" class="trig" aria-haspopup="menu" aria-expanded=${this.open ? 'true' : 'false'} aria-controls="menu"
149
154
  title=${msg(str`Theme: ${themeLabel(this.value)}`)} @click=${() => (this.open = !this.open)} @keydown=${(e: KeyboardEvent) => this.onTrigKey(e)}>
150
155
  <svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.25" fill="none" stroke="currentColor" stroke-width="1.5" /><path d="M8 1.75a6.25 6.25 0 0 1 0 12.5z" fill="currentColor" /></svg>
151
- ${msg('Theme')}
156
+ <span class="lbl">${msg('Theme')}</span>
152
157
  </button>
153
158
  ${this.open
154
159
  ? html`<div id="menu" class="menu" role="menu" aria-label=${msg('Theme')} @keydown=${(e: KeyboardEvent) => this.onMenuKey(e)}>
@@ -12,7 +12,7 @@ import './ca-transcript.js';
12
12
  import type { CaTranscript } from './ca-transcript.js';
13
13
  import { tagStyles } from '@contraagent/elements/transcript';
14
14
  import { isPy, type TaskContext } from './context.js';
15
- import { taskHeader } from './header.js';
15
+ import { taskHeader, taskWho } from './header.js';
16
16
  import { noTranscript } from './panes/transcript.js';
17
17
  import { inputPane, outputPane } from './panes/io.js';
18
18
  import { definitionPane } from './panes/definition.js';
@@ -112,7 +112,7 @@ export class CaTaskView extends LitElement {
112
112
  const view = this.shownView(d);
113
113
  return html`${taskHeader(c, key, (q, nth) => this.revealSearch(q, nth))} ${this.renderViews(d, view)}
114
114
  ${d.stream
115
- ? html`<ca-transcript .run=${this.run} .stream=${d.stream} base-url=${this.baseUrl} .client=${this.client} .live=${c.live}
115
+ ? html`<ca-transcript .run=${this.run} .stream=${d.stream} base-url=${this.baseUrl} .client=${this.client} .live=${c.live} .shownWho=${taskWho(c)}
116
116
  .rows=${d.rows} .round=${this.round} mode=${view === 'transcript' ? 'full' : view === 'input' ? 'tabs' : 'none'}
117
117
  @ca-rounds=${(e: CustomEvent<RoundsChangedDetail>) => {
118
118
  this.launches = e.detail.launches;
@@ -8,6 +8,7 @@ import { customElement, property, query, state } from 'lit/decorators.js';
8
8
  import { keyed } from 'lit/directives/keyed.js';
9
9
  import { classifyRounds } from '../../adapters/transcript/rounds.js';
10
10
  import { transcriptSections } from '../../adapters/transcript/sections.js';
11
+ import type { Who } from '../../adapters/transcript/who.js';
11
12
  import { StreamController } from '../../controllers/stream.js';
12
13
  import { emit, type RoundInfo, type RoundsChangedDetail } from '../../core/events.js';
13
14
  import { getLocale } from '../../core/i18n.js';
@@ -19,6 +20,7 @@ import { modelSwitchTip } from '../../words/hops.js';
19
20
 
20
21
  /** The transcript's words, in the reader's language (call at render time). */
21
22
  const strings = (): TranscriptStrings => ({
23
+ // the picker's accessible name: its buttons name each session
22
24
  sections: msg('Agent sessions'),
23
25
  sectionsTip: [
24
26
  `${msg('Retry')}: ${retryTip()}`,
@@ -90,6 +92,8 @@ export class CaTranscript extends LitElement {
90
92
  @property({ attribute: false }) round: number | null = null;
91
93
  /** The task's journal rows (finished tries): each round then says how it ended. */
92
94
  @property({ attribute: false }) rows: JournalRow[] = [];
95
+ /** Who the page names already (a task's header): each round's head shows only the coding agent, model and role that differ. */
96
+ @property({ attribute: false }) shownWho: Who | null = null;
93
97
  /** `full`; `tabs`: only the round picker (another view of the task is shown); `none`: hidden. */
94
98
  @property({ reflect: true }) mode: 'full' | 'tabs' | 'none' = 'full';
95
99
 
@@ -144,14 +148,15 @@ export class CaTranscript extends LitElement {
144
148
  /** The rounds and their sections, drawn again only when the transcript, the rows or the language changed. */
145
149
  private sections(): { rounds: RoundInfo[]; sections: TranscriptSection[] } {
146
150
  const active = this.live && this.feed.tailing;
147
- const key = [this.feed.version, this.rows, this.run, active, getLocale()];
151
+ const w = this.shownWho;
152
+ const key = [this.feed.version, this.rows, this.run, active, getLocale(), w?.agent, w?.model, w?.role];
148
153
  if (this.drawn?.key.every((k, i) => k === key[i])) return this.drawn;
149
154
  const t = this.feed.model;
150
155
  const rounds = classifyRounds(
151
156
  t.rounds.map((r) => r.launch),
152
157
  this.rows,
153
158
  );
154
- this.drawn = { key, rounds, sections: transcriptSections(t, rounds, { run: this.run, rows: this.rows, active }) };
159
+ this.drawn = { key, rounds, sections: transcriptSections(t, rounds, { run: this.run, rows: this.rows, active, shownWho: w }) };
155
160
  return this.drawn;
156
161
  }
157
162
 
@@ -8,7 +8,7 @@ import { pill } from '@contraagent/elements/primitives';
8
8
  import { facts, fmtDur, kindUnit, listText, localTime, tokenCount, unreadEvents, usageUnknown, usageUnknownWords, usd } from '../../words/format.js';
9
9
  import { attemptNames, deferredText, changedFiles, changedFilesTip, changeLabel, credentialLabel, credentialTip, locksLabel, locksTip, modelSwitch, outcomeLabel, priorityText, priorityTip, replayedLabel, replayedTip, stopLabel, workingDirectoryText, workingDirectoryTip, type ChangeKind } from '../../words/index.js';
10
10
  import { contextWarnedText, contextWarnedTip, peakContextLine, peakContextTip, peakTry } from '../../words/context.js';
11
- import { readingShort, readingTip } from '../../words/quota.js';
11
+ import { readingChange, readingShort, readingTip } from '../../words/quota.js';
12
12
  import { safetyStopCount, safetyStopLine, safetyStopTip } from '../../words/safety.js';
13
13
  import { customColumnTip, customValues } from '../../words/row-extras.js';
14
14
  import { divergenceTip, divergenceWord, rerunLine, rerunOf } from '../../words/rerun.js';
@@ -92,7 +92,7 @@ function verdict(c: TaskContext): [text: string, tone: Tone] {
92
92
  }
93
93
 
94
94
  /** The model of the latest round, as its launch named it (`--model`). */
95
- function streamModel(c: TaskContext): string {
95
+ function streamModel(c: Pick<TaskContext, 'launches'>): string {
96
96
  for (let i = c.launches.length - 1; i >= 0; i--) {
97
97
  const l = c.launches[i];
98
98
  const m = l ? launchWho(l).model : '';
@@ -105,6 +105,19 @@ function streamModel(c: TaskContext): string {
105
105
  * The header of task `key`. `onSearch(query, nth)` is called when the reader
106
106
  * picks one of its web searches (the nth with that query).
107
107
  */
108
+ /** Who ran the task, as its header names it: the running session's agent, model and role, else the latest attempt's. */
109
+ export function taskWho(c: Pick<TaskContext, 'detail' | 'launches'>): Who {
110
+ const { rows, launches } = c.detail;
111
+ const lastRow = rows.slice().sort((a, b) => String(a.ts).localeCompare(String(b.ts)))[rows.length - 1];
112
+ const lc0 = launches[0];
113
+ return {
114
+ agent: agentLabel(lc0?.cmd ? lc0.cmd.split(' ')[0] : lastRow?.agent),
115
+ // The journal may not name the model (an agent that does not report it): fall back to the one the transcript shows.
116
+ model: lc0?.model || lastRow?.model || streamModel(c),
117
+ role: lc0?.role || '',
118
+ };
119
+ }
120
+
108
121
  export function taskHeader(c: TaskContext, key: string, onSearch: (query: string, nth: number) => void): TemplateResult {
109
122
  const d = c.detail;
110
123
  const live = c.live;
@@ -112,14 +125,7 @@ export function taskHeader(c: TaskContext, key: string, onSearch: (query: string
112
125
  const py = isPy(d);
113
126
  const session = hadSession(d);
114
127
  const byTs = rows.slice().sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
115
- const lastRow = byTs[byTs.length - 1];
116
- const lc0 = launches[0];
117
- const who: Who = {
118
- agent: agentLabel(lc0?.cmd ? lc0.cmd.split(' ')[0] : lastRow?.agent),
119
- // The journal may not name the model (an agent that does not report it): fall back to the one the transcript shows.
120
- model: lc0?.model || lastRow?.model || streamModel(c),
121
- role: lc0?.role || '',
122
- };
128
+ const who = taskWho(c);
123
129
  const otherModels = session ? [...new Set(byTs.map((r) => r.model).filter((x): x is string => !!x && x !== who.model))] : [];
124
130
  const first = rows[0];
125
131
  const tries = rows.filter(isLaunched);
@@ -285,10 +291,14 @@ function quotaLine(c: TaskContext, tries: JournalRow[]) {
285
291
  const start = first ? tryQuota(first, readings).start : null;
286
292
  if (!start || !last) return nothing;
287
293
  const next = tryQuota(last, readings).next;
288
- // The identity chip names the account; both readings are that account's.
294
+ // The identity chip names the account; both readings are that account's. One line: each window from the first reading to the next.
295
+ const tip = [
296
+ msg(str`Usage at session start: ${readingShort(start)}`),
297
+ readingTip(start),
298
+ ...(next ? ['', msg(str`Usage after this task: ${readingShort(next)}`), readingTip(next)] : []),
299
+ ].join('\n');
289
300
  return html`<div class="spend quota">
290
- <span title=${readingTip(start)}>${msg(str`Usage at session start: ${readingShort(start)}`)}</span>
291
- ${next ? html`<span title=${readingTip(next)}>${msg(str`Usage after this task: ${readingShort(next)}`)}</span>` : nothing}
301
+ <span title=${tip}>${msg(str`Usage: ${readingChange(start, next)}`)}</span>
292
302
  </div>`;
293
303
  }
294
304
 
@@ -1,6 +1,6 @@
1
1
  // The input and output panes of `ca-task-view`: what the task read (parameters,
2
2
  // files, earlier tasks, the prompt) and what it wrote (files against their
3
- // contracts, the outcome, tokens and cost, later tasks).
3
+ // contracts, tokens and cost, later tasks).
4
4
  import {
5
5
  isLaunched,
6
6
  isPython,
@@ -19,10 +19,10 @@ import { msg, str } from '@lit/localize';
19
19
  import { html, nothing, type TemplateResult } from 'lit';
20
20
  import { pill } from '@contraagent/elements/primitives';
21
21
  import '@contraagent/elements/json-tree/define';
22
- import { facts, fmtDur, kindUnit, tokenCount, usd } from '../../../words/format.js';
22
+ import { facts, kindUnit, tokenCount, usd } from '../../../words/format.js';
23
23
  import { attemptLabel, attemptNames, credentialLabel, credentialTip, outcomeLabel, stopLabel } from '../../../words/index.js';
24
24
  import { accountReading, readingShort, readingTip } from '../../../words/quota.js';
25
- import { outcomeMark, outcomeTone, stopTone } from '../../../lib/tone.js';
25
+ import { outcomeMark, outcomeTone } from '../../../lib/tone.js';
26
26
  import { executorIcon } from '../../../words/executor.js';
27
27
  import { promptText } from '../../shared/prompt-text.js';
28
28
  import { contractChip } from '../../shared/contract-parts.js';
@@ -31,7 +31,7 @@ import { taskChip } from '../../shared/task-chip.js';
31
31
  import { inputView, notPassed, type EarlierTask, type InputFile } from '../inputs.js';
32
32
  import { roundLabel, roundWhy } from '../../../words/rounds.js';
33
33
  import { inputFileCheckWord, inputRootPath, inputRootTip, lineageTip, lineageWord } from '../../../words/lineage.js';
34
- import { correctionsOf, hadSession, isPy, producerOf, stateOf, taskLink, type TaskContext } from '../context.js';
34
+ import { hadSession, isPy, producerOf, stateOf, taskLink, type TaskContext } from '../context.js';
35
35
 
36
36
  const valueText = (v: unknown): string => (typeof v === 'string' ? v : JSON.stringify(v) ?? String(v));
37
37
  const sameJson = (a: unknown, b: unknown): boolean => {
@@ -56,6 +56,8 @@ export function inputPane(c: TaskContext): TemplateResult {
56
56
  const params = Object.entries(io.params);
57
57
  const { files, earlier } = inputView(io);
58
58
  const source = (v: unknown) => io.upstream.find((u) => u.outputs.some((o) => o.file.exists && sameJson(o.file.content, v)));
59
+ // An earlier task a parameter names ("From prepare · a") is given its output there: its row below does not say it again.
60
+ const givenAbove = new Set(params.map(([, v]) => source(v)?.key).filter((k): k is string => !!k));
59
61
  return html`<h4 class="sect-h">${msg('Parameters')}</h4>
60
62
  ${params.length
61
63
  ? html`<table class="ct-fields params">
@@ -76,7 +78,7 @@ export function inputPane(c: TaskContext): TemplateResult {
76
78
  ${files.map((f) => inputCard(c, f))}`
77
79
  : nothing}
78
80
  <h4 class="sect-h">${msg('Earlier tasks')}</h4>
79
- ${earlier.length ? earlier.map((e) => earlierTask(c, e)) : html`<div class="meta">${msg('No earlier tasks')}</div>`}
81
+ ${earlier.length ? earlier.map((e) => earlierTask(c, e, givenAbove.has(e.task.key))) : html`<div class="meta">${msg('No earlier tasks')}</div>`}
80
82
  ${isPy(d) ? nothing : promptSection(c)}`;
81
83
  }
82
84
 
@@ -120,9 +122,10 @@ function inputCard(c: TaskContext, f: InputFile) {
120
122
  /**
121
123
  * One earlier task: its status, how this task is joined to it (its lineage, in the words of
122
124
  * the graph's edges), how many of its output files this task reads, and the others folded
123
- * under it (open when one did not pass).
125
+ * under it (open when one did not pass). `givenAbove`: a parameter's "From" names it,
126
+ * so its lineage leaves out "given output" and keeps the rest.
124
127
  */
125
- function earlierTask(c: TaskContext, e: EarlierTask) {
128
+ function earlierTask(c: TaskContext, e: EarlierTask, givenAbove: boolean) {
126
129
  const u = e.task;
127
130
  const kindName = isPython(u) ? msg('Python task') : msg('Agent task');
128
131
  const state = c.outcomes.get(u.key)?.outcome;
@@ -134,7 +137,7 @@ function earlierTask(c: TaskContext, e: EarlierTask) {
134
137
  ${executorIcon(isPython(u), kindName)}
135
138
  ${taskLink(c, u)}
136
139
  ${state ? outcomeChip(state) : nothing}
137
- ${lineageTags(u.lineage ?? [])}
140
+ ${lineageTags((u.lineage ?? []).filter((l) => !(givenAbove && l === 'given_from')))}
138
141
  <span class="meta">${summary}</span>
139
142
  </div>`;
140
143
  if (!e.rest.length) return html`<div class="upstream">${head}</div>`;
@@ -214,24 +217,14 @@ function usageTable(c: TaskContext, rows: JournalRow[]) {
214
217
  </table></div>`;
215
218
  }
216
219
 
217
- /** What the task wrote: its output files, its outcome, tokens and cost, and the later tasks. */
220
+ /** What the task wrote: its output files, tokens and cost, and the later tasks. */
218
221
  export function outputPane(c: TaskContext): TemplateResult {
219
222
  const d = c.detail;
220
223
  const io = d.io;
221
- const last = d.rows[d.rows.length - 1];
222
- // The reason, a refusal's cause and a model switch are in the verdict above, so they are not repeated here.
223
- return html`<h4 class="sect-h">${msg('Output files')}</h4>
224
- ${io.outputs.length
224
+ // The header above names the attempts and how each ended, and the tab names the files: the pane starts with them.
225
+ return html`${io.outputs.length
225
226
  ? io.outputs.map((o) => ioFileCard(o, 'output', c.run, { producerOf: producerOf(c) }))
226
227
  : html`<div class="meta">${msg('No output files')}</div>`}
227
- ${last
228
- ? html`<h4 class="sect-h">${msg('Outcome')}</h4>
229
- <div class="ct-in">
230
- ${pill({ text: stopLabel(last.stop, last.reason), tone: stopTone(last.stop) })}
231
- ${isLaunched(last) ? html`<span class="meta">${facts(attemptLabel(last.attempt), fmtDur(last.wall_s), correctionsOf(last))}</span>` : nothing}
232
-
233
- </div>`
234
- : nothing}
235
228
  ${usageTable(c, d.rows)}
236
229
  <h4 class="sect-h">${msg('Later tasks')}</h4>
237
230
  ${io.downstream.length ? laterTasks(c, io.downstream) : html`<div class="meta">${msg('No later tasks')}</div>`}`;
@@ -150,7 +150,8 @@ export class CaTrends extends LitElement {
150
150
  /** The period, its time zone and the run group: links of the page's address. */
151
151
  private controls() {
152
152
  const data = this.view?.data;
153
- const period = data ? html`<span class="period">${periodLabel(data.period.from, data.period.to, data.period.tz)}</span>` : nothing;
153
+ // The time zone control names the zone; an export has none, so its period names it.
154
+ const period = data ? html`<span class="period">${periodLabel(data.period.from, data.period.to, this.exported ? data.period.tz : '')}</span>` : nothing;
154
155
  if (this.exported) return html`<div class="controls"><span class="meta">${exportedPeriod()}</span>${period}</div>`;
155
156
  const choice = this.days ? String(this.days) : 'dates';
156
157
  const today = data?.period.to || new Date().toISOString().slice(0, 10);