@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.
- package/custom-elements.json +24 -3
- package/dist/adapters/forecast.d.ts +20 -0
- package/dist/adapters/forecast.d.ts.map +1 -0
- package/dist/adapters/forecast.js +35 -0
- package/dist/adapters/forecast.js.map +1 -0
- package/dist/adapters/graph/compound.d.ts.map +1 -1
- package/dist/adapters/graph/compound.js +7 -5
- package/dist/adapters/graph/compound.js.map +1 -1
- package/dist/adapters/graph/highlight.d.ts +21 -5
- package/dist/adapters/graph/highlight.d.ts.map +1 -1
- package/dist/adapters/graph/highlight.js +49 -21
- package/dist/adapters/graph/highlight.js.map +1 -1
- package/dist/adapters/graph/input.d.ts +3 -1
- package/dist/adapters/graph/input.d.ts.map +1 -1
- package/dist/adapters/graph/input.js +7 -1
- package/dist/adapters/graph/input.js.map +1 -1
- package/dist/adapters/graph/project.d.ts +5 -3
- package/dist/adapters/graph/project.d.ts.map +1 -1
- package/dist/adapters/graph/project.js +13 -9
- package/dist/adapters/graph/project.js.map +1 -1
- package/dist/adapters/graph/selection.js +1 -1
- package/dist/adapters/graph/selection.js.map +1 -1
- package/dist/adapters/graph/states.d.ts.map +1 -1
- package/dist/adapters/graph/states.js +1 -1
- package/dist/adapters/graph/states.js.map +1 -1
- package/dist/adapters/graph/types.d.ts +12 -4
- package/dist/adapters/graph/types.d.ts.map +1 -1
- package/dist/adapters/graph/types.js +2 -0
- package/dist/adapters/graph/types.js.map +1 -1
- package/dist/adapters/graph/words.d.ts +5 -4
- package/dist/adapters/graph/words.d.ts.map +1 -1
- package/dist/adapters/graph/words.js +11 -9
- package/dist/adapters/graph/words.js.map +1 -1
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/journal-columns.d.ts.map +1 -1
- package/dist/adapters/journal-columns.js +4 -1
- package/dist/adapters/journal-columns.js.map +1 -1
- package/dist/adapters/kpi-tiles.d.ts.map +1 -1
- package/dist/adapters/kpi-tiles.js +1 -0
- package/dist/adapters/kpi-tiles.js.map +1 -1
- package/dist/contraagent-ui.js +674 -657
- package/dist/core/scroll.d.ts +13 -1
- package/dist/core/scroll.d.ts.map +1 -1
- package/dist/core/scroll.js +46 -12
- package/dist/core/scroll.js.map +1 -1
- package/dist/elements/overview/ca-overview.d.ts.map +1 -1
- package/dist/elements/overview/ca-overview.js +3 -3
- package/dist/elements/overview/ca-overview.js.map +1 -1
- package/dist/elements/overview/ca-pipeline-graph.d.ts +6 -1
- package/dist/elements/overview/ca-pipeline-graph.d.ts.map +1 -1
- package/dist/elements/overview/ca-pipeline-graph.js +30 -13
- package/dist/elements/overview/ca-pipeline-graph.js.map +1 -1
- package/dist/elements/overview/pipeline-graph/crumbs.d.ts +5 -0
- package/dist/elements/overview/pipeline-graph/crumbs.d.ts.map +1 -1
- package/dist/elements/overview/pipeline-graph/crumbs.js +27 -1
- package/dist/elements/overview/pipeline-graph/crumbs.js.map +1 -1
- package/dist/elements/overview/pipeline-graph/inspector.d.ts.map +1 -1
- package/dist/elements/overview/pipeline-graph/inspector.js +39 -5
- package/dist/elements/overview/pipeline-graph/inspector.js.map +1 -1
- package/dist/elements/overview/pipeline-graph/outline.js +1 -1
- package/dist/elements/overview/pipeline-graph/outline.js.map +1 -1
- package/dist/elements/overview/pipeline-graph/strings.d.ts.map +1 -1
- package/dist/elements/overview/pipeline-graph/strings.js +6 -3
- package/dist/elements/overview/pipeline-graph/strings.js.map +1 -1
- package/dist/elements/overview/pipeline-graph/styles.d.ts.map +1 -1
- package/dist/elements/overview/pipeline-graph/styles.js +6 -2
- package/dist/elements/overview/pipeline-graph/styles.js.map +1 -1
- package/dist/elements/shell/ca-monitor.d.ts +2 -0
- package/dist/elements/shell/ca-monitor.d.ts.map +1 -1
- package/dist/elements/shell/ca-monitor.js +11 -0
- package/dist/elements/shell/ca-monitor.js.map +1 -1
- package/dist/elements/shell/ca-run-picker.d.ts.map +1 -1
- package/dist/elements/shell/ca-run-picker.js +2 -0
- package/dist/elements/shell/ca-run-picker.js.map +1 -1
- package/dist/elements/task/ca-task-view.d.ts.map +1 -1
- package/dist/elements/task/ca-task-view.js +4 -1
- package/dist/elements/task/ca-task-view.js.map +1 -1
- package/dist/elements/task/header.d.ts.map +1 -1
- package/dist/elements/task/header.js +34 -3
- package/dist/elements/task/header.js.map +1 -1
- package/dist/elements/task/panes/io.d.ts +5 -1
- package/dist/elements/task/panes/io.d.ts.map +1 -1
- package/dist/elements/task/panes/io.js +56 -8
- package/dist/elements/task/panes/io.js.map +1 -1
- package/dist/elements/task/styles.d.ts.map +1 -1
- package/dist/elements/task/styles.js +6 -2
- package/dist/elements/task/styles.js.map +1 -1
- package/dist/lib/tone.d.ts.map +1 -1
- package/dist/lib/tone.js +2 -0
- package/dist/lib/tone.js.map +1 -1
- package/dist/locales/ja.d.ts +47 -5
- package/dist/locales/ja.d.ts.map +1 -1
- package/dist/locales/ja.js +47 -5
- package/dist/locales/ja.js.map +1 -1
- package/dist/words/flow-calls.d.ts +12 -0
- package/dist/words/flow-calls.d.ts.map +1 -0
- package/dist/words/flow-calls.js +31 -0
- package/dist/words/flow-calls.js.map +1 -0
- package/dist/words/forecast.d.ts +16 -0
- package/dist/words/forecast.d.ts.map +1 -0
- package/dist/words/forecast.js +17 -0
- package/dist/words/forecast.js.map +1 -0
- package/dist/words/format.d.ts +10 -0
- package/dist/words/format.d.ts.map +1 -1
- package/dist/words/format.js +17 -0
- package/dist/words/format.js.map +1 -1
- package/dist/words/index.d.ts +4 -0
- package/dist/words/index.d.ts.map +1 -1
- package/dist/words/index.js +4 -0
- package/dist/words/index.js.map +1 -1
- package/dist/words/lineage.d.ts +8 -0
- package/dist/words/lineage.d.ts.map +1 -0
- package/dist/words/lineage.js +35 -0
- package/dist/words/lineage.js.map +1 -0
- package/dist/words/outcome.d.ts.map +1 -1
- package/dist/words/outcome.js +4 -0
- package/dist/words/outcome.js.map +1 -1
- package/dist/words/rerun.d.ts +24 -0
- package/dist/words/rerun.d.ts.map +1 -0
- package/dist/words/rerun.js +41 -0
- package/dist/words/rerun.js.map +1 -0
- package/dist/words/stop.js +2 -0
- package/dist/words/stop.js.map +1 -1
- package/package.json +3 -3
- package/src/adapters/forecast.ts +43 -0
- package/src/adapters/graph/compound.ts +10 -9
- package/src/adapters/graph/highlight.ts +53 -22
- package/src/adapters/graph/input.ts +10 -2
- package/src/adapters/graph/plans.fixture.ts +2 -2
- package/src/adapters/graph/project.ts +16 -12
- package/src/adapters/graph/selection.ts +1 -1
- package/src/adapters/graph/states.ts +1 -1
- package/src/adapters/graph/types.ts +14 -4
- package/src/adapters/graph/words.ts +17 -12
- package/src/adapters/index.ts +1 -0
- package/src/adapters/journal-columns.ts +4 -1
- package/src/adapters/kpi-tiles.ts +1 -0
- package/src/core/scroll.ts +43 -11
- package/src/elements/overview/ca-overview.ts +3 -2
- package/src/elements/overview/ca-pipeline-graph.ts +34 -18
- package/src/elements/overview/pipeline-graph/crumbs.ts +28 -0
- package/src/elements/overview/pipeline-graph/inspector.ts +45 -5
- package/src/elements/overview/pipeline-graph/outline.ts +1 -1
- package/src/elements/overview/pipeline-graph/strings.ts +6 -3
- package/src/elements/overview/pipeline-graph/styles.ts +6 -2
- package/src/elements/shell/ca-monitor.ts +9 -0
- package/src/elements/shell/ca-run-picker.ts +1 -0
- package/src/elements/task/ca-task-view.ts +7 -2
- package/src/elements/task/header.ts +34 -3
- package/src/elements/task/panes/io.ts +72 -8
- package/src/elements/task/styles.ts +6 -2
- package/src/lib/tone.ts +2 -0
- package/src/locales/ja.ts +47 -5
- package/src/words/flow-calls.ts +36 -0
- package/src/words/forecast.ts +28 -0
- package/src/words/format.ts +19 -0
- package/src/words/index.ts +4 -0
- package/src/words/lineage.ts +41 -0
- package/src/words/outcome.ts +4 -0
- package/src/words/rerun.ts +61 -0
- 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:
|
|
312
|
-
const stubs = new Map<string, { id: string; label: string; to: string; type:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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):
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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,
|
|
3
|
-
//
|
|
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' `
|
|
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:
|
|
235
|
-
const stubs = new Map<string, { id: string; label: string; to: string; type:
|
|
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:
|
|
244
|
+
const link = (dep: string, type: EdgeType) => {
|
|
245
245
|
const from = nodeOfKey.get(dep);
|
|
246
246
|
if (from === to) {
|
|
247
|
-
if (type
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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:
|
|
336
|
-
export const edgeStyle = (type:
|
|
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:
|
|
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
|
-
/**
|
|
105
|
-
|
|
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:
|
|
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`
|
|
96
|
-
export function neighbours(
|
|
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:
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
-
|
|
116
|
-
|
|
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)
|
package/src/adapters/index.ts
CHANGED
|
@@ -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] },
|
package/src/core/scroll.ts
CHANGED
|
@@ -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)
|
|
4
|
-
// network,
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|