@contraagent/ui 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) 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 +19 -6
  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/ca-workers.js +1 -1
  40. package/dist/elements/overview/pipeline-graph/strings.js +3 -3
  41. package/dist/elements/overview/pipeline-graph/strings.js.map +1 -1
  42. package/dist/elements/shared/ca-definition.d.ts.map +1 -1
  43. package/dist/elements/shared/ca-definition.js +1 -2
  44. package/dist/elements/shared/ca-definition.js.map +1 -1
  45. package/dist/elements/shell/ca-hosts.d.ts.map +1 -1
  46. package/dist/elements/shell/ca-hosts.js +1 -2
  47. package/dist/elements/shell/ca-hosts.js.map +1 -1
  48. package/dist/elements/shell/ca-monitor.d.ts +3 -1
  49. package/dist/elements/shell/ca-monitor.d.ts.map +1 -1
  50. package/dist/elements/shell/ca-monitor.js +20 -9
  51. package/dist/elements/shell/ca-monitor.js.map +1 -1
  52. package/dist/elements/shell/ca-run-picker.js +1 -1
  53. package/dist/elements/shell/ca-run-picker.js.map +1 -1
  54. package/dist/elements/shell/ca-theme-switch.d.ts.map +1 -1
  55. package/dist/elements/shell/ca-theme-switch.js +6 -1
  56. package/dist/elements/shell/ca-theme-switch.js.map +1 -1
  57. package/dist/elements/task/ca-task-view.js +2 -2
  58. package/dist/elements/task/ca-task-view.js.map +1 -1
  59. package/dist/elements/task/ca-transcript.d.ts +3 -0
  60. package/dist/elements/task/ca-transcript.d.ts.map +1 -1
  61. package/dist/elements/task/ca-transcript.js +9 -2
  62. package/dist/elements/task/ca-transcript.js.map +1 -1
  63. package/dist/elements/task/header.d.ts +3 -0
  64. package/dist/elements/task/header.d.ts.map +1 -1
  65. package/dist/elements/task/header.js +22 -13
  66. package/dist/elements/task/header.js.map +1 -1
  67. package/dist/elements/task/panes/io.d.ts +1 -1
  68. package/dist/elements/task/panes/io.d.ts.map +1 -1
  69. package/dist/elements/task/panes/io.js +14 -21
  70. package/dist/elements/task/panes/io.js.map +1 -1
  71. package/dist/elements/trends/ca-trends.d.ts.map +1 -1
  72. package/dist/elements/trends/ca-trends.js +2 -1
  73. package/dist/elements/trends/ca-trends.js.map +1 -1
  74. package/dist/locales/ja.d.ts +16 -29
  75. package/dist/locales/ja.d.ts.map +1 -1
  76. package/dist/locales/ja.js +16 -29
  77. package/dist/locales/ja.js.map +1 -1
  78. package/dist/words/quota.d.ts +2 -0
  79. package/dist/words/quota.d.ts.map +1 -1
  80. package/dist/words/quota.js +4 -0
  81. package/dist/words/quota.js.map +1 -1
  82. package/dist/words/run-line.d.ts +5 -3
  83. package/dist/words/run-line.d.ts.map +1 -1
  84. package/dist/words/run-line.js +6 -4
  85. package/dist/words/run-line.js.map +1 -1
  86. package/dist/words/trends.d.ts +2 -2
  87. package/dist/words/trends.d.ts.map +1 -1
  88. package/dist/words/trends.js +5 -3
  89. package/dist/words/trends.js.map +1 -1
  90. package/package.json +3 -3
  91. package/src/adapters/journal-columns.ts +20 -6
  92. package/src/adapters/kpi-tiles.ts +37 -16
  93. package/src/adapters/transcript/sections.ts +10 -2
  94. package/src/elements/lists/ca-contracts.ts +25 -19
  95. package/src/elements/overview/ca-kpis.ts +4 -2
  96. package/src/elements/overview/ca-model-usage.ts +3 -2
  97. package/src/elements/overview/ca-overview.ts +49 -79
  98. package/src/elements/overview/ca-pipeline-graph.ts +11 -6
  99. package/src/elements/overview/ca-run-driver.ts +5 -6
  100. package/src/elements/overview/ca-workers.ts +1 -1
  101. package/src/elements/overview/pipeline-graph/strings.ts +3 -3
  102. package/src/elements/shared/ca-definition.ts +1 -2
  103. package/src/elements/shell/ca-hosts.ts +1 -2
  104. package/src/elements/shell/ca-monitor.ts +19 -8
  105. package/src/elements/shell/ca-run-picker.ts +1 -1
  106. package/src/elements/shell/ca-theme-switch.ts +6 -1
  107. package/src/elements/task/ca-task-view.ts +2 -2
  108. package/src/elements/task/ca-transcript.ts +7 -2
  109. package/src/elements/task/header.ts +24 -14
  110. package/src/elements/task/panes/io.ts +14 -21
  111. package/src/elements/trends/ca-trends.ts +2 -1
  112. package/src/locales/ja.ts +16 -29
  113. package/src/words/quota.ts +6 -0
  114. package/src/words/run-line.ts +8 -5
  115. package/src/words/trends.ts +5 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contraagent/ui",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Web components for the contraagent monitor: transcripts, task tables, the journal, plans, contracts. Framework-free (Lit), typed by @contraagent/api",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -109,8 +109,8 @@
109
109
  "./dist/elements/**"
110
110
  ],
111
111
  "dependencies": {
112
- "@contraagent/api": "0.8.0",
113
- "@contraagent/elements": "0.8.0"
112
+ "@contraagent/api": "0.9.0",
113
+ "@contraagent/elements": "0.9.0"
114
114
  },
115
115
  "peerDependencies": {
116
116
  "@lit/localize": "^0.12.2",
@@ -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
  {
@@ -196,7 +210,7 @@ export function journalColumns(ctx: JournalColumnsContext): DataColumn<JournalRo
196
210
  >${safetyStopCount(stops.length)}</span
197
211
  >`
198
212
  : nothing}${unread
199
- ? html` <span class="unread nw" title=${msg(str`Event types this contraagent version does not know (unread_events): ${unread}`)}
213
+ ? html` <span class="unread nw" title=${msg(str`Events this contraagent version could not read (unread_events): ${unread}`)}
200
214
  ><span aria-hidden="true">⚠</span> ${msg('Unrecognized events')}</span
201
215
  >`
202
216
  : nothing}${unknown
@@ -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>