@elaraai/e3-ui-cli 1.0.72 → 1.0.74

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 (220) hide show
  1. package/README.md +46 -21
  2. package/bin/e3-ui.mjs +6 -0
  3. package/dist/app/assets/{engine-BKZbmXkC-DmcCFcnb.js → engine-Bm-cIDd_-DwUdGcTZ.js} +1 -1
  4. package/dist/app/assets/{index-BJNWHnRv.js → index-mj-YVVN1.js} +91 -91
  5. package/dist/app/index.html +1 -1
  6. package/dist/cli.js +36 -5
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/tui.d.ts +84 -0
  9. package/dist/commands/tui.d.ts.map +1 -0
  10. package/dist/commands/tui.js +89 -0
  11. package/dist/commands/tui.js.map +1 -0
  12. package/dist/tui/api.d.ts +108 -0
  13. package/dist/tui/api.d.ts.map +1 -0
  14. package/dist/tui/api.js +130 -0
  15. package/dist/tui/api.js.map +1 -0
  16. package/dist/tui/app.d.ts +23 -0
  17. package/dist/tui/app.d.ts.map +1 -0
  18. package/dist/tui/app.js +244 -0
  19. package/dist/tui/app.js.map +1 -0
  20. package/dist/tui/controller.d.ts +116 -0
  21. package/dist/tui/controller.d.ts.map +1 -0
  22. package/dist/tui/controller.js +644 -0
  23. package/dist/tui/controller.js.map +1 -0
  24. package/dist/tui/data/dataflow.d.ts +68 -0
  25. package/dist/tui/data/dataflow.d.ts.map +1 -0
  26. package/dist/tui/data/dataflow.js +137 -0
  27. package/dist/tui/data/dataflow.js.map +1 -0
  28. package/dist/tui/data/dataset.d.ts +80 -0
  29. package/dist/tui/data/dataset.d.ts.map +1 -0
  30. package/dist/tui/data/dataset.js +297 -0
  31. package/dist/tui/data/dataset.js.map +1 -0
  32. package/dist/tui/data/edit-buffer.d.ts +74 -0
  33. package/dist/tui/data/edit-buffer.d.ts.map +1 -0
  34. package/dist/tui/data/edit-buffer.js +120 -0
  35. package/dist/tui/data/edit-buffer.js.map +1 -0
  36. package/dist/tui/data/feeds.d.ts +45 -0
  37. package/dist/tui/data/feeds.d.ts.map +1 -0
  38. package/dist/tui/data/feeds.js +318 -0
  39. package/dist/tui/data/feeds.js.map +1 -0
  40. package/dist/tui/data/logs.d.ts +68 -0
  41. package/dist/tui/data/logs.d.ts.map +1 -0
  42. package/dist/tui/data/logs.js +134 -0
  43. package/dist/tui/data/logs.js.map +1 -0
  44. package/dist/tui/input/commands.d.ts +159 -0
  45. package/dist/tui/input/commands.d.ts.map +1 -0
  46. package/dist/tui/input/commands.js +259 -0
  47. package/dist/tui/input/commands.js.map +1 -0
  48. package/dist/tui/input/completion.d.ts +63 -0
  49. package/dist/tui/input/completion.d.ts.map +1 -0
  50. package/dist/tui/input/completion.js +128 -0
  51. package/dist/tui/input/completion.js.map +1 -0
  52. package/dist/tui/input/keymap.d.ts +153 -0
  53. package/dist/tui/input/keymap.d.ts.map +1 -0
  54. package/dist/tui/input/keymap.js +206 -0
  55. package/dist/tui/input/keymap.js.map +1 -0
  56. package/dist/tui/input/mouse.d.ts +52 -0
  57. package/dist/tui/input/mouse.d.ts.map +1 -0
  58. package/dist/tui/input/mouse.js +55 -0
  59. package/dist/tui/input/mouse.js.map +1 -0
  60. package/dist/tui/lifecycle.d.ts +61 -0
  61. package/dist/tui/lifecycle.d.ts.map +1 -0
  62. package/dist/tui/lifecycle.js +99 -0
  63. package/dist/tui/lifecycle.js.map +1 -0
  64. package/dist/tui/model/catalogue.d.ts +28 -0
  65. package/dist/tui/model/catalogue.d.ts.map +1 -0
  66. package/dist/tui/model/catalogue.js +94 -0
  67. package/dist/tui/model/catalogue.js.map +1 -0
  68. package/dist/tui/model/help.d.ts +37 -0
  69. package/dist/tui/model/help.d.ts.map +1 -0
  70. package/dist/tui/model/help.js +210 -0
  71. package/dist/tui/model/help.js.map +1 -0
  72. package/dist/tui/model/index.d.ts +42 -0
  73. package/dist/tui/model/index.d.ts.map +1 -0
  74. package/dist/tui/model/index.js +50 -0
  75. package/dist/tui/model/index.js.map +1 -0
  76. package/dist/tui/model/status.d.ts +88 -0
  77. package/dist/tui/model/status.d.ts.map +1 -0
  78. package/dist/tui/model/status.js +137 -0
  79. package/dist/tui/model/status.js.map +1 -0
  80. package/dist/tui/model/tree.d.ts +126 -0
  81. package/dist/tui/model/tree.d.ts.map +1 -0
  82. package/dist/tui/model/tree.js +217 -0
  83. package/dist/tui/model/tree.js.map +1 -0
  84. package/dist/tui/model/types.d.ts +21 -0
  85. package/dist/tui/model/types.d.ts.map +1 -0
  86. package/dist/tui/model/types.js +46 -0
  87. package/dist/tui/model/types.js.map +1 -0
  88. package/dist/tui/render/glyphs.d.ts +122 -0
  89. package/dist/tui/render/glyphs.d.ts.map +1 -0
  90. package/dist/tui/render/glyphs.js +122 -0
  91. package/dist/tui/render/glyphs.js.map +1 -0
  92. package/dist/tui/render/layout.d.ts +145 -0
  93. package/dist/tui/render/layout.d.ts.map +1 -0
  94. package/dist/tui/render/layout.js +209 -0
  95. package/dist/tui/render/layout.js.map +1 -0
  96. package/dist/tui/render/text.d.ts +153 -0
  97. package/dist/tui/render/text.d.ts.map +1 -0
  98. package/dist/tui/render/text.js +320 -0
  99. package/dist/tui/render/text.js.map +1 -0
  100. package/dist/tui/render/theme.d.ts +88 -0
  101. package/dist/tui/render/theme.d.ts.map +1 -0
  102. package/dist/tui/render/theme.js +145 -0
  103. package/dist/tui/render/theme.js.map +1 -0
  104. package/dist/tui/session.d.ts +78 -0
  105. package/dist/tui/session.d.ts.map +1 -0
  106. package/dist/tui/session.js +237 -0
  107. package/dist/tui/session.js.map +1 -0
  108. package/dist/tui/state/actions.d.ts +584 -0
  109. package/dist/tui/state/actions.d.ts.map +1 -0
  110. package/dist/tui/state/actions.js +62 -0
  111. package/dist/tui/state/actions.js.map +1 -0
  112. package/dist/tui/state/persist.d.ts +114 -0
  113. package/dist/tui/state/persist.d.ts.map +1 -0
  114. package/dist/tui/state/persist.js +187 -0
  115. package/dist/tui/state/persist.js.map +1 -0
  116. package/dist/tui/state/poll.d.ts +99 -0
  117. package/dist/tui/state/poll.d.ts.map +1 -0
  118. package/dist/tui/state/poll.js +160 -0
  119. package/dist/tui/state/poll.js.map +1 -0
  120. package/dist/tui/state/reducer.d.ts +16 -0
  121. package/dist/tui/state/reducer.d.ts.map +1 -0
  122. package/dist/tui/state/reducer.js +363 -0
  123. package/dist/tui/state/reducer.js.map +1 -0
  124. package/dist/tui/state/store.d.ts +37 -0
  125. package/dist/tui/state/store.d.ts.map +1 -0
  126. package/dist/tui/state/store.js +84 -0
  127. package/dist/tui/state/store.js.map +1 -0
  128. package/dist/tui/suspend.d.ts +17 -0
  129. package/dist/tui/suspend.d.ts.map +1 -0
  130. package/dist/tui/suspend.js +33 -0
  131. package/dist/tui/suspend.js.map +1 -0
  132. package/dist/tui/ui/App.d.ts +44 -0
  133. package/dist/tui/ui/App.d.ts.map +1 -0
  134. package/dist/tui/ui/App.js +105 -0
  135. package/dist/tui/ui/App.js.map +1 -0
  136. package/dist/tui/ui/frame.d.ts +86 -0
  137. package/dist/tui/ui/frame.d.ts.map +1 -0
  138. package/dist/tui/ui/frame.js +42 -0
  139. package/dist/tui/ui/frame.js.map +1 -0
  140. package/dist/tui/ui/lines.d.ts +92 -0
  141. package/dist/tui/ui/lines.d.ts.map +1 -0
  142. package/dist/tui/ui/lines.js +94 -0
  143. package/dist/tui/ui/lines.js.map +1 -0
  144. package/dist/tui/ui/shell/chrome.d.ts +61 -0
  145. package/dist/tui/ui/shell/chrome.d.ts.map +1 -0
  146. package/dist/tui/ui/shell/chrome.js +272 -0
  147. package/dist/tui/ui/shell/chrome.js.map +1 -0
  148. package/dist/tui/ui/shell/widgets.d.ts +116 -0
  149. package/dist/tui/ui/shell/widgets.d.ts.map +1 -0
  150. package/dist/tui/ui/shell/widgets.js +183 -0
  151. package/dist/tui/ui/shell/widgets.js.map +1 -0
  152. package/dist/tui/ui/views/about.d.ts +20 -0
  153. package/dist/tui/ui/views/about.d.ts.map +1 -0
  154. package/dist/tui/ui/views/about.js +40 -0
  155. package/dist/tui/ui/views/about.js.map +1 -0
  156. package/dist/tui/ui/views/all.d.ts +17 -0
  157. package/dist/tui/ui/views/all.d.ts.map +1 -0
  158. package/dist/tui/ui/views/all.js +17 -0
  159. package/dist/tui/ui/views/all.js.map +1 -0
  160. package/dist/tui/ui/views/dashboard.d.ts +211 -0
  161. package/dist/tui/ui/views/dashboard.d.ts.map +1 -0
  162. package/dist/tui/ui/views/dashboard.js +651 -0
  163. package/dist/tui/ui/views/dashboard.js.map +1 -0
  164. package/dist/tui/ui/views/help.d.ts +17 -0
  165. package/dist/tui/ui/views/help.d.ts.map +1 -0
  166. package/dist/tui/ui/views/help.js +65 -0
  167. package/dist/tui/ui/views/help.js.map +1 -0
  168. package/dist/tui/ui/views/index.d.ts +42 -0
  169. package/dist/tui/ui/views/index.d.ts.map +1 -0
  170. package/dist/tui/ui/views/index.js +42 -0
  171. package/dist/tui/ui/views/index.js.map +1 -0
  172. package/dist/tui/ui/views/input.d.ts +6 -0
  173. package/dist/tui/ui/views/input.d.ts.map +1 -0
  174. package/dist/tui/ui/views/input.js +494 -0
  175. package/dist/tui/ui/views/input.js.map +1 -0
  176. package/dist/tui/ui/views/launch.d.ts +16 -0
  177. package/dist/tui/ui/views/launch.d.ts.map +1 -0
  178. package/dist/tui/ui/views/launch.js +43 -0
  179. package/dist/tui/ui/views/launch.js.map +1 -0
  180. package/dist/tui/ui/views/refusal.d.ts +20 -0
  181. package/dist/tui/ui/views/refusal.d.ts.map +1 -0
  182. package/dist/tui/ui/views/refusal.js +47 -0
  183. package/dist/tui/ui/views/refusal.js.map +1 -0
  184. package/dist/tui/ui/views/repos.d.ts +10 -0
  185. package/dist/tui/ui/views/repos.d.ts.map +1 -0
  186. package/dist/tui/ui/views/repos.js +93 -0
  187. package/dist/tui/ui/views/repos.js.map +1 -0
  188. package/dist/tui/ui/views/task.d.ts +17 -0
  189. package/dist/tui/ui/views/task.d.ts.map +1 -0
  190. package/dist/tui/ui/views/task.js +309 -0
  191. package/dist/tui/ui/views/task.js.map +1 -0
  192. package/dist/tui/ui/views/wordmark.d.ts +15 -0
  193. package/dist/tui/ui/views/wordmark.d.ts.map +1 -0
  194. package/dist/tui/ui/views/wordmark.js +50 -0
  195. package/dist/tui/ui/views/wordmark.js.map +1 -0
  196. package/dist/tui/ui/views/workspaces.d.ts +17 -0
  197. package/dist/tui/ui/views/workspaces.d.ts.map +1 -0
  198. package/dist/tui/ui/views/workspaces.js +127 -0
  199. package/dist/tui/ui/views/workspaces.js.map +1 -0
  200. package/dist/tui/ui/widgets/leaf-editor.d.ts +58 -0
  201. package/dist/tui/ui/widgets/leaf-editor.d.ts.map +1 -0
  202. package/dist/tui/ui/widgets/leaf-editor.js +119 -0
  203. package/dist/tui/ui/widgets/leaf-editor.js.map +1 -0
  204. package/dist/tui/ui/widgets/logs.d.ts +99 -0
  205. package/dist/tui/ui/widgets/logs.d.ts.map +1 -0
  206. package/dist/tui/ui/widgets/logs.js +291 -0
  207. package/dist/tui/ui/widgets/logs.js.map +1 -0
  208. package/dist/tui/ui/widgets/reads.d.ts +62 -0
  209. package/dist/tui/ui/widgets/reads.d.ts.map +1 -0
  210. package/dist/tui/ui/widgets/reads.js +122 -0
  211. package/dist/tui/ui/widgets/reads.js.map +1 -0
  212. package/dist/tui/ui/widgets/runs.d.ts +51 -0
  213. package/dist/tui/ui/widgets/runs.d.ts.map +1 -0
  214. package/dist/tui/ui/widgets/runs.js +86 -0
  215. package/dist/tui/ui/widgets/runs.js.map +1 -0
  216. package/dist/tui/ui/widgets/tree.d.ts +149 -0
  217. package/dist/tui/ui/widgets/tree.d.ts.map +1 -0
  218. package/dist/tui/ui/widgets/tree.js +584 -0
  219. package/dist/tui/ui/widgets/tree.js.map +1 -0
  220. package/package.json +22 -12
