@contraagent/ui 0.6.2 → 0.7.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 (164) hide show
  1. package/custom-elements.json +24 -3
  2. package/dist/adapters/forecast.d.ts +20 -0
  3. package/dist/adapters/forecast.d.ts.map +1 -0
  4. package/dist/adapters/forecast.js +35 -0
  5. package/dist/adapters/forecast.js.map +1 -0
  6. package/dist/adapters/graph/compound.d.ts.map +1 -1
  7. package/dist/adapters/graph/compound.js +7 -5
  8. package/dist/adapters/graph/compound.js.map +1 -1
  9. package/dist/adapters/graph/highlight.d.ts +21 -5
  10. package/dist/adapters/graph/highlight.d.ts.map +1 -1
  11. package/dist/adapters/graph/highlight.js +49 -21
  12. package/dist/adapters/graph/highlight.js.map +1 -1
  13. package/dist/adapters/graph/input.d.ts +3 -1
  14. package/dist/adapters/graph/input.d.ts.map +1 -1
  15. package/dist/adapters/graph/input.js +7 -1
  16. package/dist/adapters/graph/input.js.map +1 -1
  17. package/dist/adapters/graph/project.d.ts +5 -3
  18. package/dist/adapters/graph/project.d.ts.map +1 -1
  19. package/dist/adapters/graph/project.js +13 -9
  20. package/dist/adapters/graph/project.js.map +1 -1
  21. package/dist/adapters/graph/selection.js +1 -1
  22. package/dist/adapters/graph/selection.js.map +1 -1
  23. package/dist/adapters/graph/states.d.ts.map +1 -1
  24. package/dist/adapters/graph/states.js +1 -1
  25. package/dist/adapters/graph/states.js.map +1 -1
  26. package/dist/adapters/graph/types.d.ts +12 -4
  27. package/dist/adapters/graph/types.d.ts.map +1 -1
  28. package/dist/adapters/graph/types.js +2 -0
  29. package/dist/adapters/graph/types.js.map +1 -1
  30. package/dist/adapters/graph/words.d.ts +5 -4
  31. package/dist/adapters/graph/words.d.ts.map +1 -1
  32. package/dist/adapters/graph/words.js +11 -9
  33. package/dist/adapters/graph/words.js.map +1 -1
  34. package/dist/adapters/index.d.ts +1 -0
  35. package/dist/adapters/index.d.ts.map +1 -1
  36. package/dist/adapters/index.js +1 -0
  37. package/dist/adapters/index.js.map +1 -1
  38. package/dist/adapters/journal-columns.d.ts.map +1 -1
  39. package/dist/adapters/journal-columns.js +4 -1
  40. package/dist/adapters/journal-columns.js.map +1 -1
  41. package/dist/adapters/kpi-tiles.d.ts.map +1 -1
  42. package/dist/adapters/kpi-tiles.js +1 -0
  43. package/dist/adapters/kpi-tiles.js.map +1 -1
  44. package/dist/contraagent-ui.js +674 -657
  45. package/dist/core/scroll.d.ts +13 -1
  46. package/dist/core/scroll.d.ts.map +1 -1
  47. package/dist/core/scroll.js +46 -12
  48. package/dist/core/scroll.js.map +1 -1
  49. package/dist/elements/overview/ca-overview.d.ts.map +1 -1
  50. package/dist/elements/overview/ca-overview.js +3 -3
  51. package/dist/elements/overview/ca-overview.js.map +1 -1
  52. package/dist/elements/overview/ca-pipeline-graph.d.ts +6 -1
  53. package/dist/elements/overview/ca-pipeline-graph.d.ts.map +1 -1
  54. package/dist/elements/overview/ca-pipeline-graph.js +30 -13
  55. package/dist/elements/overview/ca-pipeline-graph.js.map +1 -1
  56. package/dist/elements/overview/pipeline-graph/crumbs.d.ts +5 -0
  57. package/dist/elements/overview/pipeline-graph/crumbs.d.ts.map +1 -1
  58. package/dist/elements/overview/pipeline-graph/crumbs.js +27 -1
  59. package/dist/elements/overview/pipeline-graph/crumbs.js.map +1 -1
  60. package/dist/elements/overview/pipeline-graph/inspector.d.ts.map +1 -1
  61. package/dist/elements/overview/pipeline-graph/inspector.js +39 -5
  62. package/dist/elements/overview/pipeline-graph/inspector.js.map +1 -1
  63. package/dist/elements/overview/pipeline-graph/outline.js +1 -1
  64. package/dist/elements/overview/pipeline-graph/outline.js.map +1 -1
  65. package/dist/elements/overview/pipeline-graph/strings.d.ts.map +1 -1
  66. package/dist/elements/overview/pipeline-graph/strings.js +6 -3
  67. package/dist/elements/overview/pipeline-graph/strings.js.map +1 -1
  68. package/dist/elements/overview/pipeline-graph/styles.d.ts.map +1 -1
  69. package/dist/elements/overview/pipeline-graph/styles.js +6 -2
  70. package/dist/elements/overview/pipeline-graph/styles.js.map +1 -1
  71. package/dist/elements/shell/ca-monitor.d.ts +2 -0
  72. package/dist/elements/shell/ca-monitor.d.ts.map +1 -1
  73. package/dist/elements/shell/ca-monitor.js +11 -0
  74. package/dist/elements/shell/ca-monitor.js.map +1 -1
  75. package/dist/elements/shell/ca-run-picker.d.ts.map +1 -1
  76. package/dist/elements/shell/ca-run-picker.js +2 -0
  77. package/dist/elements/shell/ca-run-picker.js.map +1 -1
  78. package/dist/elements/task/ca-task-view.d.ts.map +1 -1
  79. package/dist/elements/task/ca-task-view.js +4 -1
  80. package/dist/elements/task/ca-task-view.js.map +1 -1
  81. package/dist/elements/task/header.d.ts.map +1 -1
  82. package/dist/elements/task/header.js +34 -3
  83. package/dist/elements/task/header.js.map +1 -1
  84. package/dist/elements/task/panes/io.d.ts +5 -1
  85. package/dist/elements/task/panes/io.d.ts.map +1 -1
  86. package/dist/elements/task/panes/io.js +56 -8
  87. package/dist/elements/task/panes/io.js.map +1 -1
  88. package/dist/elements/task/styles.d.ts.map +1 -1
  89. package/dist/elements/task/styles.js +6 -2
  90. package/dist/elements/task/styles.js.map +1 -1
  91. package/dist/lib/tone.d.ts.map +1 -1
  92. package/dist/lib/tone.js +2 -0
  93. package/dist/lib/tone.js.map +1 -1
  94. package/dist/locales/ja.d.ts +47 -5
  95. package/dist/locales/ja.d.ts.map +1 -1
  96. package/dist/locales/ja.js +47 -5
  97. package/dist/locales/ja.js.map +1 -1
  98. package/dist/words/flow-calls.d.ts +12 -0
  99. package/dist/words/flow-calls.d.ts.map +1 -0
  100. package/dist/words/flow-calls.js +31 -0
  101. package/dist/words/flow-calls.js.map +1 -0
  102. package/dist/words/forecast.d.ts +16 -0
  103. package/dist/words/forecast.d.ts.map +1 -0
  104. package/dist/words/forecast.js +17 -0
  105. package/dist/words/forecast.js.map +1 -0
  106. package/dist/words/format.d.ts +10 -0
  107. package/dist/words/format.d.ts.map +1 -1
  108. package/dist/words/format.js +17 -0
  109. package/dist/words/format.js.map +1 -1
  110. package/dist/words/index.d.ts +4 -0
  111. package/dist/words/index.d.ts.map +1 -1
  112. package/dist/words/index.js +4 -0
  113. package/dist/words/index.js.map +1 -1
  114. package/dist/words/lineage.d.ts +8 -0
  115. package/dist/words/lineage.d.ts.map +1 -0
  116. package/dist/words/lineage.js +35 -0
  117. package/dist/words/lineage.js.map +1 -0
  118. package/dist/words/outcome.d.ts.map +1 -1
  119. package/dist/words/outcome.js +4 -0
  120. package/dist/words/outcome.js.map +1 -1
  121. package/dist/words/rerun.d.ts +24 -0
  122. package/dist/words/rerun.d.ts.map +1 -0
  123. package/dist/words/rerun.js +41 -0
  124. package/dist/words/rerun.js.map +1 -0
  125. package/dist/words/stop.js +2 -0
  126. package/dist/words/stop.js.map +1 -1
  127. package/package.json +3 -3
  128. package/src/adapters/forecast.ts +43 -0
  129. package/src/adapters/graph/compound.ts +10 -9
  130. package/src/adapters/graph/highlight.ts +53 -22
  131. package/src/adapters/graph/input.ts +10 -2
  132. package/src/adapters/graph/plans.fixture.ts +2 -2
  133. package/src/adapters/graph/project.ts +16 -12
  134. package/src/adapters/graph/selection.ts +1 -1
  135. package/src/adapters/graph/states.ts +1 -1
  136. package/src/adapters/graph/types.ts +14 -4
  137. package/src/adapters/graph/words.ts +17 -12
  138. package/src/adapters/index.ts +1 -0
  139. package/src/adapters/journal-columns.ts +4 -1
  140. package/src/adapters/kpi-tiles.ts +1 -0
  141. package/src/core/scroll.ts +43 -11
  142. package/src/elements/overview/ca-overview.ts +3 -2
  143. package/src/elements/overview/ca-pipeline-graph.ts +34 -18
  144. package/src/elements/overview/pipeline-graph/crumbs.ts +28 -0
  145. package/src/elements/overview/pipeline-graph/inspector.ts +45 -5
  146. package/src/elements/overview/pipeline-graph/outline.ts +1 -1
  147. package/src/elements/overview/pipeline-graph/strings.ts +6 -3
  148. package/src/elements/overview/pipeline-graph/styles.ts +6 -2
  149. package/src/elements/shell/ca-monitor.ts +9 -0
  150. package/src/elements/shell/ca-run-picker.ts +1 -0
  151. package/src/elements/task/ca-task-view.ts +7 -2
  152. package/src/elements/task/header.ts +34 -3
  153. package/src/elements/task/panes/io.ts +72 -8
  154. package/src/elements/task/styles.ts +6 -2
  155. package/src/lib/tone.ts +2 -0
  156. package/src/locales/ja.ts +47 -5
  157. package/src/words/flow-calls.ts +36 -0
  158. package/src/words/forecast.ts +28 -0
  159. package/src/words/format.ts +19 -0
  160. package/src/words/index.ts +4 -0
  161. package/src/words/lineage.ts +41 -0
  162. package/src/words/outcome.ts +4 -0
  163. package/src/words/rerun.ts +61 -0
  164. package/src/words/stop.ts +2 -0
