@contraagent/ui 0.6.2 → 0.7.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 (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
@@ -1,6 +1,6 @@
1
- import type { JournalRow, Plan } from '@contraagent/api';
1
+ import type { Forecast, JournalRow, Plan } from '@contraagent/api';
2
2
  import '@contraagent/elements/graph/define';
3
- import { NARROW, arrange, extentOf, type CaeLayeredGraph, type GraphInput, type GraphOrder } from '@contraagent/elements/graph';
3
+ import { NARROW, arrange, extentOf, type CaeLayeredGraph, type EdgeStyle, type GraphInput, type GraphOrder, type LitMarks } from '@contraagent/elements/graph';
4
4
  import type { MarkMatrixInput } from '@contraagent/elements/matrix';
5
5
  import type { StateStyles } from '@contraagent/elements/primitives';
6
6
  import { localized, msg } from '@lit/localize';
@@ -12,16 +12,16 @@ import { related, taskLinks, type TaskLinks } from '../../adapters/graph/highlig
12
12
  import { graphInput, matrixFromPlan } from '../../adapters/graph/input.js';
13
13
  import { autoLayer, parentLayer, resolveLayer, splitLayer } from '../../adapters/graph/layer.js';
14
14
  import type { MatrixSort, PassMatrix } from '../../adapters/graph/matrix.js';
15
- import { arranged, project, type Projection } from '../../adapters/graph/project.js';
15
+ import { arranged, edgeStyle, project, type Projection } from '../../adapters/graph/project.js';
16
16
  import { markSel, parseSelection } from '../../adapters/graph/selection.js';
17
- import type { LayerGraph, LayerMark } from '../../adapters/graph/types.js';
17
+ import { EDGE_TYPES, type LayerGraph, type LayerMark } from '../../adapters/graph/types.js';
18
18
  import { taskStateStyles } from '../../adapters/graph/words.js';
19
19
  import { emit } from '../../core/events.js';
20
20
  import { getLocale } from '../../core/i18n.js';
21
21
  import { flushMemory, RememberedSet } from '../../core/memory.js';
22
22
  import { executorIcon } from '../../words/executor.js';
23
23
  import { go, leavePasses, open, primary, select, tasksOf } from './pipeline-graph/actions.js';
24
- import { crumbs, missingLine, openControls, type OpenMode } from './pipeline-graph/crumbs.js';
24
+ import { crumbs, forecastNote, missingLine, openControls, unawaitedLine, type OpenMode } from './pipeline-graph/crumbs.js';
25
25
  import type { PipelineHost } from './pipeline-graph/host.js';
26
26
  import { inspector } from './pipeline-graph/inspector.js';
27
27
  import { outline, outlineTree, type OutlineHost, type OutlineItem } from './pipeline-graph/outline.js';
@@ -115,6 +115,7 @@ export class CaPipelineGraph extends LitElement {
115
115
  static override styles = graphStyles;
116
116
  static override properties = {
117
117
  plan: { attribute: false },
118
+ forecast: { attribute: false },
118
119
  journal: { attribute: false },
119
120
  run: {},
120
121
  live: { type: Boolean },
@@ -130,6 +131,8 @@ export class CaPipelineGraph extends LitElement {
130
131
  };
131
132
 
132
133
  plan: Plan = { nodes: [], groups: [] };
134
+ /** The run's forecast (`Forecast`): a kind node shows the tasks it is expected to have beyond its own; null for none. */
135
+ forecast: Forecast | null = null;
133
136
  /** The finished attempts: they tell why a task did not start. */
134
137
  journal: JournalRow[] = [];
135
138
  /** The run id: paths in a cause are shown relative to it. */
@@ -195,7 +198,7 @@ export class CaPipelineGraph extends LitElement {
195
198
  private wide = true;
196
199
  private readonly oh: OutlineHost = this.outlineHost();
197
200
  /** What a hovered or selected mark lights, for this layer. */
198
- private lights: (key: string) => ReadonlySet<string> = () => new Set();
201
+ private lights: (key: string) => LitMarks = () => new Set();
199
202
  /** After this update, the focus goes into the new layer: on the item it came from (`cameFrom`), else the first. */
200
203
  private refocus = false;
201
204
  private cameFrom = '';
@@ -265,7 +268,7 @@ export class CaPipelineGraph extends LitElement {
265
268
  this.prevOrder = this.orders.get(path.join('/'));
266
269
  }
267
270
  const locale = getLocale();
268
- const words = structure || changed.has('journal') || changed.has('live') || locale !== this.locale;
271
+ const words = structure || changed.has('journal') || changed.has('live') || changed.has('forecast') || locale !== this.locale;
269
272
  if (words || changed.has('sort')) this.build(words, locale);
270
273
  }
271
274
 
@@ -275,7 +278,7 @@ export class CaPipelineGraph extends LitElement {
275
278
  if (!model) return;
276
279
  const proj = this.proj;
277
280
  const { path, loop } = splitLayer(this.segments);
278
- const x = { plan: this.plan, journal: this.journal, run: this.run, live: this.live, links: this.links };
281
+ const x = { plan: this.plan, journal: this.journal, run: this.run, live: this.live, links: this.links, forecast: this.forecast };
279
282
  if (words) {
280
283
  this.locale = locale;
281
284
  this.states = taskStateStyles(this.live);
@@ -429,12 +432,17 @@ export class CaPipelineGraph extends LitElement {
429
432
  if (!this.plan.nodes.length) {
430
433
  return html`<div class="blank" style="padding:24px">${this.live ? msg('No tasks scheduled yet') : msg('No tasks scheduled')}</div>`;
431
434
  }
432
- const graph = this.renderGraph();
433
- if (!this.tree.length) return graph;
434
435
  // The outline: a column beside the graph, or, on a narrow frame, a part above it that opens.
435
- return this.wide
436
- ? html`<div class="whole">${outline(this.oh, this.tree)}<div class="g">${graph}</div></div>`
437
- : html`<details class="odet"><summary>${msg('Outline of the flow')}</summary>${outline(this.oh, this.tree)}</details>${graph}`;
436
+ // The graph keeps its place in one template whichever it is, and whether there is an
437
+ // outline: a change between them (the first size report, a resize, a plan that gains a
438
+ // function) keeps the graph's element, and with it the focus, the zoom and the pan.
439
+ const side = this.wide && this.tree.length > 0;
440
+ const part = !this.tree.length
441
+ ? nothing
442
+ : side
443
+ ? outline(this.oh, this.tree)
444
+ : html`<details class="odet"><summary>${msg('Outline of the flow')}</summary>${outline(this.oh, this.tree)}</details>`;
445
+ return html`<div class="whole ${side ? 'side' : ''}">${part}<div class="g">${this.renderGraph()}</div></div>`;
438
446
  }
439
447
 
440
448
  private renderGraph() {
@@ -446,7 +454,7 @@ export class CaPipelineGraph extends LitElement {
446
454
  .renderIcon=${renderIcon} .iconStyles=${iconStyles}
447
455
  @cae-select=${this.onSelect} @cae-activate=${this.onActivate} @cae-drill=${this.onDrill} @cae-view-change=${this.onView} @cae-toggle=${this.onToggle}
448
456
  @cae-sort=${(e: CustomEvent<{ id: string }>) => (this.sort = e.detail.id as MatrixSort)}>
449
- <div slot="crumbs">${crumbs(this.h, splitLayer(this.segments).loop ? null : openControls(this.openMode(), (m) => this.setMode(m)))}${missingLine(this.missing)}</div>
457
+ <div slot="crumbs">${crumbs(this.h, splitLayer(this.segments).loop ? null : openControls(this.openMode(), (m) => this.setMode(m)))}${missingLine(this.missing)}${this.segments.join('/') === this.auto.join('/') ? html`${forecastNote(this.plan, this.forecast)}${unawaitedLine(this.h)}` : nothing}</div>
450
458
  ${s ? inspector(this.h, s) : nothing}
451
459
  </cae-layered-graph>`;
452
460
  }
@@ -582,19 +590,27 @@ const renderIcon = (name: string) => executorIcon(name === 'python');
582
590
 
583
591
  /**
584
592
  * What a hovered or selected mark lights in a layer: for a unit (or the key of a mark
585
- * without one), the marks of the tasks joined to its tasks upstream or downstream.
593
+ * without one), the marks of the tasks joined to its tasks upstream or downstream, each
594
+ * with the style of the edge of its link (a mark of several tasks, the strongest), none
595
+ * for a mark of the unit's own tasks.
586
596
  */
587
- function lightsOf(plan: Plan, links: TaskLinks, g: LayerGraph): (key: string) => ReadonlySet<string> {
597
+ function lightsOf(plan: Plan, links: TaskLinks, g: LayerGraph): (key: string) => LitMarks {
588
598
  const marks: [string, LayerMark][] = g.nodes.flatMap((n) =>
589
599
  [...n.units, ...n.single].filter((m): m is LayerMark => m !== null).map((m): [string, LayerMark] => [markSel(n.id, m), m]),
590
600
  );
591
601
  const keyOf = new Map(marks.map(([id, m]) => [id, m.key]));
592
- const memo = new Map<string, ReadonlySet<string>>();
602
+ const memo = new Map<string, LitMarks>();
593
603
  return (key) => {
594
604
  const had = memo.get(key);
595
605
  if (had) return had;
596
606
  const on = related(plan.nodes, links, keyOf.get(key) ?? key);
597
- const lit = new Set(marks.filter(([, m]) => m.keys.some((k) => on.has(k))).map(([id]) => id));
607
+ const lit = new Map<string, EdgeStyle | null>();
608
+ for (const [id, m] of marks) {
609
+ const types = m.keys.filter((k) => on.has(k)).map((k) => on.get(k) ?? null);
610
+ if (!types.length) continue;
611
+ const strongest = EDGE_TYPES.find((t) => types.includes(t));
612
+ lit.set(id, types.includes(null) || !strongest ? null : edgeStyle(strongest));
613
+ }
598
614
  memo.set(key, lit);
599
615
  return lit;
600
616
  };
@@ -1,11 +1,17 @@
1
1
  // The line above the frame: where the layer is, the pinned unit, and a layer the run does not have.
2
+ import type { Forecast, Plan } from '@contraagent/api';
2
3
  import { msg, str } from '@lit/localize';
4
+ import { forecastShown, notInPlan } from '../../../adapters/forecast.js';
5
+ import { forecastLine, forecastTip, notInPlanLine } from '../../../words/forecast.js';
3
6
  import { html, nothing, type TemplateResult } from 'lit';
4
7
  import { shortName } from '../../../adapters/graph/flow-index.js';
5
8
  import { splitLayer } from '../../../adapters/graph/layer.js';
6
9
  import { LOOP_SEGMENT } from '../../../adapters/graph/types.js';
7
10
  import { where } from '../../../adapters/graph/words.js';
8
11
  import { emit } from '../../../core/events.js';
12
+ import { kindUnit } from '../../../words/format.js';
13
+ import { unawaitedCount, unawaitedTip } from '../../../words/flow-calls.js';
14
+ import { taskChip } from '../../shared/task-chip.js';
9
15
  import { go } from './actions.js';
10
16
  import type { PipelineHost } from './host.js';
11
17
 
@@ -73,3 +79,25 @@ export function missingLine(missing: string) {
73
79
  const name = segs[segs.length - 1] || missing;
74
80
  return html`<div class="missing" role="status" title=${missing}>${msg(html`No layer <code>${name}</code> in this run`)}</div>`;
75
81
  }
82
+
83
+ /** The tasks a top flow call created that no flow awaited (`FlowCallNode.unawaited`): a count and their names, each a chip that opens it. */
84
+ export function unawaitedLine(h: PipelineHost) {
85
+ const keys = [...new Set((h.plan.flow_calls ?? []).filter((f) => f.parent === null).flatMap((f) => f.unawaited ?? []))];
86
+ if (!keys.length) return nothing;
87
+ return html`<div class="unawaited" title=${unawaitedTip()}>
88
+ <span class="meta">${unawaitedCount(keys.length)}</span>
89
+ ${keys.map((k) => {
90
+ const n = h.links.byKey.get(k);
91
+ return taskChip(h.el, n ? kindUnit(n) : k, n ?? { key: k });
92
+ })}
93
+ </div>`;
94
+ }
95
+
96
+ /** The forecast, when it expects tasks this run has not created: their count, time and cost, and the kinds not in the plan yet. */
97
+ export function forecastNote(plan: Plan, forecast: Forecast | null) {
98
+ if (!forecastShown(forecast)) return nothing;
99
+ const missing = notInPlanLine(notInPlan(forecast, plan));
100
+ return html`<div class="forecast" title=${forecastTip(forecast)}><span class="expected-mark" aria-hidden="true"></span>${forecastLine(forecast)}${missing
101
+ ? html`<span class="sep" aria-hidden="true"> | </span>${missing}`
102
+ : nothing}</div>`;
103
+ }
@@ -12,6 +12,8 @@ import { doneOf, exceptions, executorWord, neighbours, notDone, stateWord, taskS
12
12
  import { executorIcon } from '../../../words/executor.js';
13
13
  import { fmtDur, kindUnit, listText, usd } from '../../../words/format.js';
14
14
  import { deadlineAtText, passCount, passLabel, passSpread, passesRan, timeBoxFacts, timeBoxTip } from '../../../words/index.js';
15
+ import { stoppedAt, stoppedHere } from '../../../words/flow-calls.js';
16
+ import { taskChip } from '../../shared/task-chip.js';
15
17
  import { primary, select, tasksOf } from './actions.js';
16
18
  import type { PipelineHost } from './host.js';
17
19
 
@@ -39,7 +41,8 @@ function inspect(h: PipelineHost, s: Selected) {
39
41
  if (s.type === 'node') {
40
42
  const n = s.node;
41
43
  const c = n.counts;
42
- const { waits, after, feeds } = neighbours(n, h.graph);
44
+ const { given, order, created, feeds } = neighbours(n, h.graph);
45
+ const earlier = edgeRows(row, list, given, order, created);
43
46
  const states = exceptions(n, h.live).map(([cls, t]) => html`<div class=${cls}>${t}</div>`);
44
47
  if (n.type === 'flow') {
45
48
  return html`<h4 title=${n.name}>${innerIcon()}${n.label}</h4>
@@ -48,8 +51,7 @@ function inspect(h: PipelineHost, s: Selected) {
48
51
  ${row(msg('Flow calls'), n.calls)}
49
52
  ${n.timeBox ? row(msg('Deadline'), html`<span title=${timeBoxTip()}>${timeBoxFacts(n.timeBox)}</span>`) : nothing}
50
53
  ${row(msg('Tasks', { id: 'tasks', desc: 'A count of tasks' }), html`${doneOf(c)}${states}`)}
51
- ${row(msg('Waits for'), list(waits))}
52
- ${after.length ? row(msg('Created after'), list(after)) : nothing}
54
+ ${earlier}
53
55
  ${row(msg('Later tasks'), list(feeds))}
54
56
  </dl>
55
57
  <div class="acts">${link(msg(str`Inside ${n.label}`), () => primary(h, s), true)}${clear}</div>`;
@@ -59,8 +61,7 @@ function inspect(h: PipelineHost, s: Selected) {
59
61
  <dl>
60
62
  ${n.kinds.length > 1 ? row(msg('Kinds'), listText(n.kinds)) : nothing}
61
63
  ${row(msg('Tasks', { id: 'tasks', desc: 'A count of tasks' }), html`${doneOf(c)}${states}`)}
62
- ${row(msg('Waits for'), list(waits))}
63
- ${after.length ? row(msg('Created after'), list(after)) : nothing}
64
+ ${earlier}
64
65
  ${row(msg('Later tasks'), list(feeds))}
65
66
  </dl>
66
67
  <div class="acts">${link(msg(str`Tasks of ${n.label}`), () => primary(h, s), true)}${clear}</div>`;
@@ -82,6 +83,7 @@ function inspect(h: PipelineHost, s: Selected) {
82
83
  ${m.deadlineAt ? row(msg('Deadline'), html`<span title=${timeBoxTip()}>${deadlineAtText(m.deadlineAt)}</span>`) : nothing}
83
84
  ${c ? row(msg('Tasks', { id: 'tasks', desc: 'A count of tasks' }), html`${doneOf(c)}${notDone(c, h.live).map((t) => html`<div>${t}</div>`)}`) : nothing}
84
85
  ${m.error !== undefined ? row(msg('Error'), html`<span class="err">${m.error || msg('Flow call stopped with an error')}</span>`) : nothing}
86
+ ${stopRow(h, row, m.key)}
85
87
  </dl>
86
88
  <div class="acts">${link(msg(str`Inside ${shortName(m.flow)}`), () => primary(h, s), true)}${clear}</div>`;
87
89
  }
@@ -97,8 +99,46 @@ function inspect(h: PipelineHost, s: Selected) {
97
99
  ${row(msg('Attempts'), ran.length || msg('none', { id: 'none', desc: 'An empty value' }))}
98
100
  ${row(msg('Time'), ran.length ? fmtDur(wall) : msg('none', { id: 'none', desc: 'An empty value' }))}
99
101
  ${row(msg('Cost'), costs.length ? usd(costs.reduce((a, x) => a + x, 0)) : '—')}
102
+ ${task ? stoppedRow(h, row, task.key) : nothing}
100
103
  </dl>
101
104
  <div class="acts">${link(msg('Task detail'), () => primary(h, s), true)}${node.kinds.length === 1
102
105
  ? link(msg(str`Tasks of ${node.label}`), () => tasksOf(h, node))
103
106
  : nothing}${clear}</div>`;
104
107
  }
108
+
109
+ /**
110
+ * The nodes a node comes after, one row per lineage it has: given their output, run after
111
+ * them (order only), created after their results; "Given output of: none" when it has none.
112
+ */
113
+ function edgeRows(
114
+ row: (dt: string, dd: unknown) => unknown,
115
+ list: (xs: string[]) => string,
116
+ given: string[],
117
+ order: string[],
118
+ created: string[],
119
+ ) {
120
+ const none = !given.length && !order.length && !created.length;
121
+ return html`${given.length || none ? row(msg('Given output of'), list(given)) : nothing}
122
+ ${order.length ? row(msg('Runs after'), list(order)) : nothing}
123
+ ${created.length ? row(msg('Created after'), list(created)) : nothing}`;
124
+ }
125
+
126
+ /** A flow call's mark that raised: where it stopped and the failed task it stopped for, which opens. */
127
+ function stopRow(h: PipelineHost, row: (dt: string, dd: unknown) => unknown, id: string) {
128
+ const call = h.plan.flow_calls?.find((f) => f.id === id);
129
+ if (!call || call.state !== 'raised') return nothing;
130
+ const by = call.raised_by ? h.links.byKey.get(call.raised_by) : undefined;
131
+ const name = by ? kindUnit(by) : (call.raised_by ?? '');
132
+ const text = stoppedHere(call, name, h.run);
133
+ if (!text) return nothing;
134
+ return row(msg('Stopped', { id: 'flow-call-stopped', desc: 'Where a flow call stopped, and the failure that stopped it' }), html`<div>${text}</div>${call.raised_by
135
+ ? html`<div class="down">${taskChip(h.el, name, by ?? { key: call.raised_by })}</div>`
136
+ : nothing}`);
137
+ }
138
+
139
+ /** A task's mark whose failure stopped flow calls (`raised_by`): each, with where it stopped. */
140
+ function stoppedRow(h: PipelineHost, row: (dt: string, dd: unknown) => unknown, key: string) {
141
+ const calls = (h.plan.flow_calls ?? []).filter((f) => f.raised_by === key);
142
+ if (!calls.length) return nothing;
143
+ return row(msg('Stopped', { id: 'flow-call-stopped', desc: 'Where a flow call stopped, and the failure that stopped it' }), calls.map((f) => html`<div>${stoppedAt(f, h.run)}</div>`));
144
+ }
@@ -32,7 +32,7 @@ export interface OutlineItem {
32
32
  parts: { id: string; layer: string[]; name: string }[];
33
33
  }
34
34
 
35
- const ZERO = (): NodeCounts => ({ total: 0, done: 0, partial: 0, failed: 0, running: 0, pending: 0, skipped: 0, blocked: 0 });
35
+ const ZERO = (): NodeCounts => ({ total: 0, done: 0, partial: 0, failed: 0, running: 0, pending: 0, skipped: 0, blocked: 0, cancelled: 0 });
36
36
 
37
37
  /** The outline of the flow: the places of `stats` (from the top of the flow) as a tree, a loop an item around the places in it. */
38
38
  export function outlineTree(stats: ReadonlyMap<string, PlaceStats>): OutlineItem[] {
@@ -3,6 +3,7 @@ import type { LayeredGraphStrings } from '@contraagent/elements/graph';
3
3
  import type { MarkMatrixStrings } from '@contraagent/elements/matrix';
4
4
  import { msg, str } from '@lit/localize';
5
5
  import { listText } from '../../../words/format.js';
6
+ import { lineageWord } from '../../../words/lineage.js';
6
7
 
7
8
  /**
8
9
  * Every word of the graph (`up`: Escape's hint without a selection, '' at the top;
@@ -29,8 +30,9 @@ export function graphStrings(up: boolean, functions: boolean): Omit<LayeredGraph
29
30
  stubTip: msg('Outside this layer'),
30
31
  more: (n) => msg(str`+${n}`),
31
32
  badge: msg('number of passes'),
32
- solidEdge: msg('waits for output'),
33
- dashedEdge: msg('created after result'),
33
+ solidEdge: lineageWord('given_from'),
34
+ dottedEdge: lineageWord('after'),
35
+ dashedEdge: lineageWord('created_from'),
34
36
  cycle: msg('next pass'),
35
37
  drill: msg('has a level below'),
36
38
  open: msg('Show inside'),
@@ -38,7 +40,8 @@ export function graphStrings(up: boolean, functions: boolean): Omit<LayeredGraph
38
40
  iconNames: { agent: msg('agent task'), python: msg('Python task') },
39
41
  nameHeader: functions ? msg('Kind or function') : msg('Kind'),
40
42
  statusHeader: msg('Status', { id: 'status', desc: 'The status of a task: done, failed, not started' }),
41
- solidHeader: msg('Waits for'),
43
+ solidHeader: msg('Given output of'),
44
+ dottedHeader: msg('Runs after'),
42
45
  dashedHeader: msg('Created after'),
43
46
  laterHeader: msg('Later tasks'),
44
47
  none: msg('none', { id: 'none', desc: 'An empty value' }),
@@ -54,10 +54,14 @@ export const graphStyles = [
54
54
  .switch:focus-visible { outline: var(--focus-width) solid var(--focus); outline-offset: 1px; }
55
55
  .switch[aria-pressed='true'] { color: var(--text); background: var(--accent-bg); border-color: var(--accent); }
56
56
  .opens { display: inline-flex; gap: 6px; margin-left: auto; }
57
+ .forecast { font-size: 12px; color: var(--text-muted); margin: 0 0 8px; display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
58
+ .forecast .expected-mark { display: inline-block; width: 18px; height: 10px; border: 1.5px dotted var(--line2); border-radius: 999px; margin-right: 4px; }
59
+ .unawaited { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 8px; }
60
+ .card .down { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
57
61
 
58
62
  /* The outline: a column beside the graph, or a part above it on a narrow frame. */
59
- .whole { display: grid; grid-template-columns: minmax(200px, 282px) minmax(0, 1fr); gap: 14px; align-items: start; }
60
- .whole > .g { min-width: 0; }
63
+ .whole, .whole > .g { display: flex; flex-direction: column; flex: auto; min-height: 0; min-width: 0; }
64
+ .whole.side { display: grid; grid-template-columns: minmax(200px, 282px) minmax(0, 1fr); gap: 14px; align-items: start; }
61
65
  .outline { font-size: 12px; max-height: 70vh; overflow: auto; padding: 2px 6px 2px 0; border-right: 1px solid var(--line); }
62
66
  .otree, .otree ul { list-style: none; margin: 0; padding: 0; }
63
67
  .otree ul { padding-left: 10px; }
@@ -374,6 +374,15 @@ export class CaMonitor extends LitElement {
374
374
  else this.mainScroll.restore();
375
375
  this.pendingScroll = null;
376
376
  }
377
+ if (this.snap && this.viewRoute) this.mainScroll.update(this.complete());
378
+ }
379
+
380
+ /** Whether all of what the place shows has arrived: the run's replay, the trends asked for, the hosts. */
381
+ private complete(): boolean {
382
+ const { snap, viewRoute } = this;
383
+ if (viewRoute.page === 'hosts') return snap.hosts !== null;
384
+ if (viewRoute.page === 'trends') return !!snap.trends?.data && !snap.trends.loading;
385
+ return snap.run === viewRoute.run && snap.state.ready;
377
386
  }
378
387
 
379
388
  /** Move the line under the selected tab when the tab bar changes size. */
@@ -231,6 +231,7 @@ export class CaRunPicker extends LitElement {
231
231
  if (t.not_started) parts.push(stateCount('not_started', t.not_started));
232
232
  if (t.partial) parts.push(stateCount('partial', t.partial));
233
233
  if (t.skipped) parts.push(stateCount('skipped', t.skipped));
234
+ if (t.cancelled) parts.push(stateCount('cancelled', t.cancelled));
234
235
  if (t.running) parts.push(stateCount('running', t.running));
235
236
  if (t.waiting) parts.push(stateCount('waiting', t.waiting));
236
237
  if (t.not_run) parts.push(stateCount('not_run', t.not_run));
@@ -1,4 +1,4 @@
1
- import { deriveRun, type JournalRow, type LaunchEvent, type MonitorClient, type Plan, type TaskDetail, type TaskOutcome } from '@contraagent/api';
1
+ import { deriveRun, type JournalRow, type LaunchEvent, type MonitorClient, type Plan, type TaskDetail, type TaskOutcome, type UpstreamTask } from '@contraagent/api';
2
2
  import { localized, msg } from '@lit/localize';
3
3
  import { LitElement, html, nothing, type PropertyValues } from 'lit';
4
4
  import { customElement, property, query, state } from 'lit/decorators.js';
@@ -129,7 +129,9 @@ export class CaTaskView extends LitElement {
129
129
  const io = d.io;
130
130
  const py = isPy(d) && !d.stream;
131
131
  const n: Partial<Record<TaskView, number>> = {
132
- input: new Set([...io.inputs.map((o) => o.path), ...io.upstream.flatMap((u) => u.outputs.map((o) => o.path))]).size + Object.keys(io.params).length,
132
+ input:
133
+ new Set([...io.inputs.map((o) => o.path), ...(io.input_files ?? []).map((f) => f.path), ...io.upstream.filter(readsFrom).flatMap((u) => u.outputs.map((o) => o.path))]).size +
134
+ Object.keys(io.params).length,
133
135
  output: io.outputs.length,
134
136
  };
135
137
  return html`<div class="tviews" role="tablist">
@@ -155,3 +157,6 @@ declare global {
155
157
  'ca-task-view': CaTaskView;
156
158
  }
157
159
  }
160
+
161
+ /** An earlier task this one reads from or waits for (not one it was only created after); every one of an export written before lineage. */
162
+ const readsFrom = (u: UpstreamTask): boolean => !u.lineage?.length || u.lineage.some((l) => l !== 'created_from');
@@ -5,12 +5,14 @@ import { msg, str } from '@lit/localize';
5
5
  import { html, nothing, type TemplateResult } from 'lit';
6
6
  import { toneAttr } from '@contraagent/elements';
7
7
  import { pill } from '@contraagent/elements/primitives';
8
- import { facts, fmtDur, kindUnit, listText, localTime, tokenCount, unreadEvents, usd } from '../../words/format.js';
8
+ import { facts, fmtDur, kindUnit, listText, localTime, tokenCount, unreadEvents, usageUnknown, usageUnknownWords, usd } from '../../words/format.js';
9
9
  import { attemptNames, deferredText, changedFiles, changedFilesTip, changeLabel, credentialLabel, credentialTip, locksLabel, locksTip, modelSwitch, outcomeLabel, priorityText, priorityTip, replayedLabel, replayedTip, stopLabel, type ChangeKind } from '../../words/index.js';
10
10
  import { contextWarnedText, contextWarnedTip, peakContextLine, peakContextTip, peakTry } from '../../words/context.js';
11
11
  import { readingShort, readingTip } from '../../words/quota.js';
12
12
  import { safetyStopCount, safetyStopLine, safetyStopTip } from '../../words/safety.js';
13
13
  import { customColumnTip, customValues } from '../../words/row-extras.js';
14
+ import { divergenceTip, divergenceWord, rerunLine, rerunOf } from '../../words/rerun.js';
15
+ import { stoppedAt } from '../../words/flow-calls.js';
14
16
  import { isStop, type Tone } from '../../lib/tone.js';
15
17
  import { executorIcon } from '../../words/executor.js';
16
18
  import '@contraagent/elements/json-tree/define';
@@ -120,6 +122,7 @@ export function taskHeader(c: TaskContext, key: string, onSearch: (query: string
120
122
  const otherModels = session ? [...new Set(byTs.map((r) => r.model).filter((x): x is string => !!x && x !== who.model))] : [];
121
123
  const first = rows[0];
122
124
  const tries = rows.filter(isLaunched);
125
+ const unknownUsage = usageUnknownWords(usageUnknown(tries));
123
126
  const extras = Object.entries(d).filter(([k]) => !SHOWN_KEYS.has(k));
124
127
  const [verdictText, tone] = verdict(c);
125
128
  const kindName = py ? msg('Python task') : msg('Agent task');
@@ -136,7 +139,8 @@ export function taskHeader(c: TaskContext, key: string, onSearch: (query: string
136
139
  </div>`
137
140
  : nothing}
138
141
  ${node?.priority ? html`<div class="meta" title=${priorityTip()}>${priorityText(node.priority)}</div>` : nothing}
139
- ${node?.rerun ? html`<div class="meta">${msg(str`Ran again after a resume: ${node.rerun}`)}</div>` : nothing}
142
+ ${rerunMeta(rows, node?.rerun)}
143
+ ${stoppedFlowCalls(c)}
140
144
  ${session
141
145
  ? html`<div class="whos">
142
146
  ${tagChips([...whoTags(who), ...identityTags(tries)])}
@@ -166,6 +170,9 @@ export function taskHeader(c: TaskContext, key: string, onSearch: (query: string
166
170
  <span aria-hidden="true">⚠</span> ${msg(str`Unrecognized events: ${unreadEvents(tries)}`)}
167
171
  </div>`
168
172
  : nothing}
173
+ ${unknownUsage
174
+ ? html`<div class="meta unread" title=${unknownUsage.tip}><span aria-hidden="true">⚠</span> ${unknownUsage.label}</div>`
175
+ : nothing}
169
176
  ${d.task_detail_error ? html`<div class="meta v-bad">${msg(str`The task_detail callback failed: ${d.task_detail_error ?? ''}`)}</div>` : nothing}
170
177
  ${launches.map(
171
178
  (lc) => html`<details class="launch">
@@ -286,6 +293,8 @@ function quotaLine(c: TaskContext, tries: JournalRow[]) {
286
293
  /** One finished attempt: how it ended, how long, its cost, its safety stops; the rest (when, corrections, model, model switch) in its tooltip. */
287
294
  function runChip(c: TaskContext, r: JournalRow, name: string, session: boolean, extras: readonly string[]) {
288
295
  const look = r.stop === 'partial' ? 'part' : r.ok ? 'ok' : 'bad';
296
+ const rows = c.detail.rows;
297
+ const rerun = rerunOf(rows, rows.indexOf(r));
289
298
  const played = !!c.replayMarks && !!r.replayed;
290
299
  const reason = runRelative(r.reason, c.run);
291
300
  const sw = modelSwitch(r.hops, null, r.skipped_hops);
@@ -300,12 +309,34 @@ function runChip(c: TaskContext, r: JournalRow, name: string, session: boolean,
300
309
  reason,
301
310
  r.route || '',
302
311
  played ? replayedTip() : '',
312
+ rerun ? rerunLine(rerun) : '',
303
313
  ...customValues(r, extras).map(([k, v]) => `${k}: ${v}`),
304
314
  ]
305
315
  .filter(Boolean)
306
316
  .join('\n');
307
317
  const cost = r.cost_usd !== null && r.cost_usd !== undefined ? usd(r.cost_usd) : '';
308
318
  return html`<span class="runchip ${look}" title=${tip}>
309
- ${mark(r)} ${facts(name, stopLabel(r.stop, r.reason), fmtDur(r.wall_s), cost, safety, played ? replayedLabel() : '')}
319
+ ${mark(r)} ${facts(name, stopLabel(r.stop, r.reason), fmtDur(r.wall_s), cost, safety, played ? replayedLabel() : '', rerun?.divergence ? divergenceWord() : '')}
310
320
  </span>`;
311
321
  }
322
+
323
+ /**
324
+ * Why a resume ran the task again, from its latest journal row with a `rerun`: the parts
325
+ * of its fingerprint that changed, and a divergence; the plan's `rerun` before its row
326
+ * arrives. The recorded reason is the tooltip.
327
+ */
328
+ function rerunMeta(rows: readonly JournalRow[], planned: string | undefined) {
329
+ const i = rows.map((r) => !!r.rerun).lastIndexOf(true);
330
+ const r = i >= 0 ? rerunOf(rows, i) : planned ? { text: planned, changed: [], divergence: planned.startsWith('divergence') } : null;
331
+ if (!r) return nothing;
332
+ return html`<div class="meta rerun" title=${r.text}>${rerunLine(r)}${r.divergence
333
+ ? html` <span class="divergence" title=${divergenceTip()}>${pill({ text: divergenceWord(), tone: 'attention' })}</span>`
334
+ : nothing}</div>`;
335
+ }
336
+
337
+ /** The flow calls this task's failure stopped (`FlowCallNode.raised_by`), each with where: "This failure stopped review_loop at flow.py:42". */
338
+ function stoppedFlowCalls(c: TaskContext) {
339
+ const calls = (c.plan?.flow_calls ?? []).filter((f) => f.raised_by === c.detail.key);
340
+ if (!calls.length) return nothing;
341
+ return html`${calls.map((f) => html`<div class="meta stopped" title=${f.error ?? ''}>${stoppedAt(f, c.run)}</div>`)}`;
342
+ }
@@ -1,7 +1,20 @@
1
1
  // The input and output panes of `ca-task-view`: what the task read (parameters,
2
2
  // files, earlier tasks, the prompt) and what it wrote (files against their
3
3
  // contracts, the outcome, tokens and cost, later tasks).
4
- import { isLaunched, isPython, runReadings, runStarts, tryQuota, type JournalRow, type Outcome, type SourcedReading } from '@contraagent/api';
4
+ import {
5
+ isLaunched,
6
+ isPython,
7
+ runReadings,
8
+ runRelative,
9
+ runStarts,
10
+ tryQuota,
11
+ type DownstreamTask,
12
+ type JournalRow,
13
+ type Lineage,
14
+ type Outcome,
15
+ type SourcedReading,
16
+ type TaskInputFile,
17
+ } from '@contraagent/api';
5
18
  import { msg, str } from '@lit/localize';
6
19
  import { html, nothing, type TemplateResult } from 'lit';
7
20
  import { pill } from '@contraagent/elements/primitives';
@@ -17,6 +30,7 @@ import { ioFileCard } from '../../shared/file-card.js';
17
30
  import { taskChip } from '../../shared/task-chip.js';
18
31
  import { inputView, notPassed, type EarlierTask, type InputFile } from '../inputs.js';
19
32
  import { roundLabel, roundWhy } from '../../../words/rounds.js';
33
+ import { inputFileCheckWord, lineageTip, lineageWord } from '../../../words/lineage.js';
20
34
  import { correctionsOf, hadSession, isPy, producerOf, stateOf, taskLink, type TaskContext } from '../context.js';
21
35
 
22
36
  const valueText = (v: unknown): string => (typeof v === 'string' ? v : JSON.stringify(v) ?? String(v));
@@ -31,7 +45,11 @@ const sameJson = (a: unknown, b: unknown): boolean => {
31
45
  /** A task's outcome as a pill, in its words. */
32
46
  const outcomeChip = (o: Outcome) => pill({ text: outcomeLabel(o), tone: outcomeTone(o), mark: outcomeMark(o) });
33
47
 
34
- /** What the task read: its parameters, its input files, the earlier tasks, and (for an agent task) its prompt. */
48
+ /**
49
+ * What the task read: its parameters, its input files (`InputFile` params), the files
50
+ * an input contract checks before it starts, the earlier tasks, and (for an agent task)
51
+ * its prompt.
52
+ */
35
53
  export function inputPane(c: TaskContext): TemplateResult {
36
54
  const d = c.detail;
37
55
  const io = d.io;
@@ -52,13 +70,37 @@ export function inputPane(c: TaskContext): TemplateResult {
52
70
  </table>`
53
71
  : html`<div class="meta">${msg('No parameters')}</div>`}
54
72
  <h4 class="sect-h">${msg('Input files')}</h4>
55
- ${files.length ? files.map((f) => inputCard(c, f)) : html`<div class="meta">${msg('No input files')}</div>`}
73
+ ${io.input_files?.length ? io.input_files.map((f) => inputFileCard(c, f)) : html`<div class="meta">${msg('No input files')}</div>`}
74
+ ${files.length
75
+ ? html`<h4 class="sect-h">${msg('Checked files it reads')}</h4>
76
+ ${files.map((f) => inputCard(c, f))}`
77
+ : nothing}
56
78
  <h4 class="sect-h">${msg('Earlier tasks')}</h4>
57
79
  ${earlier.length ? earlier.map((e) => earlierTask(c, e)) : html`<div class="meta">${msg('No earlier tasks')}</div>`}
58
80
  ${isPy(d) ? nothing : promptSection(c)}`;
59
81
  }
60
82
 
61
- /** One file the task reads: its check, and the earlier task that writes it or that it is from before the run. */
83
+ /**
84
+ * One input file of the task (an `InputFile` param): its check at the start, its path,
85
+ * the param that holds it, its hash, and the earlier task whose output file it is.
86
+ */
87
+ function inputFileCard(c: TaskContext, f: TaskInputFile) {
88
+ const tone = f.check === 'passed' ? 'done' : f.check === 'refused' ? 'failed' : 'neutral';
89
+ const mark = f.check === 'passed' ? 'done' : f.check === 'refused' ? 'failed' : 'pending';
90
+ const writer = f.writer ? (c.detail.io.upstream.find((u) => u.key === f.writer) ?? c.outcomes.get(f.writer)) : undefined;
91
+ const origin = f.writer
92
+ ? html`<span>${msg(html`Output of ${taskLink(c, writer ?? { key: f.writer, kind: f.writer, unit: '' })}`)}</span>`
93
+ : nothing;
94
+ return html`<div class="io-card one infile">
95
+ <div class="io-hd">${pill({ text: inputFileCheckWord(f.check), tone, mark })}<span class="jmono ct-path" title=${f.path}>${runRelative(f.path, c.run)}</span></div>
96
+ <div class="meta io-from"><span title=${f.sha256}>${facts(msg(str`Param ${f.param}`), `sha256 ${f.sha256.slice(0, 12)}`)}</span>${f.writer
97
+ ? html` | ${origin}`
98
+ : nothing}</div>
99
+ ${f.reason ? html`<div class="meta err">${f.reason}</div>` : nothing}
100
+ </div>`;
101
+ }
102
+
103
+ /** One file an input contract checks before the task starts: its check, and the earlier task that writes it or that it is from before the run. */
62
104
  function inputCard(c: TaskContext, f: InputFile) {
63
105
  const u = f.from;
64
106
  const state = u ? c.outcomes.get(u.key)?.outcome : undefined;
@@ -72,7 +114,11 @@ function inputCard(c: TaskContext, f: InputFile) {
72
114
  return ioFileCard({ ...f.spec, status: st }, 'input', c.run, { origin, chip });
73
115
  }
74
116
 
75
- /** One earlier task: its status, how many of its output files this task reads, and the others folded under it (open when one did not pass). */
117
+ /**
118
+ * One earlier task: its status, how this task is joined to it (its lineage, in the words of
119
+ * the graph's edges), how many of its output files this task reads, and the others folded
120
+ * under it (open when one did not pass).
121
+ */
76
122
  function earlierTask(c: TaskContext, e: EarlierTask) {
77
123
  const u = e.task;
78
124
  const kindName = isPython(u) ? msg('Python task') : msg('Agent task');
@@ -85,6 +131,7 @@ function earlierTask(c: TaskContext, e: EarlierTask) {
85
131
  ${executorIcon(isPython(u), kindName)}
86
132
  ${taskLink(c, u)}
87
133
  ${state ? outcomeChip(state) : nothing}
134
+ ${lineageTags(u.lineage ?? [])}
88
135
  <span class="meta">${summary}</span>
89
136
  </div>`;
90
137
  if (!e.rest.length) return html`<div class="upstream">${head}</div>`;
@@ -184,7 +231,24 @@ export function outputPane(c: TaskContext): TemplateResult {
184
231
  : nothing}
185
232
  ${usageTable(c, d.rows)}
186
233
  <h4 class="sect-h">${msg('Later tasks')}</h4>
187
- ${io.downstream.length
188
- ? html`<div class="down">${io.downstream.map((x) => taskChip(c.host, kindUnit(x), x))}</div>`
189
- : html`<div class="meta">${msg('No later tasks')}</div>`}`;
234
+ ${io.downstream.length ? laterTasks(c, io.downstream) : html`<div class="meta">${msg('No later tasks')}</div>`}`;
235
+ }
236
+
237
+ /** A task's lineage as tags, in the words of the graph's edges, each with the field it comes from as its tooltip. */
238
+ function lineageTags(lineage: readonly Lineage[]) {
239
+ return lineage.map((l) => html`<span class="lineage" data-lineage=${l} title=${lineageTip(l)}>${lineageWord(l)}</span>`);
240
+ }
241
+
242
+ /**
243
+ * The later tasks, a row of chips per lineage (by the strongest each has): given output,
244
+ * runs after, created after result; one row without a tag for those an export written
245
+ * before lineage holds.
246
+ */
247
+ function laterTasks(c: TaskContext, down: readonly DownstreamTask[]) {
248
+ const order: Lineage[] = ['given_from', 'after', 'created_from'];
249
+ const first = (x: DownstreamTask): Lineage | null => order.find((l) => (x.lineage ?? []).includes(l)) ?? null;
250
+ return [...order, null]
251
+ .map((l) => [l, down.filter((x) => first(x) === l)] as const)
252
+ .filter(([, xs]) => xs.length)
253
+ .map(([l, xs]) => html`<div class="down">${l ? lineageTags([l]) : nothing}${xs.map((x) => taskChip(c.host, kindUnit(x), x))}</div>`);
190
254
  }
@@ -56,8 +56,12 @@ export const taskViewStyles = css`
56
56
  /* A positioned box: what is placed in it (a screen reader's text) scrolls with it, not past the view's end. */
57
57
  .pane { flex: 1 1 auto; min-height: 0; overflow: auto; position: relative; }
58
58
  .upstream { margin-bottom: 12px; }
59
- .up-hd { display: flex; align-items: center; gap: 10px; margin: 2px 0 6px; }
60
- .down { display: flex; flex-wrap: wrap; gap: 8px; }
59
+ .up-hd { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; margin: 2px 0 6px; }
60
+ .down { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 6px; }
61
+ /* How a task is joined to an earlier one, drawn as the graph draws its edge: given output solid, runs after dotted, created after result dashed. */
62
+ .lineage { font-size: 11px; color: var(--text-2); border: 1.5px solid var(--line2); border-radius: 999px; padding: 0 7px; white-space: nowrap; }
63
+ .lineage[data-lineage='after'] { border-style: dotted; }
64
+ .lineage[data-lineage='created_from'] { border-style: dashed; }
61
65
  .notrans { flex: 1 1 auto; border: 1px solid var(--line); border-radius: 14px; }
62
66
  details.upstream > summary { cursor: pointer; list-style: none; }
63
67
  details.upstream > summary::-webkit-details-marker { display: none; }
package/src/lib/tone.ts CHANGED
@@ -37,6 +37,7 @@ const STOP_TONE: Readonly<Record<Stop, Tone>> = {
37
37
  partial: 'attention',
38
38
  invalid_input: 'attention',
39
39
  skipped: 'muted',
40
+ cancelled: 'muted',
40
41
  timeout: 'failed',
41
42
  max_turns: 'failed',
42
43
  budget: 'failed',
@@ -81,6 +82,7 @@ export function outcomeTone(o: Outcome): Tone {
81
82
  return 'running';
82
83
  case 'waiting':
83
84
  case 'skipped':
85
+ case 'cancelled':
84
86
  return 'muted';
85
87
  }
86
88
  }