@@ -0,0 +1,651 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
+ */
5
+ import { breakpoint, columnPlan, scrollIntoView } from '../../render/layout.js';
6
+ import { agoShort, formatDuration, formatSize, hashShort, padEnd, padStart, timeAgo } from '../../render/text.js';
7
+ import { layoutOf, registerListModel } from '../../model/index.js';
8
+ import { datasetEntries } from '../../model/catalogue.js';
9
+ import { datasetStatusCell, eventCell, executionDuration, executionStatusCell, statusText, taskStatusCell } from '../../model/status.js';
10
+ import { registerViewHooks } from '../../controller.js';
11
+ import { isRunLive, lockHolderText } from '../../data/dataflow.js';
12
+ import { b, blank, d, lineWidth, lrLine, t } from '../lines.js';
13
+ import { centredBlock, sectionLine, tableLine, tablePlan, withScrollbar } from '../shell/widgets.js';
14
+ import { registerView } from './index.js';
15
+ /** Event rows the execution panel shows at most. */
16
+ export const MAX_EVENT_ROWS = 6;
17
+ /** Cells of the accounted bar at most (longer workspaces are sampled). */
18
+ const MAX_BAR_CELLS = 40;
19
+ /**
20
+ * The column builder's context from a render context.
21
+ *
22
+ * @param state - The store state
23
+ * @param ctx - The render context
24
+ * @returns The dashboard context
25
+ */
26
+ export function dashboardCtx(state, ctx) {
27
+ return { g: ctx.g, now: ctx.now, columns: ctx.layout.columns, bp: breakpoint(state.size), spinner: ctx.spinner };
28
+ }
29
+ /** `12s` / `1m` / `3h` — an event's age without the `ago`. */
30
+ function ageBare(then, now) {
31
+ const ago = timeAgo(then, now);
32
+ return ago === 'just now' ? '0s' : ago.replace(/ ago$/, '');
33
+ }
34
+ /** The reason with its first letter lowered (`Waiting for task 'x'` → `waiting for task 'x'`). */
35
+ function lowerFirst(text) {
36
+ return text.length === 0 ? text : text[0].toLowerCase() + text.slice(1);
37
+ }
38
+ /** The dashboard title line: ` main ● DEPLOYED · demand@1.4.2 · deployed 3d ago · lock: none`. */
39
+ export function dashboardTitle(state, ws, ctx) {
40
+ const g = ctx.g;
41
+ const info = (state.data.workspaces ?? []).find(w => w.name === ws);
42
+ const wsState = state.data.workspaceState[ws];
43
+ const status = state.data.status[ws]?.result;
44
+ const right = [];
45
+ if (info === undefined || !info.deployed) {
46
+ right.push(b(`${g.empty} EMPTY`, 'muted'));
47
+ }
48
+ else {
49
+ const pkg = info.packageName.type === 'some' ? `${info.packageName.value}${info.packageVersion.type === 'some' ? `@${info.packageVersion.value}` : ''}` : '';
50
+ right.push(b(`${g.dot} DEPLOYED`, 'pos'), d(` ${g.sep} ${pkg}`));
51
+ if (wsState !== undefined && wsState !== null)
52
+ right.push(d(` ${g.sep} deployed ${timeAgo(wsState.deployedAt, ctx.now)}`));
53
+ }
54
+ const lock = status?.lock;
55
+ if (lock !== undefined) {
56
+ if (lock.type === 'some') {
57
+ right.push(t(` ${g.sep} `), b(`lock: ${lockHolderText(lock.value, ctx.now, g)}`, 'warn'));
58
+ }
59
+ else {
60
+ right.push(d(` ${g.sep} lock: none`));
61
+ }
62
+ }
63
+ right.push(t(' '));
64
+ return lrLine([t(' '), b(ws)], right, ctx.layout.columns);
65
+ }
66
+ /** The task stats: the four the design shows, plus in-progress / stale-running when non-zero. */
67
+ function taskStats(s, g) {
68
+ const stats = [
69
+ { glyph: g.dot, tone: 'pos', word: 'up-to-date', count: Number(s.upToDate) },
70
+ { glyph: g.half, tone: 'warn', word: 'waiting', count: Number(s.waiting) },
71
+ { glyph: g.cross, tone: 'neg', word: 'failed', count: Number(s.failed) + Number(s.error) },
72
+ { glyph: g.empty, tone: 'muted', word: 'ready', count: Number(s.ready) },
73
+ ];
74
+ if (Number(s.inProgress) > 0)
75
+ stats.push({ glyph: g.quarter, tone: 'info', word: 'in-progress', count: Number(s.inProgress) });
76
+ if (Number(s.staleRunning) > 0)
77
+ stats.push({ glyph: g.half, tone: 'warn', word: 'stale-running', count: Number(s.staleRunning) });
78
+ return stats;
79
+ }
80
+ /** The dataset stats. */
81
+ function datasetStats(s, g) {
82
+ return [
83
+ { glyph: g.dot, tone: 'pos', word: 'up-to-date', count: Number(s.upToDate) },
84
+ { glyph: g.half, tone: 'warn', word: 'stale', count: Number(s.stale) },
85
+ { glyph: g.empty, tone: 'muted', word: 'unset', count: Number(s.unset) },
86
+ ];
87
+ }
88
+ /** A two-wide grid of stat cells (`● up-to-date 4 ◐ waiting 1`). */
89
+ function statGrid(stats) {
90
+ const lines = [];
91
+ for (let i = 0; i < stats.length; i += 2) {
92
+ const first = stats[i];
93
+ const second = stats[i + 1];
94
+ const line = [b(first.glyph, first.tone), t(padEnd(` ${first.word}`, 17)), t(padStart(String(first.count), 4))];
95
+ if (second !== undefined)
96
+ line.push(t(' '), b(second.glyph, second.tone), t(padEnd(` ${second.word}`, 13)), t(padStart(String(second.count), 4)));
97
+ lines.push(line);
98
+ }
99
+ return lines;
100
+ }
101
+ /**
102
+ * The accounted bar: one cell per task (sampled past 40), lowest first —
103
+ * `✗` failed / error, `▁` ready, `▃` waiting, `▅` in progress, `▇`
104
+ * up-to-date — and `N of M accounted`, where a task is accounted for once
105
+ * the dataflow has touched it (anything but `ready`).
106
+ *
107
+ * @param tasks - The workspace's tasks
108
+ * @param g - The glyph set
109
+ * @returns The bar's spans and the counts
110
+ */
111
+ export function accountedBar(tasks, g) {
112
+ const height = (status) => status === 'ready' ? 0 : status === 'waiting' ? 1 : status === 'in-progress' || status === 'stale-running' ? 2 : status === 'up-to-date' ? 3 : -1;
113
+ const heights = tasks.map(task => height(task.status.type)).sort((x, y) => x - y);
114
+ const n = Math.min(heights.length, MAX_BAR_CELLS);
115
+ const bar = [];
116
+ for (let i = 0; i < n; i++) {
117
+ const h = heights[Math.floor((i * heights.length) / n)];
118
+ bar.push(h < 0 ? b(g.cross, 'neg') : b(g.bars[h], 'brand'));
119
+ }
120
+ return { bar, accounted: tasks.filter(task => task.status.type !== 'ready').length, total: tasks.length };
121
+ }
122
+ /** The counts block: the two headed groups side by side (or stacked when narrow) and the accounted bar. */
123
+ function countsLines(status, dctx) {
124
+ const g = dctx.g;
125
+ const tasks = statGrid(taskStats(status.summary.tasks, g));
126
+ const datasets = statGrid(datasetStats(status.summary.datasets, g));
127
+ const taskHead = [b(`TASKS ${status.summary.tasks.total}`)];
128
+ const datasetHead = [b(`DATASETS ${status.summary.datasets.total}`)];
129
+ const colL = Math.floor((dctx.columns - 2) / 2);
130
+ const out = [];
131
+ if (colL >= 44) {
132
+ const left = [taskHead, ...tasks];
133
+ const right = [datasetHead, ...datasets];
134
+ const n = Math.max(left.length, right.length);
135
+ for (let i = 0; i < n; i++) {
136
+ const l = left[i] ?? [];
137
+ const r = right[i] ?? [];
138
+ out.push([t(' '), ...l, t(' '.repeat(Math.max(0, colL - lineWidth(l)))), ...r]);
139
+ }
140
+ }
141
+ else {
142
+ for (const line of [taskHead, ...tasks, datasetHead, ...datasets])
143
+ out.push([t(' '), ...line]);
144
+ }
145
+ const { bar, accounted, total } = accountedBar(status.tasks, g);
146
+ out.push([t(' '), ...bar, d(`${bar.length > 0 ? ' ' : ''}${accounted} of ${total} accounted`)]);
147
+ return out;
148
+ }
149
+ // ---------------------------------------------------------------------------
150
+ // The model
151
+ // ---------------------------------------------------------------------------
152
+ /**
153
+ * The latest event of each task, in order of first appearance.
154
+ *
155
+ * @param events - The execution's events
156
+ * @returns One event per task
157
+ */
158
+ export function latestPerTask(events) {
159
+ const order = [];
160
+ const latest = new Map();
161
+ for (const event of events) {
162
+ const task = event.value.task;
163
+ if (!latest.has(task))
164
+ order.push(task);
165
+ latest.set(task, event);
166
+ }
167
+ return order.map(task => latest.get(task));
168
+ }
169
+ /** Whether an execution is live (running, launched and not yet seen running, or being stopped). */
170
+ function isLive(execution) {
171
+ return execution !== undefined && (execution.state?.status.type === 'running' || execution.settling || execution.stopping);
172
+ }
173
+ /** Whether an event's row opens the task's logs. */
174
+ function opensLogs(event) {
175
+ return event.type === 'failed' || event.type === 'error';
176
+ }
177
+ let cached = null;
178
+ function sameKey(a, b) {
179
+ return a.ws === b.ws && a.status === b.status && a.statusError === b.statusError && a.workspaceState === b.workspaceState
180
+ && a.execution === b.execution && a.datasets === b.datasets && a.taskList === b.taskList
181
+ && a.columns === b.columns && a.bp === b.bp && a.g === b.g;
182
+ }
183
+ /**
184
+ * The dashboard model for a workspace — built once per data change and
185
+ * returned as the same object until the status, the execution, the
186
+ * dataset list, the task list, the width or the breakpoint changes. The
187
+ * clock, the spinner and the selection are not inputs: they restyle lines
188
+ * at render ({@link dashboardLines}).
189
+ *
190
+ * @param state - The store state
191
+ * @param ws - The workspace
192
+ * @param dctx - The dashboard context (its width and breakpoint key the cache)
193
+ * @returns The model
194
+ */
195
+ export function dashboardModel(state, ws, dctx) {
196
+ const key = {
197
+ ws,
198
+ status: state.data.status[ws]?.result,
199
+ statusError: state.data.statusError[ws],
200
+ workspaceState: state.data.workspaceState[ws],
201
+ execution: state.data.execution[ws],
202
+ datasets: state.data.datasets[ws],
203
+ taskList: state.data.taskList[ws],
204
+ columns: dctx.columns,
205
+ bp: dctx.bp,
206
+ g: dctx.g,
207
+ };
208
+ if (cached !== null && sameKey(cached.key, key))
209
+ return cached.model;
210
+ const model = buildModel(state, ws, dctx, key);
211
+ cached = { key, model };
212
+ return model;
213
+ }
214
+ /** Builds the model (the cache's miss path). */
215
+ function buildModel(state, ws, dctx, key) {
216
+ const g = dctx.g;
217
+ const width = dctx.columns - 1;
218
+ const status = key.status;
219
+ const execution = key.execution;
220
+ const empty = {
221
+ status, placeholder: [], counts: [], execution, live: false, done: 0, events: [], latest: new Map(),
222
+ taskPlan: [], tasks: [], inputPlan: [], inputs: [], inputNames: [], rows: [], total: 0, panelAt: 0, tasksAt: 0, inputsAt: 0,
223
+ };
224
+ if (status === undefined) {
225
+ const placeholder = [];
226
+ if (key.workspaceState === null) {
227
+ placeholder.push(...centredBlock(g.empty, 'muted', 'NOTHING DEPLOYED', [`${ws} has no package yet`, `e3 workspace deploy <repo> ${ws} <package>[@version] deploy one`, '/workspaces pick another workspace'], width));
228
+ }
229
+ else if (key.statusError !== undefined) {
230
+ placeholder.push([t(' '), b(`${g.cross} ${key.statusError}`, 'neg')]);
231
+ }
232
+ else {
233
+ placeholder.push([t(' '), d('loading…')]);
234
+ }
235
+ return { ...empty, placeholder, total: placeholder.length };
236
+ }
237
+ // The dataset map and the latest event per task: once per build, shared by every row.
238
+ const entries = datasetEntries(state, ws);
239
+ const latest = new Map(latestPerTask(execution?.events ?? []).map(e => [e.value.task, e]));
240
+ const live = isLive(execution);
241
+ const done = execution === undefined ? 0 : execution.events.filter(e => e.type !== 'start').length;
242
+ const events = execution === undefined || execution.state === null ? []
243
+ : live ? [...latest.values()].slice(-MAX_EVENT_ROWS)
244
+ : [...latest.values()].filter(opensLogs);
245
+ const tasks = status.tasks.map(task => {
246
+ const cell = taskStatusCell(task.status, g);
247
+ // The reason / pid / cached detail lives in the last column; only a failure's exit code / message stays inline.
248
+ const bare = task.status.type !== 'failed' && task.status.type !== 'error';
249
+ const inputs = task.inputs.filter(p => p.startsWith('.inputs.')).map(p => p.slice('.inputs.'.length));
250
+ const entry = entries.get(task.output);
251
+ return {
252
+ task,
253
+ event: latest.get(task.name),
254
+ size: entry?.size != null ? formatSize(entry.size) : '—',
255
+ cells: {
256
+ name: task.name,
257
+ status: { text: bare ? `${cell.glyph} ${cell.word}` : statusText(cell), tone: cell.tone },
258
+ dependsOn: task.dependsOn.length > 0 ? task.dependsOn.join(', ') : '—',
259
+ inputs: inputs.length > 0 ? inputs.join(', ') : '—',
260
+ output: entry?.type ?? '—',
261
+ },
262
+ };
263
+ });
264
+ const inputDatasets = status.datasets.filter(ds => !ds.isTaskOutput && ds.path.startsWith('.inputs.'));
265
+ const inputNames = inputDatasets.map(ds => ds.path.slice('.inputs.'.length));
266
+ const inputs = inputDatasets.map((ds, i) => {
267
+ const cell = datasetStatusCell(ds.status.type, g);
268
+ const entry = entries.get(ds.path);
269
+ return {
270
+ cells: {
271
+ name: inputNames[i],
272
+ status: { text: statusText(cell), tone: cell.tone },
273
+ type: entry?.type ?? '—',
274
+ size: entry?.size != null ? formatSize(entry.size) : '—',
275
+ hash: ds.hash.type === 'some' ? hashShort(ds.hash.value) : '—',
276
+ },
277
+ };
278
+ });
279
+ const taskPlan = tablePlan(columnPlan('tasks', dctx.bp), tasks, width);
280
+ const inputPlan = tablePlan(columnPlan('inputs', dctx.bp), inputs, width);
281
+ const counts = countsLines(status, dctx);
282
+ // The geometry: every selectable row's line, numbered in column order — failures, tasks, inputs.
283
+ const rows = [];
284
+ let line = counts.length + 1;
285
+ const panelAt = line;
286
+ const shown = events.slice(0, MAX_EVENT_ROWS);
287
+ shown.forEach((event, i) => {
288
+ if (opensLogs(event))
289
+ rows.push({ kind: 'logs', name: event.value.task, line: panelAt + 1 + i });
290
+ });
291
+ line += 1 + shown.length + (events.length > shown.length ? 1 : 0) + 1;
292
+ const tasksAt = line;
293
+ line += 2;
294
+ status.tasks.forEach((task, i) => rows.push({ kind: 'task', name: task.name, line: line + i }));
295
+ line += Math.max(1, tasks.length) + 1;
296
+ const inputsAt = line;
297
+ line += 2;
298
+ inputNames.forEach((name, i) => rows.push({ kind: 'input', name, line: line + i }));
299
+ line += Math.max(1, inputs.length);
300
+ return { ...empty, counts, live, done, events, latest, taskPlan, tasks, inputPlan, inputs, inputNames, rows, total: line, panelAt, tasksAt, inputsAt };
301
+ }
302
+ // ---------------------------------------------------------------------------
303
+ // Rendering
304
+ // ---------------------------------------------------------------------------
305
+ /** The execution panel: a header line, then the event rows (the header and the ages carry the clock and the spinner). */
306
+ function executionLines(model, sel, dctx) {
307
+ const g = dctx.g;
308
+ const sep = g.sep;
309
+ const width = dctx.columns - 1;
310
+ const spin = g.spinner[dctx.spinner % g.spinner.length];
311
+ const execution = model.execution;
312
+ const tasksTotal = model.status?.tasks.length ?? 0;
313
+ const lines = [];
314
+ let title = 'LAST EXECUTION';
315
+ let right;
316
+ if (execution === undefined) {
317
+ right = [d('…')];
318
+ }
319
+ else if (execution.state === null) {
320
+ right = execution.settling ? [b(`${g.quarter} STARTING`, 'info'), d(` ${sep} ${spin}`)] : [d(`${g.empty} never run ${sep} r run`)];
321
+ if (execution.settling)
322
+ title = 'EXECUTION';
323
+ }
324
+ else if (model.live) {
325
+ title = 'EXECUTION';
326
+ const head = execution.stopping ? b(`${g.square} STOPPING`, 'warn') : b(`${g.quarter} RUNNING`, 'info');
327
+ right = [head, d(` ${sep} started ${timeAgo(execution.state.startedAt, dctx.now)} ${sep} ${model.done} of ${tasksTotal} tasks ${sep} ${spin}`)];
328
+ }
329
+ else {
330
+ const state = execution.state;
331
+ const cell = executionStatusCell(state.status.type, g);
332
+ let detail = ` ${sep} started ${timeAgo(state.startedAt, dctx.now)}`;
333
+ if (state.summary.type === 'some') {
334
+ const s = state.summary.value;
335
+ detail += ` ${sep} ${formatDuration(executionDuration(state) ?? s.duration)} ${sep} executed ${s.executed} ${sep} cached ${s.cached} ${sep} failed ${s.failed} ${sep} skipped ${s.skipped}`;
336
+ }
337
+ right = [b(`${cell.glyph} ${cell.word}`, cell.tone), d(detail)];
338
+ }
339
+ lines.push(lrLine([t(' '), b(title)], [...right, t(' ')], width));
340
+ const shown = model.events.slice(0, MAX_EVENT_ROWS);
341
+ for (const event of shown) {
342
+ const cell = eventCell(event, g);
343
+ const logs = opensLogs(event);
344
+ const selected = logs && sel?.kind === 'logs' && sel.name === cell.task;
345
+ const left = [
346
+ t(' '),
347
+ selected ? b(g.sel, 'brand') : t(' '),
348
+ t(padStart(ageBare(cell.timestamp, dctx.now), 4)),
349
+ t(' '),
350
+ b(cell.glyph, cell.tone),
351
+ t(padEnd(` ${cell.word}`, 13)),
352
+ selected ? b(cell.task) : t(cell.task),
353
+ ];
354
+ const detail = [];
355
+ if (event.type === 'start')
356
+ detail.push(d(`${spin} ${ageBare(cell.timestamp, dctx.now)}`));
357
+ else if (cell.detail !== '')
358
+ detail.push(d(cell.detail));
359
+ if (logs)
360
+ detail.push(t(' '), b(`${g.enter} logs`, 'brand'));
361
+ lines.push(lrLine(left, [...detail, t(' ')], width));
362
+ }
363
+ if (model.events.length > shown.length) {
364
+ lines.push([t(' '), d(`… ${model.events.length - shown.length} more failed ${sep} /logs <task>`)]);
365
+ }
366
+ return lines;
367
+ }
368
+ /** The `SIZE · LAST RUN` cell of a task row. */
369
+ function lastRunText(task, event, size, dctx) {
370
+ const g = dctx.g;
371
+ const spin = g.spinner[dctx.spinner % g.spinner.length];
372
+ switch (task.status.type) {
373
+ case 'in-progress': {
374
+ const since = task.status.value.startedAt.type === 'some' ? task.status.value.startedAt.value : event?.type === 'start' ? event.value.timestamp : null;
375
+ return `${spin} ${since !== null ? ageBare(since, dctx.now) : 'running'}`;
376
+ }
377
+ case 'waiting':
378
+ return `— ${g.sep} ${lowerFirst(task.status.value.reason)}`;
379
+ default: {
380
+ const run = event === undefined ? (task.status.type === 'ready' ? 'never' : '—')
381
+ : event.type === 'complete' || event.type === 'failed' ? formatDuration(event.value.duration)
382
+ : event.type === 'cached' ? 'cached'
383
+ : event.type === 'error' ? 'error'
384
+ : event.type === 'input_unavailable' ? 'skipped'
385
+ : '—';
386
+ return `${size} ${g.sep} ${run}`;
387
+ }
388
+ }
389
+ }
390
+ /**
391
+ * The column's lines in `[top, top + visible)` — the window the screen
392
+ * shows, rendered from the model with the selected row restyled and the
393
+ * clock and the spinner stamped in.
394
+ *
395
+ * @param model - The dashboard model
396
+ * @param dctx - The dashboard context
397
+ * @param sel - The selected row index
398
+ * @param top - The first line
399
+ * @param visible - Lines in the window
400
+ * @returns The lines (fewer when the column ends first)
401
+ */
402
+ export function dashboardLines(model, dctx, sel, top, visible) {
403
+ const g = dctx.g;
404
+ const width = dctx.columns - 1;
405
+ const first = Math.max(0, top);
406
+ const end = Math.min(model.total, first + visible);
407
+ if (model.status === undefined)
408
+ return model.placeholder.slice(first, end);
409
+ const selected = model.rows[Math.max(0, Math.min(sel, model.rows.length - 1))];
410
+ const panel = executionLines(model, selected, dctx);
411
+ const taskSel = selected?.kind === 'task' ? model.tasks.findIndex(row => row.task.name === selected.name) : -1;
412
+ const inputSel = selected?.kind === 'input' ? model.inputNames.indexOf(selected.name) : -1;
413
+ const tasksFirst = model.tasksAt + 2;
414
+ const inputsFirst = model.inputsAt + 2;
415
+ const out = [];
416
+ for (let i = first; i < end; i++) {
417
+ if (i < model.counts.length)
418
+ out.push(model.counts[i]);
419
+ else if (i < model.panelAt)
420
+ out.push(blank(width));
421
+ else if (i < model.panelAt + panel.length)
422
+ out.push(panel[i - model.panelAt]);
423
+ else if (i < model.tasksAt)
424
+ out.push(blank(width));
425
+ else if (i === model.tasksAt)
426
+ out.push(sectionLine('TASKS', '', width));
427
+ else if (i === model.tasksAt + 1)
428
+ out.push(tableLine(model.taskPlan, null, false, width, g));
429
+ else if (i < model.inputsAt - 1) {
430
+ const row = model.tasks[i - tasksFirst];
431
+ if (row === undefined)
432
+ out.push([t(' '), d('no tasks')]);
433
+ else
434
+ out.push(tableLine(model.taskPlan, { cells: { ...row.cells, size: lastRunText(row.task, row.event, row.size, dctx) } }, i - tasksFirst === taskSel, width, g));
435
+ }
436
+ else if (i < model.inputsAt)
437
+ out.push(blank(width));
438
+ else if (i === model.inputsAt)
439
+ out.push(sectionLine('INPUTS', '', width));
440
+ else if (i === model.inputsAt + 1)
441
+ out.push(tableLine(model.inputPlan, null, false, width, g));
442
+ else {
443
+ const row = model.inputs[i - inputsFirst];
444
+ if (row === undefined)
445
+ out.push([t(' '), d('no inputs')]);
446
+ else
447
+ out.push(tableLine(model.inputPlan, row, i - inputsFirst === inputSel, width, g));
448
+ }
449
+ }
450
+ return out;
451
+ }
452
+ /**
453
+ * The dashboard body: the fixed title, then the column window with its
454
+ * scrollbar — plus the window's clickable rows and pane for the mouse.
455
+ *
456
+ * @param state - The store state
457
+ * @param ctx - The render context
458
+ * @returns The body lines, hits and pane
459
+ */
460
+ export function renderDashboard(state, ctx) {
461
+ if (state.view.kind !== 'dashboard')
462
+ return { body: [], hits: [], pane: null };
463
+ const ws = state.view.ws;
464
+ const width = ctx.layout.columns;
465
+ const out = [dashboardTitle(state, ws, ctx)];
466
+ const dctx = dashboardCtx(state, ctx);
467
+ const model = dashboardModel(state, ws, dctx);
468
+ const visible = Math.max(1, ctx.layout.bodyRows - 1);
469
+ const total = model.total;
470
+ const top = Math.max(0, Math.min(state.view.list.top, Math.max(0, total - visible)));
471
+ const window = dashboardLines(model, dctx, state.view.list.sel, top, visible);
472
+ while (window.length < visible)
473
+ window.push(blank(width - 1));
474
+ out.push(...withScrollbar(window, width, total, visible, top, ctx.g));
475
+ const hits = model.rows
476
+ .map((row, index) => ({ row, index }))
477
+ .filter(({ row }) => row.line >= top && row.line < top + visible)
478
+ .map(({ row, index }) => ({ row: 1 + (row.line - top), x0: 0, x1: width, target: { kind: 'dashboard', index } }));
479
+ return { body: out, hits, pane: { top: 1, rows: visible, total, visible, scrollTop: top } };
480
+ }
481
+ /**
482
+ * Scrolls the column by lines (the wheel), or to a line (a thumb drag),
483
+ * keeping the selection inside the window.
484
+ *
485
+ * @param state - The store state
486
+ * @param controller - The controller
487
+ * @param to - `{ delta }` lines, or `{ top }` absolute
488
+ */
489
+ export function scrollDashboard(state, controller, to) {
490
+ const nav = navigation(state, controller);
491
+ if (nav === null || state.view.kind !== 'dashboard')
492
+ return;
493
+ const { model, visible } = nav;
494
+ const total = model.total;
495
+ const current = state.view.list.top;
496
+ const top = Math.max(0, Math.min('delta' in to ? current + to.delta : to.top, Math.max(0, total - visible)));
497
+ let sel = state.view.list.sel;
498
+ if (model.rows.length > 0) {
499
+ const line = model.rows[Math.max(0, Math.min(sel, model.rows.length - 1))].line;
500
+ if (line < top || line >= top + visible) {
501
+ // The selection follows the window: the first (or last) row inside it.
502
+ const inside = model.rows.map((r, i) => ({ r, i })).filter(({ r }) => r.line >= top && r.line < top + visible);
503
+ const pick = line < top ? inside[0] : inside[inside.length - 1];
504
+ if (pick !== undefined)
505
+ sel = pick.i;
506
+ }
507
+ }
508
+ controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel, top } } });
509
+ }
510
+ registerView('dashboard', (state, ctx) => {
511
+ const { body, hits, pane } = renderDashboard(state, ctx);
512
+ return { body, hits, pane: pane ?? undefined, hints: dashboardHints(state, ctx) };
513
+ });
514
+ /** The dashboard hints. */
515
+ export function dashboardHints(state, ctx) {
516
+ const polled = state.data.polledAt;
517
+ const ws = state.view.kind === 'dashboard' ? state.view.ws : null;
518
+ const running = ws !== null && isRunLive(state, ws);
519
+ return {
520
+ left: running ? '↑↓ move ⏎ open x stop / commands' : '↑↓ move ⏎ open r run x stop w workspaces / commands',
521
+ right: polled !== null ? `polled ${agoShort(polled, ctx.now)}` : '',
522
+ };
523
+ }
524
+ // ---------------------------------------------------------------------------
525
+ // Navigation
526
+ // ---------------------------------------------------------------------------
527
+ /** The model and the window geometry for navigation (no theme needed; the model comes from the cache). */
528
+ function navigation(state, controller) {
529
+ if (state.view.kind !== 'dashboard')
530
+ return null;
531
+ const layout = layoutOf(state);
532
+ const dctx = { g: controller.deps.glyphs, now: controller.deps.now(), columns: layout.columns, bp: breakpoint(state.size), spinner: 0 };
533
+ return { model: dashboardModel(state, state.view.ws, dctx), visible: Math.max(1, layout.bodyRows - 1) };
534
+ }
535
+ /**
536
+ * Selects a row of the column (by index) and scrolls it into view.
537
+ *
538
+ * @param state - The store state
539
+ * @param controller - The controller
540
+ * @param index - The row index
541
+ */
542
+ export function selectDashboardRow(state, controller, index) {
543
+ const nav = navigation(state, controller);
544
+ if (nav === null || state.view.kind !== 'dashboard')
545
+ return;
546
+ const { model, visible } = nav;
547
+ if (model.rows.length === 0)
548
+ return;
549
+ const sel = Math.max(0, Math.min(index, model.rows.length - 1));
550
+ const top = scrollIntoView(state.view.list.top, model.rows[sel].line, visible, model.total);
551
+ controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel, top } } });
552
+ }
553
+ /**
554
+ * Moves the selection (or scrolls the column when it has no selectable
555
+ * rows); page moves step by a window of lines.
556
+ *
557
+ * @param state - The store state
558
+ * @param controller - The controller
559
+ * @param op - The navigation
560
+ */
561
+ export function moveDashboard(state, controller, op) {
562
+ const nav = navigation(state, controller);
563
+ if (nav === null || state.view.kind !== 'dashboard')
564
+ return;
565
+ const { model, visible } = nav;
566
+ const total = model.total;
567
+ const count = model.rows.length;
568
+ const page = Math.max(1, visible - 1);
569
+ if (count === 0) {
570
+ const delta = op === 'up' ? -1 : op === 'down' ? 1 : op === 'pageUp' ? -page : op === 'pageDown' ? page : op === 'home' ? -total : total;
571
+ const top = Math.max(0, Math.min(state.view.list.top + delta, Math.max(0, total - visible)));
572
+ controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel: 0, top } } });
573
+ return;
574
+ }
575
+ const last = count - 1;
576
+ const current = Math.max(0, Math.min(state.view.list.sel, last));
577
+ const line = model.rows[current].line;
578
+ let sel = current;
579
+ switch (op) {
580
+ case 'up':
581
+ sel = Math.max(0, current - 1);
582
+ break;
583
+ case 'down':
584
+ sel = Math.min(last, current + 1);
585
+ break;
586
+ case 'home':
587
+ sel = 0;
588
+ break;
589
+ case 'end':
590
+ sel = last;
591
+ break;
592
+ case 'pageUp': {
593
+ const target = line - page;
594
+ let i = current;
595
+ while (i > 0 && model.rows[i - 1].line >= target)
596
+ i--;
597
+ sel = i === current ? Math.max(0, current - 1) : i;
598
+ break;
599
+ }
600
+ case 'pageDown': {
601
+ const target = line + page;
602
+ let i = current;
603
+ while (i < last && model.rows[i + 1].line <= target)
604
+ i++;
605
+ sel = i === current ? Math.min(last, current + 1) : i;
606
+ break;
607
+ }
608
+ }
609
+ // Home shows the column from its start and End from its end; the other moves scroll minimally.
610
+ const top = op === 'home' ? 0
611
+ : op === 'end' ? Math.max(0, total - visible)
612
+ : scrollIntoView(state.view.list.top, model.rows[sel].line, visible, total);
613
+ controller.dispatch({ type: 'view/set', view: { ...state.view, list: { sel, top } } });
614
+ }
615
+ // The generic list model is not used: the column's rows sit at irregular
616
+ // line offsets, so the view moves and scrolls itself (`moveDashboard`).
617
+ registerListModel('dashboard', () => ({ count: 0, visible: 0 }));
618
+ registerViewHooks('dashboard', {
619
+ click: (target, _event, state, controller) => {
620
+ if (target.kind !== 'dashboard')
621
+ return false;
622
+ selectDashboardRow(state, controller, target.index);
623
+ return true;
624
+ },
625
+ scroll: (to, state, controller) => {
626
+ scrollDashboard(state, controller, to);
627
+ return true;
628
+ },
629
+ open: (state, controller) => {
630
+ const nav = navigation(state, controller);
631
+ if (nav === null || state.view.kind !== 'dashboard')
632
+ return;
633
+ const row = nav.model.rows[Math.max(0, Math.min(state.view.list.sel, nav.model.rows.length - 1))];
634
+ if (row === undefined)
635
+ return;
636
+ const ws = state.view.ws;
637
+ if (row.kind === 'logs')
638
+ controller.openTask(ws, row.name, 'stdout');
639
+ else if (row.kind === 'task')
640
+ controller.openTask(ws, row.name);
641
+ else
642
+ controller.openInput(ws, row.name);
643
+ },
644
+ key: (action, state, controller) => {
645
+ if (action.kind !== 'move' || state.view.kind !== 'dashboard')
646
+ return false;
647
+ moveDashboard(state, controller, action.op);
648
+ return true;
649
+ },
650
+ });
651
+ //# sourceMappingURL=dashboard.js.map