@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
@@ -36,9 +36,19 @@ export function shortStop(stop: string | null | undefined): string {
36
36
  return stop === 'invalid_input' ? outcomeLabel('not_started') : stopLabel(stop);
37
37
  }
38
38
 
39
- /** The identity the attempt authenticated as; both when a model switch changed it. */
40
- function identity(e: JournalRow) {
41
- const ids = [...new Set([...(e.hops ?? []).map((h) => h.credential), e.credential].filter(Boolean))];
39
+ /** The identities an attempt authenticated as, in order; both when a model switch changed it. */
40
+ const identities = (e: JournalRow): string[] => [...new Set([...(e.hops ?? []).map((h) => h.credential), e.credential].filter((c): c is string => !!c))];
41
+
42
+ /** The one identity every attempt that ran authenticated as alone; '' when they differ or none is known. */
43
+ export function sharedIdentity(journal: readonly JournalRow[]): string {
44
+ const seen = new Set(journal.filter(isLaunched).map((e) => identities(e).join('\n')));
45
+ const [only] = seen;
46
+ return seen.size === 1 && only && !only.includes('\n') ? only : '';
47
+ }
48
+
49
+ /** The identity the attempt authenticated as (both when a model switch changed it), unless `shared`: then the column's header names it. */
50
+ function identity(e: JournalRow, shared: string) {
51
+ const ids = identities(e);
42
52
  if (!ids.length) return nothing;
43
53
  // The subscription usage the attempt's first session started at, when the agent reported one.
44
54
  const start = (e.quota ?? [])[0];
@@ -47,6 +57,7 @@ function identity(e: JournalRow) {
47
57
  const own = start && ids.length === 1 && (start.credential ?? '') === ids[0];
48
58
  const shown = start ? (own ? readingShort(start) : accountReading(start)) : '';
49
59
  const tips = [tip, ...ids.map(credentialTip)].filter(Boolean).join('\n');
60
+ if (shared) return start ? html`<span class="sub" title=${tips || nothing}><span class="q">${shown}</span></span>` : nothing;
50
61
  return html`<span class="sub" title=${tips || nothing}>${ids.map((c) => credentialLabel(c)).join(' → ')}${start ? html` <span class="q">${shown}</span>` : nothing}</span>`;
51
62
  }
52
63
 
@@ -55,6 +66,8 @@ export function journalColumns(ctx: JournalColumnsContext): DataColumn<JournalRo
55
66
  const maxWall = Math.max(1, ...ctx.rows.map((e) => e.wall_s || 0));
56
67
  const tries = attemptFacts(ctx.journal);
57
68
  const index = new Map(ctx.journal.map((r, i) => [r, i]));
69
+ // Every attempt ran as one identity: the header names it once, the rows do not.
70
+ const shared = sharedIdentity(ctx.journal);
58
71
  /** The attempt that later did the task, after this one failed (0: none). */
59
72
  const laterTry = (e: JournalRow): number => {
60
73
  const f = tries.get(e.key);
@@ -133,11 +146,12 @@ export function journalColumns(ctx: JournalColumnsContext): DataColumn<JournalRo
133
146
  },
134
147
  {
135
148
  id: 'agent',
136
- header: msg('Agent, model, identity'),
149
+ header: shared ? msg('Agent, model') : msg('Agent, model, identity'),
150
+ ...(shared ? { tip: [msg(str`Identity: ${credentialLabel(shared)}`), credentialTip(shared)].filter(Boolean).join('\n') } : {}),
137
151
  width: 'minmax(150px,1fr)',
138
152
  cell: (e) =>
139
153
  html`<span class="jmono jdim two" title=${facts(e.route, modelSwitch(e.hops, null, e.skipped_hops)) || nothing}>${isLaunched(e)
140
- ? html`<span>${[e.agent, e.model].filter(Boolean).join(' / ') || '—'}</span>${identity(e)}`
154
+ ? html`<span>${[e.agent, e.model].filter(Boolean).join(' / ') || '—'}</span>${identity(e, shared)}`
141
155
  : '—'}</span>`,
142
156
  },
143
157
  {
@@ -31,9 +31,10 @@ export interface Kpis {
31
31
  /** Tries after a task's first, and the tasks they belong to. */
32
32
  retries: number;
33
33
  retriedTasks: number;
34
- /** The keys of the tasks that were retried / had fix-ups (to open one directly when it is the only one). */
34
+ /** The keys of the tasks that were retried / had fix-ups / switched models (to name or open one directly when it is the only one). */
35
35
  retriedKeys: string[];
36
36
  fixupKeys: string[];
37
+ switchKeys: string[];
37
38
  /** Fix-ups (sessions resumed with check errors), and the tasks that had any. */
38
39
  fixups: number;
39
40
  fixupTasks: number;
@@ -67,6 +68,7 @@ export function kpis(journal: readonly JournalRow[]): Kpis {
67
68
  fixupTasks: tasks(fixed),
68
69
  switches: switched.reduce((a, e) => a + (e.hops || []).length - 1, 0),
69
70
  switchTasks: tasks(switched),
71
+ switchKeys: keys(switched),
70
72
  };
71
73
  }
72
74
 
@@ -100,21 +102,40 @@ function notStartedCauses(outcomes: Iterable<TaskOutcome>): string[] {
100
102
  /** The history filtered by `stop`, named `label`. */
101
103
  const history = (stop: string, label: string): StatTile['action'] => ({ id: `history:${stop}`, label });
102
104
 
103
- /** The tasks behind a count: the task itself when it is one (named), else the history filtered to them (`label`). */
104
- function show(journal: readonly JournalRow[], keys: readonly string[], stop: string, label: string): StatTile['action'] {
105
+ /** The one task behind a count, by key and name; undefined when there are several. */
106
+ function onlyTask(journal: readonly JournalRow[], keys: readonly string[]): { key: string; name: string } | undefined {
105
107
  const only = keys.length === 1 ? keys[0] : undefined;
106
- if (!only) return history(stop, label);
108
+ if (!only) return undefined;
107
109
  const row = journal.find((r) => r.key === only);
108
- return { id: `task:${only}`, label: row ? kindUnit(row) : only };
110
+ return { key: only, name: row ? kindUnit(row) : only };
111
+ }
112
+
113
+ /** The tasks behind a count: the task itself when it is one (named), else the history filtered to them (`label`). */
114
+ function show(journal: readonly JournalRow[], keys: readonly string[], stop: string, label: string): StatTile['action'] {
115
+ const one = onlyTask(journal, keys);
116
+ return one ? { id: `task:${one.key}`, label: one.name } : history(stop, label);
117
+ }
118
+
119
+ /**
120
+ * The line under a count of events: the one task they belong to by name, else the
121
+ * count of tasks (`many`) when it differs from the count of events; none at 0.
122
+ */
123
+ function tasksLine(journal: readonly JournalRow[], count: number, keys: readonly string[], many: (n: number) => string): string[][] {
124
+ if (!count) return [];
125
+ const one = onlyTask(journal, keys);
126
+ if (one) return [[one.name]];
127
+ return keys.length === count ? [] : [[many(keys.length)]];
109
128
  }
110
129
 
111
130
  /**
112
131
  * The tiles: tasks (with `d` from the plan; from the journal alone otherwise),
113
132
  * attempts, cost and tokens (when reported), failed attempts, not started,
114
133
  * agent time, retries, corrections and model switches. Neutral by default:
115
- * only a problem count above 0 takes its tone (`problemTone`).
134
+ * only a problem count above 0 takes its tone (`problemTone`). A tile's lines
135
+ * add facts the value does not give: a 0 has none. `highestCost: false` leaves
136
+ * the kind with the highest cost out of the cost tile.
116
137
  */
117
- export function kpiTiles(journal: readonly JournalRow[], d: Pick<RunDerived, 'outcomes' | 'counts'>): StatTile[] {
138
+ export function kpiTiles(journal: readonly JournalRow[], d: Pick<RunDerived, 'outcomes' | 'counts'>, opts: { highestCost?: boolean } = {}): StatTile[] {
118
139
  const k = kpis(journal);
119
140
  const { outcomes, counts: c } = d;
120
141
  // The total is neutral; the parts that are problems carry their own status.
@@ -135,13 +156,13 @@ export function kpiTiles(journal: readonly JournalRow[], d: Pick<RunDerived, 'ou
135
156
  sub: [factList(k.launches === 1 ? msg('1 agent session') : msg(str`${k.launches} agent sessions`), k.python === 1 ? msg('1 Python task attempt') : msg(str`${k.python} Python task attempts`))],
136
157
  tip: msg('Attempts that ran, retries included (journal.jsonl)'),
137
158
  },
138
- ...spendTiles(journal),
159
+ ...spendTiles(journal, opts.highestCost ?? true),
139
160
  {
140
161
  id: 'bad',
141
162
  tone: problemTone(k.bad, 'failed'),
142
163
  label: msg('Failed attempts'),
143
164
  value: k.bad,
144
- sub: [k.bad ? [] : [msg('All attempts done')]],
165
+ sub: [],
145
166
  ...(k.bad ? { action: history('bad', msg('Failed attempts in history')) } : {}),
146
167
  },
147
168
  {
@@ -149,7 +170,7 @@ export function kpiTiles(journal: readonly JournalRow[], d: Pick<RunDerived, 'ou
149
170
  tone: problemTone(c.not_started, 'attention'),
150
171
  label: msg('Not started'),
151
172
  value: c.not_started,
152
- sub: [c.not_started ? notStartedCauses(outcomes.values()) : [msg('All tasks started')]],
173
+ sub: c.not_started ? [notStartedCauses(outcomes.values())] : [],
153
174
  ...(c.not_started ? { action: history('invalid_input', msg('Tasks not started in history')) } : {}),
154
175
  },
155
176
  {
@@ -163,7 +184,7 @@ export function kpiTiles(journal: readonly JournalRow[], d: Pick<RunDerived, 'ou
163
184
  id: 'retry',
164
185
  label: msg('Retries'),
165
186
  value: k.retries,
166
- sub: [[!k.retries ? msg('No retries') : k.retriedTasks === 1 ? msg('1 task retried') : msg(str`${k.retriedTasks} tasks retried`)]],
187
+ sub: tasksLine(journal, k.retries, k.retriedKeys, (n) => msg(str`${n} tasks retried`)),
167
188
  tip: retryTip(),
168
189
  ...(k.retries ? { action: show(journal, k.retriedKeys, 'retried', msg('Retried tasks in history')) } : {}),
169
190
  },
@@ -171,7 +192,7 @@ export function kpiTiles(journal: readonly JournalRow[], d: Pick<RunDerived, 'ou
171
192
  id: 'ct',
172
193
  label: msg('Corrections'),
173
194
  value: k.fixups,
174
- sub: [[!k.fixups ? msg('No corrections') : k.fixupTasks === 1 ? msg('1 task with a correction') : msg(str`${k.fixupTasks} tasks with corrections`)]],
195
+ sub: tasksLine(journal, k.fixups, k.fixupKeys, (n) => msg(str`${n} tasks with corrections`)),
175
196
  tip: correctionTip(),
176
197
  ...(k.fixups ? { action: show(journal, k.fixupKeys, 'fixup', msg('Attempts with corrections in history')) } : {}),
177
198
  },
@@ -179,14 +200,14 @@ export function kpiTiles(journal: readonly JournalRow[], d: Pick<RunDerived, 'ou
179
200
  id: 'fo',
180
201
  label: msg('Model switches'),
181
202
  value: k.switches,
182
- sub: [[!k.switches ? msg('No model switches') : k.switchTasks === 1 ? msg('1 task with a model switch') : msg(str`${k.switchTasks} tasks with model switches`)]],
203
+ sub: tasksLine(journal, k.switches, k.switchKeys, (n) => msg(str`${n} tasks with model switches`)),
183
204
  tip: modelSwitchTip(),
184
205
  },
185
206
  ];
186
207
  }
187
208
 
188
- /** Cost and tokens, when any attempt reported them. */
189
- function spendTiles(journal: readonly JournalRow[]): StatTile[] {
209
+ /** Cost and tokens, when any attempt reported them; the cost tile names the kind with the highest cost when `highest`. */
210
+ function spendTiles(journal: readonly JournalRow[], highest: boolean): StatTile[] {
190
211
  const s = spendOf(journal);
191
212
  if (!s) return [];
192
213
  const top = spendByKind(journal)[0];
@@ -195,7 +216,7 @@ function spendTiles(journal: readonly JournalRow[]): StatTile[] {
195
216
  id: 'cost',
196
217
  label: msg('Cost'),
197
218
  value: usd(s.cost),
198
- sub: top && top.share !== null ? [[msg(str`Highest cost: ${top.kind}`)], factList(usd(top.spend.cost), msg(str`${Math.round(top.share * 100)}% of the total`))] : [[]],
219
+ sub: highest && top && top.share !== null ? [[msg(str`Highest cost: ${top.kind}`)], factList(usd(top.spend.cost), msg(str`${Math.round(top.share * 100)}% of the total`))] : [[]],
199
220
  tip: msg('Reported by the coding agent'),
200
221
  },
201
222
  {
@@ -9,7 +9,7 @@ import { attemptNames } from '../../words/attempts.js';
9
9
  import { roundLabel, roundMark, switchText } from '../../words/rounds.js';
10
10
  import { stopLabel } from '../../words/stop.js';
11
11
  import type { Transcript } from './model.js';
12
- import { whoTags } from './who.js';
12
+ import { whoTags, type Who } from './who.js';
13
13
 
14
14
  export interface SectionsContext {
15
15
  /** The run id: paths in a reason are shown relative to it. */
@@ -18,6 +18,14 @@ export interface SectionsContext {
18
18
  rows: readonly JournalRow[];
19
19
  /** The latest round is still being written (the task runs and its transcript is tailed). */
20
20
  active: boolean;
21
+ /** Who the page names already (the task's header): a section's head shows only what differs from it. */
22
+ shownWho?: Who | null;
23
+ }
24
+
25
+ /** What of `who` differs from `shown`: the agent, model and role the reader has not seen yet. */
26
+ export function whoBeyond(who: Who | null | undefined, shown: Who | null | undefined): Who | null {
27
+ if (!who || !shown) return who ?? null;
28
+ return { agent: who.agent === shown.agent ? '' : who.agent, model: who.model === shown.model ? '' : who.model, role: who.role === shown.role ? '' : who.role };
21
29
  }
22
30
 
23
31
  /** The command line, and the prompt with what was not saved of it. */
@@ -72,7 +80,7 @@ export function transcriptSections(t: Transcript, rounds: readonly RoundInfo[],
72
80
  facts: model ? [model.replace(/^claude-/, '')] : [],
73
81
  verdict: mark ? { tone: mark === '✓' ? 'done' : 'failed', text: word, tip } : undefined,
74
82
  active: i === last && ctx.active,
75
- tags: r.launch ? whoTags(r.who) : [],
83
+ tags: r.launch ? whoTags(whoBeyond(r.who, ctx.shownWho)) : [],
76
84
  details: r.launch ? launchDetails(r.launch) : [],
77
85
  // the model leaves an event's title to the reader's words: the only event is an API request sent again
78
86
  items: r.items.map((it): TranscriptItem => (it.type === 'event' ? { ...it, title: msg('API request sent again') } : it)),
@@ -8,19 +8,16 @@ import { RememberedSet } from '../../core/memory.js';
8
8
  import type { TaskView } from '../../core/view-route.js';
9
9
  import { base } from '@contraagent/elements/styles';
10
10
  import { executorIcon, executorIconStyles } from '../../words/executor.js';
11
- import { kindUnit } from '../../words/format.js';
12
- import { correctionRoundTip, correctionRoundsLabel, notRunText, type Producer } from '../../words/contract.js';
11
+ import { facts, kindUnit } from '../../words/format.js';
12
+ import { contractStatusLabel, correctionRoundTip, correctionRoundsLabel, notRunText, type Producer } from '../../words/contract.js';
13
13
  import { contractChip, contractStyles, fieldCheckTable, fileConditions, inputChip } from '../shared/contract-parts.js';
14
14
 
15
15
  const CT_ORDER: readonly ContractStatus[] = ['invalid', 'refused', 'failed', 'running', 'pending', 'valid'];
16
16
 
17
- /** Output contracts per status, worst first; a line of text when none is declared. */
18
- export function ctChips(outputs: Outputs | null) {
17
+ /** Output contracts per status that is not passed, worst first: the passed ones head their own group. */
18
+ function ctChips(outputs: Outputs | null) {
19
19
  const t = outputs?.tally;
20
- const chips = t ? CT_ORDER.filter((k) => t[k]) : [];
21
- return chips.length
22
- ? chips.map((k) => contractChip(k, t?.[k]))
23
- : html`<span class="meta">${msg('No checked files')}</span>`;
20
+ return (t ? CT_ORDER.filter((k) => k !== 'valid' && t[k]) : []).map((k) => contractChip(k, t?.[k]));
24
21
  }
25
22
 
26
23
  const shortPath = (p: string) => p.split('/').slice(-2).join('/');
@@ -34,8 +31,9 @@ const rank = (s: ContractStatus | null | undefined) => (s ? CT_ORDER.indexOf(s)
34
31
 
35
32
  /**
36
33
  * The files each task must write (`OutputContract`) and read (`InputContract`),
37
- * problems first, each checked field by field against its schema. Links open
38
- * the task (`ca-open-task`).
34
+ * problems first, each with the chip of its status, then the passed ones under
35
+ * one chip; each checked field by field against its schema. Links open the
36
+ * task (`ca-open-task`).
39
37
  *
40
38
  * @fires ca-open-task - `{key, view}`: a link to the task's input, output or transcript
41
39
  */
@@ -49,6 +47,8 @@ export class CaContracts extends LitElement {
49
47
  css`
50
48
  :host { display: block; container-type: inline-size; }
51
49
  .ct-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
50
+ /* the passed outputs: one chip heads them, their rows carry none */
51
+ .passed { margin: 6px 0 0; padding: 10px 0 8px; font: inherit; border-top: 1px solid var(--line); }
52
52
  details.ct { border-top: 1px solid var(--line); }
53
53
  details.ct > summary { list-style: none; cursor: pointer; padding: 7px 4px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }
54
54
  details.ct > summary > * { min-width: 0; overflow-wrap: anywhere; }
@@ -85,24 +85,30 @@ export class CaContracts extends LitElement {
85
85
  const cs = this.outputs?.contracts ?? [];
86
86
  this.outcomes = this.plan ? deriveRun(this.journal, this.plan).outcomes : new Map<string, TaskOutcome>();
87
87
  if (!cs.length) {
88
- return html`<div class="blank"><span class="ic" aria-hidden="true">▣</span><b>${msg('No checked files')}</b>
89
- <span>${msg('No task declares an OutputContract or an InputContract')}</span></div>`;
88
+ return html`<div class="blank"><span class="ic" aria-hidden="true">▣</span><b>${facts(msg('No checked files'), msg('no task declares an OutputContract or an InputContract'))}</b></div>`;
90
89
  }
91
90
  const outs = cs
92
91
  .flatMap((c) => (c.outputs ?? []).map((o) => [c, o] as const))
93
92
  .sort(([a, x], [b, y]) => rank(x.status) - rank(y.status) || a.key.localeCompare(b.key));
94
93
  const ins = cs.flatMap((c) => (c.inputs ?? []).map((i) => [c, i] as const));
94
+ const others = outs.filter(([, o]) => o.status !== 'valid');
95
+ const passed = outs.filter(([, o]) => o.status === 'valid');
96
+ const chips = ctChips(this.outputs);
95
97
  return html`<div class="g12">
96
98
  <section class="panel c12">
97
- <h3 title=${msg('OutputContract')}>${msg('Outputs')}
98
- <span class="r">${msg('Files each task must write')}</span></h3>
99
- <div class="ct-row">${ctChips(this.outputs)}</div>
100
- ${repeat(outs, ([c, o]) => `out|${c.key}|${o.path}`, ([c, o]) => this.outRow(c, o))}
99
+ <h3 title=${msg('Files each task must write (OutputContract)')}>${msg('Outputs')}</h3>
100
+ ${chips.length ? html`<div class="ct-row">${chips}</div>` : nothing}
101
+ ${repeat(others, ([c, o]) => `out|${c.key}|${o.path}`, ([c, o]) => this.outRow(c, o))}
102
+ ${passed.length
103
+ ? html`<h4 class="passed">${contractChip('valid', passed.length)}</h4>
104
+ <div role="group" aria-label=${contractStatusLabel('valid')}>
105
+ ${repeat(passed, ([c, o]) => `out|${c.key}|${o.path}`, ([c, o]) => this.outRow(c, o))}
106
+ </div>`
107
+ : nothing}
101
108
  </section>
102
109
  ${ins.length
103
110
  ? html`<section class="panel c12">
104
- <h3 title=${msg('InputContract')}>${msg('Inputs')}
105
- <span class="r">${msg('Files each task reads')}</span></h3>
111
+ <h3 title=${msg('Files each task reads (InputContract)')}>${msg('Inputs')}</h3>
106
112
  ${repeat(ins, ([c, i]) => `in|${c.key}|${i.path}`, ([c, i]) => this.inRow(c, i))}
107
113
  </section>`
108
114
  : nothing}
@@ -165,7 +171,7 @@ export class CaContracts extends LitElement {
165
171
  // The chip says why there is no file; the size column shows a size only when there is one.
166
172
  return this.details(
167
173
  `out|${c.key}|${o.path}`,
168
- html`${contractChip(status)}
174
+ html`${status === 'valid' ? nothing : contractChip(status)}
169
175
  ${rounds > 1 ? html`<span class="meta" title=${correctionRoundTip()}>${correctionRoundsLabel(rounds, status === 'valid')}</span>` : nothing}
170
176
  ${this.who(c)}
171
177
  <span class="jmono jdim ct-path" title=${o.path}>${shortPath(o.path)}</span>
@@ -19,7 +19,7 @@ import { emit } from '../../core/events.js';
19
19
  * @csspart label - a tile's label
20
20
  * @csspart value - a tile's value
21
21
  * @csspart sub - each line under the value
22
- * @csspart action - a tile's link
22
+ * @csspart action - a tile's link, over the whole tile
23
23
  */
24
24
  @customElement('ca-kpis')
25
25
  @localized()
@@ -31,6 +31,8 @@ export class CaKpis extends LitElement {
31
31
  @property({ attribute: false }) journal: JournalRow[] = [];
32
32
  /** The scheduled tasks; without it, tasks are counted from the journal. */
33
33
  @property({ attribute: false }) plan: Plan = { nodes: [], groups: [] };
34
+ /** Leaves the kind with the highest cost out of the cost tile, for a page that shows the cost by kind beside it. */
35
+ @property({ type: Boolean, attribute: 'no-highest-cost' }) noHighestCost = false;
34
36
 
35
37
  private readonly onAction = (e: CustomEvent<{ action: string }>): void => {
36
38
  e.stopPropagation();
@@ -40,7 +42,7 @@ export class CaKpis extends LitElement {
40
42
  };
41
43
 
42
44
  override render() {
43
- return html`<cae-stat-tiles exportparts="tiles, tile, label, value, sub, action" .tiles=${kpiTiles(this.journal, deriveRun(this.journal, this.plan))} @cae-action=${this.onAction}></cae-stat-tiles>`;
45
+ return html`<cae-stat-tiles exportparts="tiles, tile, label, value, sub, action" .tiles=${kpiTiles(this.journal, deriveRun(this.journal, this.plan), { highestCost: !this.noHighestCost })} @cae-action=${this.onAction}></cae-stat-tiles>`;
44
46
  }
45
47
  }
46
48
 
@@ -16,7 +16,8 @@ const COLUMNS: readonly ModelMeasure[] = ['cost', 'tokens', 'time', 'sessions'];
16
16
 
17
17
  /**
18
18
  * What each model was used for in a run (`Plan.models`): the share of each model in
19
- * the chosen measure (cost, tokens, time or agent sessions) as one bar, and a table
19
+ * the chosen measure (cost, tokens, time or agent sessions) as one bar (the table
20
+ * names each segment, so the bar has no names under it), and a table
20
21
  * of every model with its share and every measure, and its peak context when
21
22
  * a session on it reported one. A model keeps its colour, and
22
23
  * its place, in the order it was first used, whatever the measure; past six models
@@ -74,7 +75,7 @@ export class CaModelUsage extends LitElement {
74
75
  @cae-change=${(e: CustomEvent<{ value: string }>) => (this.measure = e.detail.value as ModelMeasure)}></cae-segmented>
75
76
  </div>`
76
77
  : nothing}
77
- ${shareBar(shareParts(this.models, measure), { label: measureLabel(measure) })}
78
+ ${shareBar(shareParts(this.models, measure), { label: measureLabel(measure), names: false })}
78
79
  <div class="wrap" data-scroll="x" tabindex="0" role="region" aria-label=${msg('Models')}>
79
80
  <table>
80
81
  <thead>
@@ -7,7 +7,6 @@ import {
7
7
  type TypedPanelBody,
8
8
  liveActivities,
9
9
  progress,
10
- result,
11
10
  runAccounts,
12
11
  runRelative,
13
12
  spendOf,
@@ -29,9 +28,9 @@ import { facts, kindUnit, localTime, sentence } from '../../words/format.js';
29
28
  import { notStartedCause, outcomeLabel, stateCount, stopLabel } from '../../words/index.js';
30
29
  import { base } from '@contraagent/elements/styles';
31
30
  import { pill, statusWord } from '@contraagent/elements/primitives';
32
- import { contractTone, outcomeTone } from '../../lib/tone.js';
31
+ import { outcomeTone } from '../../lib/tone.js';
33
32
  import { contractChip, formatOf } from '../shared/contract-parts.js';
34
- import { contractStatusLabel, contractStatusWhy } from '../../words/contract.js';
33
+ import { contractStatusLabel } from '../../words/contract.js';
35
34
  import { taskChip } from '../shared/task-chip.js';
36
35
  import '../lists/ca-proc-tree.js';
37
36
  import './ca-kpis.js';
@@ -132,11 +131,15 @@ export class CaOverview extends LitElement {
132
131
  }
133
132
  .ok { display: flex; align-items: baseline; gap: 6px; color: var(--text-2); }
134
133
  .ok span[aria-hidden] { color: var(--status-done); }
135
- /* a failed output: its status and its task on one line, what it is and where under them */
136
- .ofl { grid-template-columns: max-content minmax(0, 1fr); }
137
- .ofl > .jstop { white-space: nowrap; }
134
+ /* a failed output, under the chip of its status: its task and what it is on one line, where under them */
135
+ .ofl { grid-template-columns: minmax(0, 1fr) auto; }
138
136
  .ofl > b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
139
- .ofl > .meta { grid-column: 1 / -1; overflow: hidden; text-overflow: ellipsis; }
137
+ .ofl > .meta { overflow: hidden; text-overflow: ellipsis; }
138
+ .ofl > .meta:last-child { grid-column: 1 / -1; }
139
+ .ogroup + .ogroup, .ogroup + .ct-row { margin-top: 10px; }
140
+ /* a heading that is a link to its tab */
141
+ h3 a.hl { color: inherit; text-decoration: none; }
142
+ h3 a.hl:hover { color: var(--accent); text-decoration: underline; }
140
143
  .ct-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
141
144
  .pcards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); gap: 12px; }
142
145
  .pcard { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; align-items: start; padding: 12px 14px; border-radius: 10px;
@@ -158,10 +161,8 @@ export class CaOverview extends LitElement {
158
161
  .ofl > .meta { white-space: nowrap; }
159
162
  .stopped { grid-column: 1 / 4; color: var(--text-muted); font-size: 11px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
160
163
  .fl.muted { opacity: 0.7; }
161
- .problems h4.sub { margin: 10px 0 2px; font-size: 11px; font-weight: 600; color: var(--text-muted); display: flex; }
162
- .problems h4.sub .r { margin-left: auto; font-weight: 400; }
163
- .next { margin: 10px 2px 2px; font-size: 12px; color: var(--text-2); }
164
- .next code { font-family: var(--mono); }
164
+ /* the tasks done after retrying: a group of their own under a rule, after the problems */
165
+ .recovered { margin-top: 8px; padding-top: 6px; border-top: 1px solid var(--line); }
165
166
  .more { font-size: 12px; margin: 6px 10px 0; }
166
167
  `,
167
168
  ];
@@ -211,29 +212,27 @@ export class CaOverview extends LitElement {
211
212
  <section class="panel enter problems">
212
213
  <h3>${msg('Problems')}
213
214
  <span class="r"><a class="lnk" href="#" @click=${(e: Event) => this.nav(e, () => emit(this, 'ca-navigate', { tab: 'journal', stop: 'bad' }))}>${msg('Failed attempts in history')}</a></span></h3>
214
- ${this.problems(live)}
215
+ ${this.problems()}
215
216
  </section>
216
217
  <section class="panel enter outputs">
217
- <h3 title=${msg('Checked files (OutputContract, InputContract)')}>${msg('Outputs')}
218
- <span class="r"><a class="lnk" href="#" @click=${(e: Event) => this.nav(e, () => this.go('outputs'))}>${msg('All outputs')}</a></span></h3>
218
+ <h3 title=${msg('Checked files (OutputContract, InputContract)')}><a class="hl" href="#" @click=${(e: Event) => this.nav(e, () => this.go('outputs'))}>${msg('Outputs')}</a></h3>
219
219
  ${this.outputs()}
220
220
  </section>
221
221
  ${running ? html`<div class="row">${this.runningCard(live, busy, max)}</div>` : nothing}
222
222
  </div>
223
223
  <section class="panel enter">
224
224
  <h3 title=${msg('Every task the run scheduled, by kind (plan.jsonl)')}>${msg('Pipeline')}
225
- <span class="r">${this.progressLine(live)}</span></h3>
225
+ ${live ? html`<span class="r">${this.progressLine()}</span>` : nothing}</h3>
226
226
  <ca-pipeline-graph .plan=${s.plan} .forecast=${s.forecast} .journal=${s.journal} .run=${this.run} .layer=${this.layer} .unit=${this.unit} .sel=${this.sel} ?live=${live}></ca-pipeline-graph>
227
227
  </section>
228
228
  <section class="panel enter">
229
229
  <h3 title=${msg('Counted from every finished attempt (journal.jsonl)')}>${msg('Totals')}</h3>
230
- <ca-kpis .journal=${s.journal} .plan=${s.plan}></ca-kpis>
230
+ <ca-kpis .journal=${s.journal} .plan=${s.plan} no-highest-cost></ca-kpis>
231
231
  </section>
232
232
  <div class="band parts">
233
233
  ${live
234
234
  ? html`<section class="panel enter">
235
- <h3>${msg('Process tree')}
236
- <span class="r"><a class="lnk" href="#" @click=${(e: Event) => this.nav(e, () => this.go('procs'))}>${msg('All processes')}</a></span></h3>
235
+ <h3><a class="hl" href="#" @click=${(e: Event) => this.nav(e, () => this.go('procs'))}>${msg('Process tree')}</a></h3>
237
236
  <ca-proc-tree .procs=${s.procs}></ca-proc-tree>
238
237
  </section>`
239
238
  : nothing}
@@ -245,15 +244,13 @@ export class CaOverview extends LitElement {
245
244
  : nothing}
246
245
  ${s.plan.models?.length
247
246
  ? html`<section class="panel enter">
248
- <h3 title=${msg('What the agent sessions spent on each model, each model of a model switch counted separately (Plan.models)')}>${msg('Models')}
249
- <span class="r">${s.plan.models.length === 1 ? msg('1 model') : msg(str`${s.plan.models.length} models`)}</span></h3>
247
+ <h3 title=${msg('What the agent sessions spent on each model, each model of a model switch counted separately (Plan.models)')}>${msg('Models')}</h3>
250
248
  <ca-model-usage .models=${s.plan.models}></ca-model-usage>
251
249
  </section>`
252
250
  : nothing}
253
251
  ${runAccounts(s.journal).length || s.plan.rests?.length
254
252
  ? html`<section class="panel enter">
255
- <h3 title=${msg('Account-wide | read at each agent session start')}>
256
- ${msg('Subscription usage')}<span class="r">${msg('Per account, first → last reading')}</span></h3>
253
+ <h3 title=${msg('Account-wide | first and last reading per account | read at each agent session start')}>${msg('Subscription usage')}</h3>
257
254
  <ca-quota .journal=${s.journal} .caps=${s.plan.quota_caps ?? []} .rests=${s.plan.rests ?? []} .rule=${s.plan.quota_rule ?? ''} ?live=${live}></ca-quota>
258
255
  </section>`
259
256
  : nothing}
@@ -277,8 +274,7 @@ export class CaOverview extends LitElement {
277
274
  </section>`
278
275
  : nothing}
279
276
  <section class="panel enter">
280
- <h3>${msg('Timeline')}
281
- <span class="r">${this.triesLine()}</span></h3>
277
+ <h3>${msg('Timeline')}</h3>
282
278
  <ca-timeline .journal=${s.journal} .plan=${s.plan} .procs=${s.procs} .live=${live} .run=${this.run}></ca-timeline>
283
279
  </section>
284
280
  </div>`;
@@ -298,60 +294,44 @@ export class CaOverview extends LitElement {
298
294
  if (!contracts.length) {
299
295
  return html`<div class="meta">${msg('No output files')}</div>`;
300
296
  }
301
- const chips = CT_ORDER.filter((k) => tally[k]);
302
297
  // Only what went wrong with a file; tasks that never started are the problems section's.
303
298
  const bad = contracts
304
299
  .flatMap((c) => (c.outputs ?? []).map((o): [ContractRecord, ContractSpec] => [c, o]))
305
300
  .filter(([, o]) => o.status === 'invalid' || o.status === 'failed');
306
301
  const shown = bad.slice(0, OUTPUTS_SHOWN);
307
- const refused = tally.refused;
308
- return html`<div class="ct-row">
309
- ${chips.length
310
- ? chips.map((k) => contractChip(k, tally[k]))
311
- : html`<span class="meta">${msg('No checked files')}</span>`}
312
- </div>
313
- ${shown.map(
314
- ([c, o]) => html`<button type="button" class="fl ofl" @click=${() => this.go('outputs')}>
315
- ${statusWord({ text: o.status ? contractStatusLabel(o.status) : '—', tone: contractTone(o.status), title: o.status ? contractStatusWhy(o.status) : null })}
316
- <b class="jmono" title=${c.key}>${kindUnit({ kind: c.kind, unit: c.unit === c.key ? '' : c.unit })}</b>
317
- <span class="meta">${formatOf(o)}</span>
318
- <span class="meta" title=${runRelative(o.errors?.[0] || o.path, this.run)}>${runRelative(o.errors?.[0] || o.path, this.run)}</span>
319
- </button>`,
302
+ // The rows shown sit under the chip of their status; the other statuses are a row of chips after them.
303
+ const groups = CT_ORDER.filter((k) => tally[k] && shown.some(([, o]) => o.status === k));
304
+ const chips = CT_ORDER.filter((k) => tally[k] && !groups.includes(k));
305
+ return html`${groups.map(
306
+ (k) => html`<div class="ogroup" role="group" aria-label=${contractStatusLabel(k)}>
307
+ <div class="ct-row">${contractChip(k, tally[k])}</div>
308
+ ${shown
309
+ .filter(([, o]) => o.status === k)
310
+ .map(
311
+ ([c, o]) => html`<button type="button" class="fl ofl" @click=${() => this.go('outputs')}>
312
+ <b class="jmono" title=${c.key}>${kindUnit({ kind: c.kind, unit: c.unit === c.key ? '' : c.unit })}</b>
313
+ <span class="meta">${formatOf(o)}</span>
314
+ <span class="meta" title=${runRelative(o.errors?.[0] || o.path, this.run)}>${runRelative(o.errors?.[0] || o.path, this.run)}</span>
315
+ </button>`,
316
+ )}
317
+ </div>`,
320
318
  )}
321
319
  ${bad.length > shown.length
322
320
  ? html`<div class="more"><a class="lnk" href="#" @click=${(e: Event) => this.nav(e, () => this.go('outputs'))}>${bad.length - shown.length === 1
323
321
  ? msg('1 more output')
324
322
  : msg(str`${bad.length - shown.length} more outputs`)}</a></div>`
325
323
  : nothing}
326
- ${refused
327
- ? html`<div class="meta more">${refused === 1
328
- ? msg('1 output not written (task not started)')
329
- : msg(str`${refused} outputs not written (tasks not started)`)}</div>`
324
+ ${chips.length || !groups.length
325
+ ? html`<div class="ct-row">
326
+ ${chips.length ? chips.map((k) => contractChip(k, tally[k])) : html`<span class="meta">${msg('No checked files')}</span>`}
327
+ </div>`
330
328
  : nothing}`;
331
329
  }
332
330
 
333
- /** While running, how far the run is; once over, how its tasks ended. */
334
- private progressLine(live: boolean): string {
335
- const plan = this.state.plan;
336
- const r = result(plan);
337
- if (live || r.left) {
338
- const p = progress(plan);
339
- return facts(msg(str`${p.pct}%`), msg(str`${p.done} of ${p.total} tasks done`), p.partial ? stateCount('partial', p.partial) : '', p.skipped ? stateCount('skipped', p.skipped) : '', p.cancelled ? stateCount('cancelled', p.cancelled) : '');
340
- }
341
- return facts(
342
- stateCount('done', r.done),
343
- r.skipped ? stateCount('skipped', r.skipped) : '',
344
- r.failed ? stateCount('failed', r.failed) : '',
345
- r.notStarted ? stateCount('not_started', r.notStarted) : '',
346
- r.partial ? stateCount('partial', r.partial) : '',
347
- r.cancelled ? stateCount('cancelled', r.cancelled) : '',
348
- );
349
- }
350
-
351
- /** "23 attempts | 4 not started": a task that never started has no attempt. */
352
- private triesLine(): string {
353
- const { tries, notStarted } = deriveRun(this.state.journal, this.state.plan).tries;
354
- return facts(tries === 1 ? msg('1 attempt') : msg(str`${tries} attempts`), notStarted ? stateCount('not_started', notStarted) : '');
331
+ /** While running, how far the run is; once over, the run's state and the tiles tell how its tasks ended. */
332
+ private progressLine(): string {
333
+ const p = progress(this.state.plan);
334
+ return facts(msg(str`${p.pct}%`), msg(str`${p.done} of ${p.total} tasks done`), p.partial ? stateCount('partial', p.partial) : '', p.skipped ? stateCount('skipped', p.skipped) : '', p.cancelled ? stateCount('cancelled', p.cancelled) : '');
355
335
  }
356
336
 
357
337
  /** A link's click: no navigation of the page's own, then `go`. */
@@ -425,7 +405,7 @@ export class CaOverview extends LitElement {
425
405
  return kindUnit(t);
426
406
  }
427
407
 
428
- private problems(live: boolean) {
408
+ private problems() {
429
409
  const { outcomes, problems } = deriveRun(this.state.journal, this.state.plan);
430
410
  const { failed, notStarted, partial, recovered } = problems;
431
411
  const issues = failed.length + notStarted.length + partial.length;
@@ -482,8 +462,8 @@ export class CaOverview extends LitElement {
482
462
  </div>`,
483
463
  )}
484
464
  ${rest(partial.length, PROBLEMS_SHOWN, 'partial')}
485
- ${recovered.length ? html`<h4 class="sub">${sentence(outcomeLabel('recovered'))} <span class="r">${recovered.length}</span></h4>` : nothing}
486
- ${repeat(
465
+ ${recovered.length
466
+ ? html`<div class="recovered" role="group" aria-label=${sentence(outcomeLabel('recovered'))}>${repeat(
487
467
  recovered.slice(0, RECOVERED_SHOWN),
488
468
  (t) => t.key,
489
469
  (t) => {
@@ -498,24 +478,14 @@ export class CaOverview extends LitElement {
498
478
  </div>`;
499
479
  },
500
480
  )}
501
- ${rest(recovered.length, RECOVERED_SHOWN, 'retried')}
502
- ${this.nextStep(live, issues)}`;
481
+ ${rest(recovered.length, RECOVERED_SHOWN, 'retried')}</div>`
482
+ : nothing}`;
503
483
  }
504
484
 
505
485
  /** A row's second line: one line, the whole of it in its tooltip. */
506
486
  private metaLine(text: string) {
507
487
  return html`<div class="meta" title=${text}>${text}</div>`;
508
488
  }
509
-
510
- /** For a finished run with problems: how many tasks are not done (the tasks resume=True runs again). */
511
- private nextStep(live: boolean, problemCount: number) {
512
- if (live || !problemCount) return nothing;
513
- const c = deriveRun(this.state.journal, this.state.plan).counts;
514
- // A partly done task is finished in this run, and not done: a resume runs it again.
515
- const again = c.total - c.finished + c.partial;
516
- if (!again) return nothing;
517
- return html`<div class="next" title="resume=True">${msg(str`${again} of ${c.total} tasks not done`)}</div>`;
518
- }
519
489
  }
520
490
 
521
491
  declare global {