@@ -16,8 +16,8 @@
16
16
  import { isPython, type FlowCallNode, type Plan } from '@contraagent/api';
17
17
  import { cmpPasses, indexOf, loopsAt, passesAt, placeOf, push, shortName, startsWith, type FlowIndex, type FlowTask } from './flow-index.js';
18
18
  import { arrange, extentOf } from '@contraagent/elements/graph';
19
- import { projectTasks, shapeOf, type Projection } from './project.js';
20
- import type { FlowBox, GraphEdge, GraphNode, GraphStub, LayerModel, LoopBox, NodeCounts } from './types.js';
19
+ import { projectTasks, shapeOf, stronger, type Projection } from './project.js';
20
+ import type { EdgeType, FlowBox, GraphEdge, GraphNode, GraphStub, LayerModel, LoopBox, NodeCounts } from './types.js';
21
21
 
22
22
  /** Between a function's place and the id of an item drawn inside it. */
23
23
  export const IN_SEP = '::';
@@ -95,7 +95,7 @@ export function placeStats(ix: FlowIndex, focus: readonly string[], live = false
95
95
  const key = placeKey(layer, p.id);
96
96
  let s = out.get(key);
97
97
  if (!s) {
98
- const counts: NodeCounts = { total: 0, done: 0, partial: 0, failed: 0, running: 0, pending: 0, skipped: 0, blocked: 0 };
98
+ const counts: NodeCounts = { total: 0, done: 0, partial: 0, failed: 0, running: 0, pending: 0, skipped: 0, blocked: 0, cancelled: 0 };
99
99
  const file = d > 0 ? (t.frames[d - 1]?.file ?? '') : '';
100
100
  s = { key, layer, id: p.id, name: p.name, depth: d - d0, loops: p.loops, file, counts, tasks: 0, agent: 0, failed: 0, running: 0, stuck: 0, errors: false, kids: new Set(), fns: new Set(), parent };
101
101
  out.set(key, s);
@@ -308,17 +308,17 @@ function assemble(ix: FlowIndex, focus: readonly string[], inside: readonly Flow
308
308
  };
309
309
  const layerAt = (t: FlowTask): string[] => t.path.slice(0, (containerOf.get(t.n.key) as { depth: number }).depth);
310
310
  const inOrder = new Set<string>();
311
- const pairs = new Map<string, { from: string; to: string; type: 'after' | 'created'; tasks: Set<string>; samePass: boolean }>();
312
- const stubs = new Map<string, { id: string; label: string; to: string; type: 'after' | 'created'; tasks: Set<string> }>();
311
+ const pairs = new Map<string, { from: string; to: string; type: EdgeType; tasks: Set<string>; samePass: boolean }>();
312
+ const stubs = new Map<string, { id: string; label: string; to: string; type: EdgeType; tasks: Set<string> }>();
313
313
  for (const t of inside) {
314
314
  const to = drawnOf(t);
315
- const link = (dep: string, type: 'after' | 'created') => {
315
+ const link = (dep: string, type: EdgeType) => {
316
316
  const o = ix.byKey.get(dep);
317
317
  if (!o) return;
318
318
  if (startsWith(o.path, focus)) {
319
319
  const from = drawnOf(o);
320
320
  if (from === to) {
321
- if (type === 'after') inOrder.add(to);
321
+ if (type !== 'created') inOrder.add(to);
322
322
  return;
323
323
  }
324
324
  // The loops that matter are those of the deepest layer both are drawn in.
@@ -344,15 +344,16 @@ function assemble(ix: FlowIndex, focus: readonly string[], inside: readonly Flow
344
344
  const sk = `${s.id}\u0000${to}\u0000${type}`;
345
345
  (stubs.get(sk) ?? stubs.set(sk, { ...s, to, type, tasks: new Set() }).get(sk))?.tasks.add(t.n.key);
346
346
  };
347
+ for (const dep of t.n.given_from ?? []) link(dep, 'given');
347
348
  for (const dep of t.n.after ?? []) link(dep, 'after');
348
349
  for (const dep of t.from) link(dep, 'created');
349
350
  }
350
351
  for (const n of nodes) n.inOrder = inOrder.has(n.id);
351
352
  const edges: GraphEdge[] = [...pairs.values()]
352
- .filter((e) => e.type === 'after' || !pairs.has(`${e.from}\u0000${e.to}\u0000after`))
353
+ .filter((e) => !stronger(e.type).some((x) => pairs.has(`${e.from}\u0000${e.to}\u0000${x}`)))
353
354
  .map((e) => ({ from: e.from, to: e.to, tasks: e.tasks.size, type: e.type, samePass: e.samePass, back: false }));
354
355
  const stubList: GraphStub[] = [...stubs.values()]
355
- .filter((s) => s.type === 'after' || !stubs.has(`${s.id}\u0000${s.to}\u0000after`))
356
+ .filter((s) => !stronger(s.type).some((x) => stubs.has(`${s.id}\u0000${s.to}\u0000${x}`)))
356
357
  .map((s) => ({ id: s.id, label: s.label, to: s.to, tasks: s.tasks.size, type: s.type }));
357
358
 
358
359
  return { path: [...focus], nodes, boxes, fns: [...fns.values()], edges, stubs: stubList, units };
@@ -1,40 +1,71 @@
1
1
  // What a hovered, focused or selected mark highlights: its unit's tasks and every
2
- // task joined to them.
2
+ // task joined to them, each with how it is joined.
3
3
  import type { PlanNode } from '@contraagent/api';
4
+ import { EDGE_TYPES, type EdgeType } from './types.js';
4
5
 
5
- /** The plan's tasks by key, and the tasks that wait for each task. */
6
+ /** One link of a task to an earlier one: the earlier task's key, and how they are joined. */
7
+ interface Link {
8
+ key: string;
9
+ type: EdgeType;
10
+ }
11
+
12
+ /** The plan's tasks by key, and the links of each task to its earlier and to its later tasks. */
6
13
  export interface TaskLinks {
7
14
  byKey: ReadonlyMap<string, PlanNode>;
8
- waiters: ReadonlyMap<string, readonly string[]>;
15
+ earlier: ReadonlyMap<string, readonly Link[]>;
16
+ later: ReadonlyMap<string, readonly Link[]>;
9
17
  }
10
18
 
11
19
  export function taskLinks(nodes: readonly PlanNode[]): TaskLinks {
12
- const waiters = new Map<string, string[]>();
13
- for (const n of nodes) for (const d of n.after ?? []) (waiters.get(d) ?? waiters.set(d, []).get(d))?.push(n.key);
14
- return { byKey: new Map(nodes.map((n) => [n.key, n])), waiters };
20
+ const earlier = new Map<string, Link[]>();
21
+ const later = new Map<string, Link[]>();
22
+ for (const n of nodes) {
23
+ const ls = linksOf(n);
24
+ earlier.set(n.key, ls);
25
+ for (const l of ls) (later.get(l.key) ?? later.set(l.key, []).get(l.key))?.push({ key: n.key, type: l.type });
26
+ }
27
+ return { byKey: new Map(nodes.map((n) => [n.key, n])), earlier, later };
15
28
  }
16
29
 
17
30
  /**
18
31
  * The tasks to highlight for `hl` (a unit, or the key of a task without one): the
19
- * unit's own, and every task joined to them upstream or downstream (a task without a
20
- * unit, another unit's).
32
+ * unit's own (null), and every task joined to them upstream or downstream (a task
33
+ * without a unit, another unit's), each with the weakest link on the strongest path
34
+ * that joins it: `given` when outputs alone lead to it, `after` when order does,
35
+ * `created` when it takes a task created after a result.
21
36
  */
22
- export function related(nodes: readonly PlanNode[], links: TaskLinks, hl: string): Set<string> {
37
+ export function related(nodes: readonly PlanNode[], links: TaskLinks, hl: string): Map<string, EdgeType | null> {
23
38
  const seeds = nodes.filter((n) => (n.unit ? n.unit === hl : n.key === hl)).map((n) => n.key);
24
- const on = new Set(seeds);
25
- const walk = (next: (key: string) => readonly string[]) => {
26
- const seen = new Set(seeds);
27
- const queue = [...seeds];
28
- for (let k = queue.pop(); k !== undefined; k = queue.pop()) {
29
- for (const x of next(k)) {
30
- if (seen.has(x)) continue;
31
- seen.add(x);
32
- on.add(x);
33
- queue.push(x);
39
+ const on = new Map<string, EdgeType | null>(seeds.map((k) => [k, null]));
40
+ // One walk per type, the strongest first, each over the links of that type and the stronger
41
+ // ones: what a walk reaches first is joined by that type.
42
+ for (const [i, type] of EDGE_TYPES.entries()) {
43
+ const allowed = new Set(EDGE_TYPES.slice(0, i + 1));
44
+ for (const next of [links.earlier, links.later]) {
45
+ const seen = new Set(seeds);
46
+ const queue = [...seeds];
47
+ for (let k = queue.pop(); k !== undefined; k = queue.pop()) {
48
+ for (const l of next.get(k) ?? []) {
49
+ if (!allowed.has(l.type) || seen.has(l.key)) continue;
50
+ seen.add(l.key);
51
+ if (!on.has(l.key)) on.set(l.key, type);
52
+ queue.push(l.key);
53
+ }
34
54
  }
35
55
  }
36
- };
37
- walk((k) => links.waiters.get(k) ?? []);
38
- walk((k) => links.byKey.get(k)?.after ?? []);
56
+ }
39
57
  return on;
40
58
  }
59
+
60
+ /**
61
+ * A task's links to the tasks it comes after, each key once by its strongest link: given
62
+ * their outputs (`given_from`), waiting for them only (`after`), created after their
63
+ * results (`created_from`).
64
+ */
65
+ export function linksOf(n: PlanNode): Link[] {
66
+ const out = new Map<string, EdgeType>();
67
+ for (const k of n.given_from ?? []) out.set(k, 'given');
68
+ for (const k of n.after ?? []) if (!out.has(k)) out.set(k, 'after');
69
+ for (const k of n.created_from ?? []) if (!out.has(k)) out.set(k, 'created');
70
+ return [...out].map(([key, type]) => ({ key, type }));
71
+ }
@@ -1,12 +1,14 @@
1
1
  // A layer of the run's flow as `<cae-layered-graph>` takes it, and a loop's passes
2
2
  // as `<cae-mark-matrix>` takes them: the pipeline graph's words on the generic model.
3
- import { deriveRun, type JournalRow, type Plan } from '@contraagent/api';
3
+ import { deriveRun, type Forecast, type JournalRow, type Plan } from '@contraagent/api';
4
4
  import type { GraphClusterInput, GraphInput, GraphMark, GraphNodeInput, GraphOrder } from '@contraagent/elements/graph';
5
5
  import type { MarkMatrixInput, MatrixHeadCell, MatrixMark } from '@contraagent/elements/matrix';
6
6
  import type { Fact } from '@contraagent/elements/primitives';
7
7
  import { msg, str } from '@lit/localize';
8
8
  import { facts, listText } from '../../words/format.js';
9
9
  import { loopLabel, notStartedCause, passCount, passLabel, passSpread } from '../../words/index.js';
10
+ import { expectedByNode } from '../forecast.js';
11
+ import { expectedPill } from '../../words/forecast.js';
10
12
  import type { TaskLinks } from './highlight.js';
11
13
  import { passMatrix, sortRows, type MatrixSort, type PassMatrix } from './matrix.js';
12
14
  import { edgeStyle, project, type Projection } from './project.js';
@@ -22,6 +24,8 @@ export interface GraphWordsContext {
22
24
  run: string;
23
25
  live: boolean;
24
26
  links: TaskLinks;
27
+ /** The run's forecast: a kind node shows the tasks it is expected to have beyond its own. */
28
+ forecast?: Forecast | null;
25
29
  }
26
30
 
27
31
  /** A note's tone by the class of a node's fact. */
@@ -71,13 +75,17 @@ export function graphInput(m: LayerModel, x: GraphWordsContext, open = false): G
71
75
  drill: !!mk.flow,
72
76
  };
73
77
  };
78
+ const expected = expectedByNode(x.forecast, m);
74
79
  const nodes = m.nodes.map((n): GraphNodeInput => {
75
80
  const c = n.counts;
81
+ const more = expected.get(n.id);
82
+ const pill = more ? expectedPill(more) : null;
76
83
  return {
77
84
  id: n.id,
78
85
  label: n.label,
79
86
  tip: nodeTip(n),
80
- summary: nodeLabel(n, m, x.live),
87
+ summary: pill ? facts(nodeLabel(n, m, x.live), pill.label) : nodeLabel(n, m, x.live),
88
+ ...(pill ? { expected: pill } : {}),
81
89
  cluster: n.box,
82
90
  icons: iconsOf(n),
83
91
  accent: n.executor === 'agent',
@@ -1,6 +1,6 @@
1
1
  // Synthetic plans for the graph's tests, as a run writes them: a task's flow path
2
- // (outermost first) with the loops around each call site, what its flow call had
3
- // received when it was created, and its flow call. Test code only: the build leaves
2
+ // (outermost first) with the loops around each call site, the results that reached
3
+ // its flow call just before it was created, and its flow call. Test code only: the build leaves
4
4
  // `*.fixture.ts` out.
5
5
  import type { FlowCallNode, Frame, Plan, PlanNode } from '@contraagent/api';
6
6
 
@@ -8,7 +8,7 @@
8
8
  // The same rule applies at every depth. Nodes whose call sites sit in the same
9
9
  // loop of the layer's function are drawn in one loop box, a nested loop's box
10
10
  // inside it; each box counts its own loop's passes (`flow[].loops[].pass`). Edges
11
- // are the tasks' `after` (data) and `created_from` (created after those results),
11
+ // are the tasks' `given_from` (data), `after` (order only) and `created_from` (created after those results),
12
12
  // projected onto the layer's nodes; an edge from a task outside the layer is an
13
13
  // incoming stub. Inside each node, one mark per unit, in the same unit order
14
14
  // everywhere in the layer.
@@ -19,7 +19,7 @@ import { isPython, type FlowCallNode, type Plan, type PlanNode, type TaskState,
19
19
  import type { Arrangement, EdgeStyle, GraphShape } from '@contraagent/elements/graph';
20
20
  import { boxChain, cmpPasses, indexOf, instanceOf, loopsAt, passesAt, placeOf, push, shortName, startsWith, type FlowIndex, type FlowTask } from './flow-index.js';
21
21
  import { worstOf, worstState } from './states.js';
22
- import { LOOP_SEGMENT, type GraphNode, type LayerGraph, type LayerMark, type LayerModel, type LoopBox, type NodeCounts } from './types.js';
22
+ import { EDGE_TYPES, LOOP_SEGMENT, type EdgeType, type GraphNode, type LayerGraph, type LayerMark, type LayerModel, type LoopBox, type NodeCounts } from './types.js';
23
23
 
24
24
  /** Who runs a kind node's tasks: a coding agent, Python, or both (`mixed`). */
25
25
  function executorOf(ns: readonly PlanNode[]): 'agent' | 'python' | 'mixed' {
@@ -124,7 +124,7 @@ export function deadlineOf(box: TimeBoxRecord): string {
124
124
 
125
125
  /** Tasks by state. */
126
126
  function taskCounts(ts: readonly FlowTask[]): NodeCounts {
127
- const c: NodeCounts = { total: ts.length, done: 0, partial: 0, failed: 0, running: 0, pending: 0, skipped: 0, blocked: 0 };
127
+ const c: NodeCounts = { total: ts.length, done: 0, partial: 0, failed: 0, running: 0, pending: 0, skipped: 0, blocked: 0, cancelled: 0 };
128
128
  for (const t of ts) c[t.n.state] += 1;
129
129
  return c;
130
130
  }
@@ -231,8 +231,8 @@ export function projectTasks(ix: FlowIndex, path: readonly string[], inside: rea
231
231
  // Edges, projected onto the layer's nodes; from outside it, stubs. An edge whose
232
232
  // every task joins a later pass of a loop around both ends goes back to the next pass.
233
233
  const inOrder = new Set<string>();
234
- const pairs = new Map<string, { from: string; to: string; type: 'after' | 'created'; tasks: Set<string>; samePass: boolean }>();
235
- const stubs = new Map<string, { id: string; label: string; to: string; type: 'after' | 'created'; tasks: Set<string> }>();
234
+ const pairs = new Map<string, { from: string; to: string; type: EdgeType; tasks: Set<string>; samePass: boolean }>();
235
+ const stubs = new Map<string, { id: string; label: string; to: string; type: EdgeType; tasks: Set<string> }>();
236
236
  const outside = (o: FlowTask): { id: string; label: string } => {
237
237
  let c = 0;
238
238
  while (c < d && o.path[c] === path[c]) c++;
@@ -241,10 +241,10 @@ export function projectTasks(ix: FlowIndex, path: readonly string[], inside: rea
241
241
  };
242
242
  for (const t of inside) {
243
243
  const to = nodeOfKey.get(t.n.key) as string;
244
- const link = (dep: string, type: 'after' | 'created') => {
244
+ const link = (dep: string, type: EdgeType) => {
245
245
  const from = nodeOfKey.get(dep);
246
246
  if (from === to) {
247
- if (type === 'after') inOrder.add(to);
247
+ if (type !== 'created') inOrder.add(to);
248
248
  return;
249
249
  }
250
250
  if (from !== undefined) {
@@ -266,18 +266,19 @@ export function projectTasks(ix: FlowIndex, path: readonly string[], inside: rea
266
266
  const k = `${s.id}\u0000${to}\u0000${type}`;
267
267
  (stubs.get(k) ?? stubs.set(k, { ...s, to, type, tasks: new Set() }).get(k))?.tasks.add(t.n.key);
268
268
  };
269
+ for (const dep of t.n.given_from ?? []) link(dep, 'given');
269
270
  for (const dep of t.n.after ?? []) link(dep, 'after');
270
271
  for (const dep of t.from) link(dep, 'created');
271
272
  }
272
- // A created_from edge is drawn only where no after edge joins the same two. In the order
273
+ // An edge is drawn only where no stronger one joins the same two (`EDGE_TYPES`). In the order
273
274
  // of their ends, not of the tasks that made them: the same plan draws the same graph,
274
275
  // whichever of its tasks ran first.
275
276
  const byEnds = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
276
277
  const all = [...pairs.values()]
277
- .filter((e) => e.type === 'after' || !pairs.has(`${e.from}\u0000${e.to}\u0000after`))
278
+ .filter((e) => !stronger(e.type).some((x) => pairs.has(`${e.from}\u0000${e.to}\u0000${x}`)))
278
279
  .sort((x, y) => byEnds(`${x.from}\u0000${x.to}\u0000${x.type}`, `${y.from}\u0000${y.to}\u0000${y.type}`));
279
280
  const stubList = [...stubs.values()]
280
- .filter((s) => s.type === 'after' || !stubs.has(`${s.id}\u0000${s.to}\u0000after`))
281
+ .filter((s) => !stronger(s.type).some((x) => stubs.has(`${s.id}\u0000${s.to}\u0000${x}`)))
281
282
  .sort((x, y) => byEnds(`${x.id}\u0000${x.to}\u0000${x.type}`, `${y.id}\u0000${y.to}\u0000${y.type}`));
282
283
 
283
284
  const boxes: LoopBox[] = [...boxParent.keys()].map((id) => {
@@ -332,8 +333,11 @@ export function projectTasks(ix: FlowIndex, path: readonly string[], inside: rea
332
333
  };
333
334
  }
334
335
 
335
- /** How an edge of the layer is drawn: an `after` edge solid, a `created` one dashed. */
336
- export const edgeStyle = (type: 'after' | 'created'): EdgeStyle => (type === 'after' ? 'solid' : 'dashed');
336
+ /** How an edge of the layer is drawn: a `given` edge solid (data), an `after` one dotted (order only), a `created` one dashed. */
337
+ export const edgeStyle = (type: EdgeType): EdgeStyle => (type === 'given' ? 'solid' : type === 'after' ? 'dotted' : 'dashed');
338
+
339
+ /** The edge types stronger than `type`: one of them joining the same two leaves `type` undrawn. */
340
+ export const stronger = (type: EdgeType): readonly EdgeType[] => EDGE_TYPES.slice(0, EDGE_TYPES.indexOf(type));
337
341
 
338
342
  /** What `arrange` reads of a layer: its nodes in their loop boxes, the boxes (each a loop: it cycles), and the links. */
339
343
  export function shapeOf(m: LayerModel): GraphShape {
@@ -48,7 +48,7 @@ export function parseSelection(g: LayerGraph, matrix: PassMatrix | null, sel: st
48
48
 
49
49
  /** The tasks of a layer's nodes by state, added up. */
50
50
  export function layerCounts(nodes: readonly Pick<GraphNode, 'counts'>[]): NodeCounts {
51
- const c: NodeCounts = { total: 0, done: 0, partial: 0, failed: 0, running: 0, pending: 0, skipped: 0, blocked: 0 };
51
+ const c: NodeCounts = { total: 0, done: 0, partial: 0, failed: 0, running: 0, pending: 0, skipped: 0, blocked: 0, cancelled: 0 };
52
52
  for (const n of nodes) for (const k of Object.keys(c) as (keyof NodeCounts)[]) c[k] += n.counts[k];
53
53
  return c;
54
54
  }
@@ -1,7 +1,7 @@
1
1
  // A task's states by severity: the one a mark of several tasks shows.
2
2
  import type { PlanNode, TaskState } from '@contraagent/api';
3
3
 
4
- export const SEVERITY: Record<TaskState, number> = { failed: 6, running: 5, blocked: 4, partial: 3, pending: 2, skipped: 1, done: 0 };
4
+ export const SEVERITY: Record<TaskState, number> = { failed: 7, running: 6, blocked: 5, partial: 4, pending: 3, cancelled: 2, skipped: 1, done: 0 };
5
5
 
6
6
  /** The state a mark of several tasks shows: the most severe of them. */
7
7
  export const worstState = (nodes: readonly PlanNode[]): TaskState =>
@@ -27,7 +27,7 @@ export interface LayerMark {
27
27
  deadlineAt?: string;
28
28
  }
29
29
 
30
- export type NodeCounts = Required<Pick<PlanGroup, 'total' | 'done' | 'partial' | 'failed' | 'running' | 'pending' | 'skipped' | 'blocked'>>;
30
+ export type NodeCounts = Required<Pick<PlanGroup, 'total' | 'done' | 'partial' | 'failed' | 'running' | 'pending' | 'skipped' | 'blocked' | 'cancelled'>>;
31
31
 
32
32
  export interface GraphNode {
33
33
  id: string;
@@ -96,13 +96,23 @@ export interface FlowBox {
96
96
  parts: { id: string; key: string; node: GraphNode }[];
97
97
  }
98
98
 
99
+ /** How tasks of two nodes are joined (`GraphEdge.type`), the strongest first. */
100
+ export type EdgeType = 'given' | 'after' | 'created';
101
+
102
+ /** The edge types, the strongest first: an edge is drawn only where no stronger one joins the same two. */
103
+ export const EDGE_TYPES: readonly EdgeType[] = ['given', 'after', 'created'];
104
+
99
105
  export interface GraphEdge {
100
106
  from: string;
101
107
  to: string;
102
108
  /** Tasks of `to` joined to a task of `from`. */
103
109
  tasks: number;
104
- /** `after`: waits for its output; `created`: created after its result (and does not wait for its output). */
105
- type: 'after' | 'created';
110
+ /**
111
+ * `given`: given its output (`given_from`, data); `after`: waits for it and is not given its output (order
112
+ * only); `created`: created after its result (`created_from`). Two nodes are joined by the first of these any of
113
+ * their tasks are.
114
+ */
115
+ type: EdgeType;
106
116
  /** Some task of it joins a task in the same pass of every loop around both ends: it sets the layers (else it goes back to a loop's next pass). */
107
117
  samePass: boolean;
108
118
  /** It goes back to a loop's next pass (or otherwise closes a cycle): the loop box stands for it, it is not drawn. */
@@ -115,7 +125,7 @@ export interface GraphStub {
115
125
  label: string;
116
126
  to: string;
117
127
  tasks: number;
118
- type: 'after' | 'created';
128
+ type: EdgeType;
119
129
  }
120
130
 
121
131
  /** A layer as the plan has it, before it is arranged: its nodes in the plan's order, its loop boxes without their layers. */
@@ -7,12 +7,12 @@ import { msg, str } from '@lit/localize';
7
7
  import { facts, fmtDur, kindUnit, listText } from '../../words/format.js';
8
8
  import { passesRan, stateCount, taskStatusLabel } from '../../words/index.js';
9
9
  import { SEVERITY } from './states.js';
10
- import type { GraphNode, LayerGraph, LayerMark, NodeCounts } from './types.js';
10
+ import type { EdgeType, GraphNode, LayerGraph, LayerMark, NodeCounts } from './types.js';
11
11
 
12
12
  /** The states the legend can show, in this order; it shows the ones the view has. */
13
- export const LEGEND: readonly TaskState[] = ['done', 'partial', 'running', 'pending', 'failed', 'blocked', 'skipped'];
13
+ export const LEGEND: readonly TaskState[] = ['done', 'partial', 'running', 'pending', 'failed', 'blocked', 'skipped', 'cancelled'];
14
14
 
15
- /** How each task state looks: done round, partly done half filled, running round and breathing, waiting a ring, failed square, not started hatched, skipped muted. */
15
+ /** How each task state looks: done round, partly done half filled, running round and breathing, waiting a ring, failed square, not started hatched, skipped muted, cancelled a muted ring. */
16
16
  const LOOKS: Record<TaskState, Pick<StateStyle, 'tone' | 'shape'>> = {
17
17
  done: { tone: 'done', shape: 'round' },
18
18
  partial: { tone: 'attention', shape: 'half' },
@@ -21,6 +21,7 @@ const LOOKS: Record<TaskState, Pick<StateStyle, 'tone' | 'shape'>> = {
21
21
  failed: { tone: 'failed', shape: 'square' },
22
22
  blocked: { tone: 'attention', shape: 'hatched' },
23
23
  skipped: { tone: 'muted', shape: 'round' },
24
+ cancelled: { tone: 'muted', shape: 'ring' },
24
25
  };
25
26
 
26
27
  /** The task states as the graph draws them, in the legend's order (`live`: a pending task waits; else it did not run). */
@@ -53,7 +54,7 @@ export function doneOf(c: NodeCounts): string {
53
54
 
54
55
  /** The tasks that are not done, by state, most severe first: "1 failed", "1 not started", "1 partly done". */
55
56
  export function notDone(c: NodeCounts, live: boolean): string[] {
56
- return (['failed', 'blocked', 'partial', 'running', 'pending', 'skipped'] as const).filter((s) => c[s]).map((s) => countOf(s, c[s], live));
57
+ return (['failed', 'blocked', 'partial', 'running', 'pending', 'cancelled', 'skipped'] as const).filter((s) => c[s]).map((s) => countOf(s, c[s], live));
57
58
  }
58
59
 
59
60
  /** A fact's class: failed, not started or partly done, running, or muted (waiting, not run, skipped). */
@@ -92,28 +93,32 @@ export function nodeTip(n: GraphNode): string {
92
93
  return facts(msg(str`Function ${n.name}`), n.timeBox ? msg(str`Deadline ${fmtDur(n.timeBox.deadline_s)}`) : '', doneOf(n.counts));
93
94
  }
94
95
 
95
- /** The names of the nodes `n` waits for, was created after, and feeds. */
96
- export function neighbours(n: GraphNode, g: Pick<LayerGraph, 'nodes' | 'edges' | 'stubs'>): { waits: string[]; after: string[]; feeds: string[] } {
96
+ /** The names of the nodes whose outputs `n`'s tasks are given, that they only run after, that they were created after, and that `n` feeds. */
97
+ export function neighbours(
98
+ n: GraphNode,
99
+ g: Pick<LayerGraph, 'nodes' | 'edges' | 'stubs'>,
100
+ ): { given: string[]; order: string[]; created: string[]; feeds: string[] } {
97
101
  const label = (id: string) => {
98
102
  const x = g.nodes.find((y) => y.id === id);
99
103
  return !x ? id : x.box ? msg(str`${x.label} in the loop`) : x.label;
100
104
  };
101
- const into = (type: 'after' | 'created') => [
105
+ const into = (type: EdgeType) => [
102
106
  ...g.stubs.filter((s) => s.to === n.id && s.type === type).map((s) => s.label),
103
107
  ...g.edges.filter((e) => e.to === n.id && e.type === type).map((e) => label(e.from)),
104
108
  ];
105
- return { waits: into('after'), after: into('created'), feeds: [...new Set(g.edges.filter((e) => e.from === n.id).map((e) => label(e.to)))] };
109
+ return { given: into('given'), order: into('after'), created: into('created'), feeds: [...new Set(g.edges.filter((e) => e.from === n.id).map((e) => label(e.to)))] };
106
110
  }
107
111
 
108
- /** The node for a screen reader: its counts, and the nodes it waits for, was created after and feeds. */
112
+ /** The node for a screen reader: its counts, and the nodes it is given the outputs of, runs after, was created after and feeds. */
109
113
  export function nodeLabel(n: GraphNode, g: Pick<LayerGraph, 'nodes' | 'edges' | 'stubs'>, live: boolean): string {
110
114
  const c = n.counts;
111
- const { waits, after, feeds } = neighbours(n, g);
115
+ const { given, order, created, feeds } = neighbours(n, g);
112
116
  return [
113
117
  n.type === 'flow' ? facts(msg(str`Function ${n.label}`), doneOf(c)) : facts(n.label, executorWord(n), doneOf(c)),
114
118
  ...exceptions(n, live).map(([, t]) => t),
115
- waits.length ? msg(str`Waits for: ${listText(waits)}`) : '',
116
- after.length ? msg(str`Created after: ${listText(after)}`) : '',
119
+ given.length ? msg(str`Given output of: ${listText(given)}`) : '',
120
+ order.length ? msg(str`Runs after: ${listText(order)}`) : '',
121
+ created.length ? msg(str`Created after: ${listText(created)}`) : '',
117
122
  feeds.length ? msg(str`Later tasks: ${listText(feeds)}`) : '',
118
123
  ]
119
124
  .filter(Boolean)
@@ -20,3 +20,4 @@ export { compound, chooseOpen, doi, interest, notable, placeKey, placeStats, tow
20
20
  export { LOOP_SEGMENT, type FlowBox, type GraphNode, type LayerGraph, type LayerMark, type LayerModel, type LoopBox } from './graph/types.js';
21
21
  export * from './transcript/index.js';
22
22
  export * from './preflight.js';
23
+ export * from './forecast.js';
@@ -8,7 +8,7 @@ import { msg, str } from '@lit/localize';
8
8
  import { html, nothing } from 'lit';
9
9
  import { stopTone } from '../lib/tone.js';
10
10
  import { executorIcon } from '../words/executor.js';
11
- import { facts, fmtDur, kindUnit, localTime, sentence, tokenCount, unreadEvents, usd } from '../words/format.js';
11
+ import { facts, fmtDur, kindUnit, localTime, sentence, tokenCount, unreadEvents, usageUnknown, usageUnknownWords, usd } from '../words/format.js';
12
12
  import { attemptLabel, correctionsText, credentialLabel, credentialTip, modelSwitch, notStartedCause, outcomeLabel, replayedLabel, replayedTip, stopLabel } from '../words/index.js';
13
13
  import { accountReading, readingShort } from '../words/quota.js';
14
14
  import { customColumnTip, customValue } from '../words/row-extras.js';
@@ -189,6 +189,7 @@ export function journalColumns(ctx: JournalColumnsContext): DataColumn<JournalRo
189
189
  width: 'minmax(200px,2.2fr)',
190
190
  cell: (e) => {
191
191
  const unread = unreadEvents([e]);
192
+ const unknown = usageUnknownWords(usageUnknown([e]));
192
193
  const stops = e.safety_stops ?? [];
193
194
  return html`<span class="jwhy" title=${e.reason || nothing}>${reason(e)}${stops.length
194
195
  ? html` <span class="tone-text nw" data-tone="attention" title=${[safetyStopTip(), ...stops.map(safetyStopLine)].join('\n')}
@@ -198,6 +199,8 @@ export function journalColumns(ctx: JournalColumnsContext): DataColumn<JournalRo
198
199
  ? html` <span class="unread nw" title=${msg(str`Event types this contraagent version does not know (unread_events): ${unread}`)}
199
200
  ><span aria-hidden="true">⚠</span> ${msg('Unrecognized events')}</span
200
201
  >`
202
+ : nothing}${unknown
203
+ ? html` <span class="unread nw" title=${unknown.tip}><span aria-hidden="true">⚠</span> ${unknown.label}</span>`
201
204
  : nothing}</span>`;
202
205
  },
203
206
  },
@@ -123,6 +123,7 @@ export function kpiTiles(journal: readonly JournalRow[], d: Pick<RunDerived, 'ou
123
123
  { text: c.failed ? stateCount('failed', c.failed) : '', tone: problemTone(c.failed, 'failed') },
124
124
  { text: c.not_started ? stateCount('not_started', c.not_started) : '', tone: problemTone(c.not_started, 'attention') },
125
125
  { text: c.partial ? stateCount('partial', c.partial) : '', tone: problemTone(c.partial, 'attention') },
126
+ { text: c.cancelled ? stateCount('cancelled', c.cancelled) : '', tone: 'neutral' },
126
127
  ];
127
128
  return [
128
129
  { id: 'tasks', label: msg('Tasks', { id: 'tasks', desc: 'A count of tasks' }), value: c.total, sub: [parts] },
@@ -1,9 +1,11 @@
1
1
  // A scroller that comes back where the reader left it. The position is
2
2
  // remembered in a memory (`memory.ts`: the history entry, or an embedded
3
- // monitor's own); restoring waits until the content, which arrives over the
4
- // network, is tall enough to scroll there.
3
+ // monitor's own). It is a position in the whole content, which arrives over the
4
+ // network, in parts on a slow connection or machine: restoring holds it until all
5
+ // of the content has arrived and is tall enough to scroll there.
5
6
  import { pageMemory, type Memory } from './memory.js';
6
7
 
8
+ /** How long restoring goes on once the content is complete: for what a view draws later (its own fetch), or content now shorter. */
7
9
  const GIVE_UP_MS = 6000;
8
10
  const INPUT_EVENTS = ['wheel', 'touchstart', 'pointerdown', 'keydown'] as const;
9
11
 
@@ -11,6 +13,9 @@ export class ScrollMemory {
11
13
  private el: HTMLElement | null = null;
12
14
  private frame = 0;
13
15
  private restoring = false;
16
+ private target = 0;
17
+ /** When restoring ends at the latest: counted from when the content is complete, null before. */
18
+ private until: number | null = null;
14
19
 
15
20
  constructor(
16
21
  private readonly id: string,
@@ -31,7 +36,11 @@ export class ScrollMemory {
31
36
  this.el = null;
32
37
  }
33
38
 
34
- /** Go back to the remembered position, as soon as the content allows; to the top when none is remembered. */
39
+ /**
40
+ * Go back to the remembered position, to the top when none is remembered. The
41
+ * position is held until `update` reports the content complete and it is tall
42
+ * enough, or until the reader scrolls.
43
+ */
35
44
  restore(): void {
36
45
  const el = this.el;
37
46
  if (!el) return;
@@ -41,16 +50,22 @@ export class ScrollMemory {
41
50
  el.scrollTop = 0;
42
51
  return;
43
52
  }
44
- const until = performance.now() + GIVE_UP_MS;
53
+ this.target = target;
54
+ this.until = null;
45
55
  this.restoring = true;
56
+ // Rows that arrive above the position (a list with the newest first) would move it
57
+ // down by their height (the browser's scroll anchoring); the position is one in the
58
+ // whole list, so it stays.
59
+ el.style.overflowAnchor = 'none';
46
60
  for (const t of INPUT_EVENTS) el.addEventListener(t, this.cancel, { passive: true, once: true });
47
- const step = () => {
48
- const max = el.scrollHeight - el.clientHeight;
49
- el.scrollTop = Math.min(target, max);
50
- if (max >= target || performance.now() > until) return this.cancel();
51
- this.frame = requestAnimationFrame(step);
52
- };
53
- step();
61
+ this.place();
62
+ }
63
+
64
+ /** The content changed, `complete` once all of it has arrived: while restoring, the position is set again at the next frame. */
65
+ update(complete: boolean): void {
66
+ if (!this.restoring) return;
67
+ if (complete) this.until ??= performance.now() + GIVE_UP_MS;
68
+ this.frame ||= requestAnimationFrame(this.step);
54
69
  }
55
70
 
56
71
  /** Start at the top (a new place). */
@@ -59,11 +74,28 @@ export class ScrollMemory {
59
74
  if (this.el) this.el.scrollTop = 0;
60
75
  }
61
76
 
77
+ /** Scroll as near the position as the content allows; true once restoring is over. */
78
+ private place(): boolean {
79
+ const el = this.el!;
80
+ const max = el.scrollHeight - el.clientHeight;
81
+ el.scrollTop = Math.min(this.target, max);
82
+ // While the content is still arriving, each update places it again.
83
+ return this.until !== null && (max >= this.target || performance.now() > this.until);
84
+ }
85
+
86
+ private readonly step = (): void => {
87
+ this.frame = 0;
88
+ if (!this.restoring || !this.el) return;
89
+ if (this.place()) return this.cancel();
90
+ if (this.until !== null) this.frame = requestAnimationFrame(this.step);
91
+ };
92
+
62
93
  private readonly cancel = (): void => {
63
94
  cancelAnimationFrame(this.frame);
64
95
  this.frame = 0;
65
96
  if (!this.restoring) return;
66
97
  this.restoring = false;
98
+ if (this.el) this.el.style.overflowAnchor = '';
67
99
  for (const t of INPUT_EVENTS) this.el?.removeEventListener(t, this.cancel);
68
100
  };
69
101
 
@@ -223,7 +223,7 @@ export class CaOverview extends LitElement {
223
223
  <section class="panel enter">
224
224
  <h3 title=${msg('Every task the run scheduled, by kind (plan.jsonl)')}>${msg('Pipeline')}
225
225
  <span class="r">${this.progressLine(live)}</span></h3>
226
- <ca-pipeline-graph .plan=${s.plan} .journal=${s.journal} .run=${this.run} .layer=${this.layer} .unit=${this.unit} .sel=${this.sel} ?live=${live}></ca-pipeline-graph>
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>
@@ -336,7 +336,7 @@ export class CaOverview extends LitElement {
336
336
  const r = result(plan);
337
337
  if (live || r.left) {
338
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) : '');
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
340
  }
341
341
  return facts(
342
342
  stateCount('done', r.done),
@@ -344,6 +344,7 @@ export class CaOverview extends LitElement {
344
344
  r.failed ? stateCount('failed', r.failed) : '',
345
345
  r.notStarted ? stateCount('not_started', r.notStarted) : '',
346
346
  r.partial ? stateCount('partial', r.partial) : '',
347
+ r.cancelled ? stateCount('cancelled', r.cancelled) : '',
347
348
  );
348
349
  }
349
350