@gaia-ai/addon-gaia-ui 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/dist/src/Agent/launch.d.ts +4 -4
  2. package/dist/src/Agent/launch.js +14 -7
  3. package/dist/src/Component/section-list/index.d.ts +117 -8
  4. package/dist/src/Component/section-list/index.js +118 -46
  5. package/dist/src/Component/stats-panel/index.d.ts +17 -5
  6. package/dist/src/Component/stats-panel/index.js +50 -52
  7. package/dist/src/Component/top-tabs/index.d.ts +95 -23
  8. package/dist/src/Component/top-tabs/index.js +87 -41
  9. package/dist/src/Entity/collection.d.ts +279 -0
  10. package/dist/src/Entity/collection.js +377 -0
  11. package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +25 -0
  12. package/dist/src/Entity/conductor/Component/conductor-list/index.js +60 -0
  13. package/dist/src/Entity/conductor/Data/conductors.d.ts +216 -0
  14. package/dist/src/Entity/conductor/Data/conductors.js +280 -0
  15. package/dist/src/Entity/conductor/Data/liveness.d.ts +2 -1
  16. package/dist/src/Entity/conductor/Data/liveness.js +12 -16
  17. package/dist/src/Entity/conductor/Data/log-tail.d.ts +25 -0
  18. package/dist/src/Entity/conductor/Data/log-tail.js +133 -0
  19. package/dist/src/Entity/conductor/Data/own-conductor.d.ts +30 -0
  20. package/dist/src/Entity/conductor/Data/own-conductor.js +73 -0
  21. package/dist/src/Entity/conductor/Data/process.d.ts +78 -0
  22. package/dist/src/Entity/conductor/Data/process.js +101 -0
  23. package/dist/src/Entity/conductor/Screen/conductors.d.ts +67 -0
  24. package/dist/src/Entity/conductor/Screen/conductors.js +233 -0
  25. package/dist/src/Entity/cycle.d.ts +18 -0
  26. package/dist/src/Entity/cycle.js +30 -0
  27. package/dist/src/Entity/project/Data/project.d.ts +8 -0
  28. package/dist/src/Entity/project/Data/project.js +12 -0
  29. package/dist/src/Entity/project/Data/projects.d.ts +121 -0
  30. package/dist/src/Entity/project/Data/projects.js +115 -0
  31. package/dist/src/Entity/project/Data/with-tickets.d.ts +5 -0
  32. package/dist/src/Entity/project/Data/with-tickets.js +15 -13
  33. package/dist/src/Entity/project/Screen/project-picker.d.ts +80 -0
  34. package/dist/src/Entity/project/Screen/project-picker.js +191 -0
  35. package/dist/src/Entity/project/Screen/projects-screen.d.ts +52 -0
  36. package/dist/src/Entity/project/Screen/projects-screen.js +122 -0
  37. package/dist/src/Entity/run/Component/run-detail/index.d.ts +26 -10
  38. package/dist/src/Entity/run/Component/run-detail/index.js +147 -39
  39. package/dist/src/Entity/run/Component/run-table/index.d.ts +5 -7
  40. package/dist/src/Entity/run/Component/run-table/index.js +9 -18
  41. package/dist/src/Entity/run/Data/query.d.ts +22 -0
  42. package/dist/src/Entity/run/Data/query.js +41 -0
  43. package/dist/src/Entity/run/Data/runs.d.ts +192 -4
  44. package/dist/src/Entity/run/Data/runs.js +188 -27
  45. package/dist/src/Entity/run/Screen/run-detail.d.ts +53 -0
  46. package/dist/src/Entity/run/Screen/run-detail.js +70 -0
  47. package/dist/src/Entity/run/Screen/runs.d.ts +86 -0
  48. package/dist/src/Entity/run/Screen/runs.js +169 -0
  49. package/dist/src/Entity/statistics/Screen/dashboard.d.ts +78 -0
  50. package/dist/src/Entity/statistics/Screen/dashboard.js +228 -0
  51. package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +12 -9
  52. package/dist/src/Entity/ticket/Component/comment-item/index.js +18 -13
  53. package/dist/src/Entity/ticket/Component/create-form/index.d.ts +16 -3
  54. package/dist/src/Entity/ticket/Component/create-form/index.js +28 -14
  55. package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +11 -6
  56. package/dist/src/Entity/ticket/Component/edit-form/index.js +77 -29
  57. package/dist/src/Entity/ticket/Component/sections/index.d.ts +8 -0
  58. package/dist/src/Entity/ticket/Component/sections/index.js +42 -0
  59. package/dist/src/Entity/ticket/Component/tab-bar/index.js +6 -0
  60. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +65 -8
  61. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +171 -84
  62. package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +4 -2
  63. package/dist/src/Entity/ticket/Component/ticket-table/index.js +8 -12
  64. package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +47 -3
  65. package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -22
  66. package/dist/src/Entity/ticket/Data/parents.d.ts +13 -1
  67. package/dist/src/Entity/ticket/Data/parents.js +64 -12
  68. package/dist/src/Entity/ticket/Data/query.d.ts +44 -7
  69. package/dist/src/Entity/ticket/Data/query.js +60 -26
  70. package/dist/src/Entity/ticket/Data/tickets.d.ts +170 -4
  71. package/dist/src/Entity/ticket/Data/tickets.js +200 -12
  72. package/dist/src/Entity/ticket/Data/url.d.ts +16 -0
  73. package/dist/src/Entity/ticket/Data/url.js +20 -0
  74. package/dist/src/Entity/ticket/Data/workflow.d.ts +1 -5
  75. package/dist/src/Entity/ticket/Data/workflow.js +5 -13
  76. package/dist/src/Entity/ticket/Data/write.d.ts +32 -0
  77. package/dist/src/Entity/ticket/Data/write.js +80 -0
  78. package/dist/src/Entity/ticket/Form/create-data.d.ts +1 -11
  79. package/dist/src/Entity/ticket/Form/create-data.js +7 -36
  80. package/dist/src/Entity/ticket/Form/create.d.ts +45 -5
  81. package/dist/src/Entity/ticket/Form/create.js +45 -8
  82. package/dist/src/Entity/ticket/Form/edit-data.d.ts +1 -1
  83. package/dist/src/Entity/ticket/Form/edit-data.js +4 -0
  84. package/dist/src/Entity/ticket/Form/edit.d.ts +79 -12
  85. package/dist/src/Entity/ticket/Form/edit.js +165 -18
  86. package/dist/src/Entity/ticket/Form/keys.d.ts +7 -0
  87. package/dist/src/Entity/ticket/Form/keys.js +7 -0
  88. package/dist/src/Entity/ticket/Form/parent-filter.d.ts +36 -0
  89. package/dist/src/Entity/ticket/Form/parent-filter.js +37 -0
  90. package/dist/src/Entity/ticket/Form/parent-source.d.ts +27 -0
  91. package/dist/src/Entity/ticket/Form/parent-source.js +56 -0
  92. package/dist/src/Entity/ticket/Form/who-filter.d.ts +33 -0
  93. package/dist/src/Entity/ticket/Form/who-filter.js +33 -0
  94. package/dist/src/Entity/ticket/Form/who-source.d.ts +18 -0
  95. package/dist/src/Entity/ticket/Form/who-source.js +38 -0
  96. package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +81 -0
  97. package/dist/src/Entity/ticket/Screen/create-ticket-form.js +356 -0
  98. package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +92 -0
  99. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +220 -0
  100. package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +44 -0
  101. package/dist/src/Entity/ticket/Screen/parent-filter-form.js +200 -0
  102. package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +136 -0
  103. package/dist/src/Entity/ticket/Screen/ticket-agents.js +257 -0
  104. package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +125 -0
  105. package/dist/src/Entity/ticket/Screen/ticket-detail.js +415 -0
  106. package/dist/src/Entity/ticket/Screen/tickets.d.ts +145 -0
  107. package/dist/src/Entity/ticket/Screen/tickets.js +538 -0
  108. package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +41 -0
  109. package/dist/src/Entity/ticket/Screen/who-filter-form.js +174 -0
  110. package/dist/src/Entity/user/Data/users.d.ts +17 -0
  111. package/dist/src/Entity/user/Data/users.js +66 -6
  112. package/dist/src/Form/field.d.ts +3 -1
  113. package/dist/src/Form/field.js +1 -0
  114. package/dist/src/Form/form.d.ts +1 -1
  115. package/dist/src/Form/form.js +61 -9
  116. package/dist/src/Form/keys.d.ts +23 -1
  117. package/dist/src/Form/keys.js +78 -23
  118. package/dist/src/Kernel/chrome.d.ts +289 -0
  119. package/dist/src/Kernel/chrome.js +404 -0
  120. package/dist/src/Kernel/clipboard.d.ts +30 -0
  121. package/dist/src/Kernel/clipboard.js +51 -0
  122. package/dist/src/Kernel/key-input.d.ts +50 -0
  123. package/dist/src/Kernel/key-input.js +158 -0
  124. package/dist/src/Kernel/mount.d.ts +187 -0
  125. package/dist/src/Kernel/mount.js +399 -0
  126. package/dist/src/Kernel/tui-kernel.d.ts +0 -11
  127. package/dist/src/Kernel/tui-kernel.js +778 -2367
  128. package/dist/src/Routing/context.d.ts +31 -0
  129. package/dist/src/Routing/context.js +52 -0
  130. package/dist/src/Routing/registry.d.ts +19 -0
  131. package/dist/src/Routing/registry.js +97 -0
  132. package/dist/src/Routing/route.d.ts +35 -0
  133. package/dist/src/Routing/route.js +1 -0
  134. package/dist/src/Routing/router.d.ts +49 -0
  135. package/dist/src/Routing/router.js +123 -0
  136. package/dist/src/Routing/routes.d.ts +17 -0
  137. package/dist/src/Routing/routes.js +57 -0
  138. package/dist/src/Screen/async-screen.d.ts +87 -0
  139. package/dist/src/Screen/async-screen.js +206 -0
  140. package/dist/src/Screen/list-screen.d.ts +193 -0
  141. package/dist/src/Screen/list-screen.js +261 -0
  142. package/dist/src/Screen/screen.d.ts +240 -0
  143. package/dist/src/Screen/screen.js +34 -0
  144. package/dist/src/Tui/controller.d.ts +13 -0
  145. package/dist/src/Tui/controller.js +1794 -0
  146. package/dist/src/Widget/autocomplete.js +75 -3
  147. package/dist/src/Widget/index.d.ts +3 -2
  148. package/dist/src/Widget/index.js +2 -1
  149. package/dist/src/Widget/option-source.d.ts +12 -1
  150. package/dist/src/Widget/option-source.js +14 -1
  151. package/dist/src/Widget/render.d.ts +23 -8
  152. package/dist/src/Widget/render.js +108 -41
  153. package/dist/src/Widget/text.js +94 -5
  154. package/dist/src/Widget/textarea.d.ts +45 -0
  155. package/dist/src/Widget/textarea.js +115 -0
  156. package/dist/src/Widget/types.d.ts +64 -1
  157. package/dist/src/launcher.d.ts +9 -2
  158. package/dist/src/launcher.js +2 -1
  159. package/dist/src/lib/ansi.js +13 -5
  160. package/dist/src/lib/clipboard.d.ts +41 -0
  161. package/dist/src/lib/clipboard.js +65 -0
  162. package/dist/src/lib/cursor.d.ts +31 -0
  163. package/dist/src/lib/cursor.js +45 -0
  164. package/dist/src/lib/format.d.ts +21 -13
  165. package/dist/src/lib/format.js +76 -28
  166. package/dist/src/lib/hyperlink.d.ts +58 -0
  167. package/dist/src/lib/hyperlink.js +104 -0
  168. package/dist/src/lib/input-trace.d.ts +48 -0
  169. package/dist/src/lib/input-trace.js +47 -0
  170. package/dist/src/lib/key-vocabulary.d.ts +19 -0
  171. package/dist/src/lib/key-vocabulary.js +25 -0
  172. package/dist/src/lib/table.d.ts +37 -0
  173. package/dist/src/lib/table.js +20 -0
  174. package/dist/src/lib/vnode.d.ts +217 -0
  175. package/dist/src/lib/vnode.js +456 -0
  176. package/dist/src/lib/width.d.ts +25 -1
  177. package/dist/src/lib/width.js +93 -12
  178. package/dist/src/plugin.js +26 -1
  179. package/dist/src/types.d.ts +67 -2
  180. package/dist/src/types.js +0 -4
  181. package/package.json +4 -2
@@ -0,0 +1,377 @@
1
+ // The ONE JSON:API collection-document shape and the ONE page read (GAIA-294).
2
+ //
3
+ // Slice-less on purpose, directly under `Entity/` beside `transport.ts`,
4
+ // `count.ts` and `cycle.ts`. That is the only placement every slice's `Data/` may
5
+ // import: `tests/boundary.test.ts` G2 forbids a data module from reaching another
6
+ // slice, and its `sliceOf` returns undefined for a file sitting here, so these
7
+ // edges are not cross-slice ones.
8
+ //
9
+ // Before this file the document was declared FIVE times — in ticket/Data/tickets,
10
+ // run/Data/runs, ticket/Data/parents, project/Data/projects and
11
+ // conductor/Data/own-conductor — and only the first two carried `links.next`. Same
12
+ // name, different shapes: the drift GAIA-292's "mirroring Task 3 exactly so the
13
+ // two lists cannot drift" was meant to prevent had already happened.
14
+ import { columnNode, rowNodeId, seamlessRowNode, textNode, } from '../lib/vnode.js';
15
+ import { displayWidth, truncateToWidth } from '../lib/width.js';
16
+ /**
17
+ * The collection total a document reports, or null when it reports none.
18
+ *
19
+ * Defensive by contract rather than by suspicion: this value is rendered to the
20
+ * operator as a total, so anything that is not a usable count must become the
21
+ * explicit unknown instead of a guess (AC 8.5). A count that is absent, not a
22
+ * number, not finite, or negative is no count.
23
+ */
24
+ function readTotal(doc) {
25
+ const count = doc?.meta?.count;
26
+ if (typeof count !== 'number')
27
+ return null;
28
+ if (!Number.isFinite(count) || count < 0)
29
+ return null;
30
+ return count;
31
+ }
32
+ /**
33
+ * One cell's text: the value, cut with an ELLIPSIS when it does not fit.
34
+ *
35
+ * PLACEMENT of the cell is Yoga's — a column's `width` is its budget, and
36
+ * `truncate` (`lib/vnode.ts`) is the renderer's backstop. What Yoga cannot say is
37
+ * that something was REMOVED: `@opentui/core@0.5.1` cuts at the last word
38
+ * boundary inside the budget and appends nothing, so `cancelled` reached the
39
+ * operator as `cancelle` in the ticket list's 8-column STATE column and
40
+ * `qualification` as `qualific` — a cut value that reads as a typo. Measured on a
41
+ * real pty at 120x40; the ellipsis is what makes the difference legible.
42
+ *
43
+ * `lib/format.ts`'s deleted `pad()` cut at `width - 1` and appended `…`, and this
44
+ * is that one behaviour and nothing else. The padding it also did is NOT Yoga's —
45
+ * an earlier revision of this comment said so, and GAIA-345 made it false in the
46
+ * same commit that wrote it: a Yoga budget places a cell but writes no glyph into
47
+ * the columns its value does not reach, which is why the selection highlight came
48
+ * out in fragments. The pad is {@link cellFill}'s again, deliberately, and the
49
+ * argument for it is written there and in {@link tableRow}. What did NOT come
50
+ * back is `pad`'s reason for existing: `cellFill` fills to a width the COLUMN
51
+ * DECLARES, never to one measured off the page.
52
+ * Reaching `lib/width.ts` for the measurement is a DELIBERATE decision on top of
53
+ * A2 already being reported as missed (`docs/evidence/GAIA-340/teardown-a2.txt`)
54
+ * — the module survives this ticket, so the alternative is not "no measurement"
55
+ * but "an illegible cut", and R5's rule against re-implementing it under another
56
+ * name is honoured by calling it rather than copying it.
57
+ *
58
+ * `width <= 0` is the TRAILING column — "no budget", not "zero budget": it takes
59
+ * whatever the fixed columns left, how much that is is Yoga's answer, and `pad`
60
+ * rendered it raw for the same reason.
61
+ */
62
+ const cellText = (raw, width) => {
63
+ const text = raw === null || raw === undefined ? '' : String(raw);
64
+ if (width <= 0 || displayWidth(text) <= width)
65
+ return text;
66
+ return `${truncateToWidth(text, width - 1)}…`;
67
+ };
68
+ /**
69
+ * The fill for a cell whose resolved width nothing declares — a GROWING column.
70
+ *
71
+ * A grower has no declared width by construction: it takes whatever the row's
72
+ * budgeted columns left, and how much that is is Yoga's answer, arrived at long
73
+ * after this tree is built. So its content is padded PAST any width it could
74
+ * resolve to and the renderer cuts the surplus — measured: a `width: 0` cell
75
+ * whose content was padded to 200 painted 49 inverse columns at a 60-column
76
+ * terminal, the code never knowing the 49.
77
+ *
78
+ * 512 is deliberate on both sides. Too small leaves the bar short of the right
79
+ * edge on a wide terminal — a visible defect, and the one this fill exists to
80
+ * prevent; 512 is four times the cockpit's own 120-column default and beyond any
81
+ * terminal it is realistically painted into, and a display wide enough to beat it
82
+ * would end the bar early rather than break anything. Too large costs bytes on
83
+ * every painted row: the string is built per cell per frame, and at 512 one
84
+ * grower across ~40 visible rows is ~20 kB of transient string per frame. Only a
85
+ * column that GROWS pays it — a budgeted column is padded to its own declared
86
+ * width instead, which is a handful of blanks.
87
+ */
88
+ const GROWN_CELL_FILL = 512;
89
+ /** The pad, allocated ONCE and sliced rather than rebuilt per cell. */
90
+ const BLANKS = ' '.repeat(GROWN_CELL_FILL);
91
+ /**
92
+ * A cell's content, PADDED so the row's attribute covers the columns the value
93
+ * does not — GAIA-345, the mechanism behind the continuous bar.
94
+ *
95
+ * CONTENT GENERATION, NOT LAYOUT MEASUREMENT, and the distinction is the whole
96
+ * argument for it: the target is a number the COLUMN DECLARES, never a width
97
+ * measured off the page (spec D4), and the cell resolves to exactly the columns
98
+ * it resolved to before — Yoga is handed the same budget either way. `cellText`
99
+ * and `elideLeft` are the two sanctioned `displayWidth` callers because they
100
+ * mark a cut against a declared width; this is the third and it is the same
101
+ * shape, one step further: it FILLS to a declared width.
102
+ *
103
+ * The two branches are not a style choice, they are what the renderer allows:
104
+ *
105
+ * - A column that cannot grow resolves to exactly `columns`, so its content is
106
+ * padded to exactly `columns` and never overflows. That matters: an
107
+ * OVER-padded fixed cell is an overflowing cell, and the renderer answers an
108
+ * overflow by word-wrapping it. With `height: 1` only the first wrapped line
109
+ * is drawn — measured, a 14-column STATE cell holding ` qualification` plus
110
+ * surplus blanks broke after the leading space and painted BLANK.
111
+ *
112
+ * GAIA-345 (re-entry) — "resolves to exactly `columns`" HOLDS ONLY WHILE THE
113
+ * CELL CANNOT SHRINK, and a `min` is what makes it able to: `textNode` sets
114
+ * `flexShrink: 1` the moment a floor says how far. Such a cell is still padded
115
+ * to its full declared width here — the pad is what the row's highlight rides
116
+ * on, and shortening it to the floor would end the bar early whenever the cell
117
+ * resolved above it. What changed is on the renderer's side: `textNode` now
118
+ * gives every cell that can resolve to an undeclared width, shrinkable ones
119
+ * included, `wrapMode: 'none'`, so the surplus is HARD-CLIPPED instead of
120
+ * word-wrapped and the failure measured just above cannot occur. The two files
121
+ * have to agree about this and the agreement is the fix; see `flexible` in
122
+ * `lib/vnode.ts`.
123
+ * - A column that grows has no such number, so it is over-padded on purpose and
124
+ * the wrap discards the surplus. Measured on this renderer: the blanks reach
125
+ * the cell's own right edge before the wrap takes effect, at every shape this
126
+ * table builds (grower first in the row, grower after a fixed column, and a
127
+ * `max`-capped grower between two).
128
+ *
129
+ * `wrapMode: 'none'` is NOT the alternative for the second branch. Measured: it
130
+ * turns the overflow into the renderer's own `…` truncation, so a value that fits
131
+ * its cell perfectly comes out cut — `Centralize the TUI list…` in a cell wide
132
+ * enough for the whole of it.
133
+ */
134
+ const cellFill = (text, columns, grows) => grows
135
+ ? `${text}${BLANKS}`
136
+ : `${text}${BLANKS.slice(0, Math.max(0, columns - displayWidth(text)))}`;
137
+ /**
138
+ * A row of cells, one per column, placed by YOGA and PAINTED to its edges.
139
+ *
140
+ * A column's `width` is its Yoga budget; the only thing measured here is whether
141
+ * the value outgrew it, so that {@link cellText} can say so. That is the boundary
142
+ * R5/D8 draws: `lib/table.ts`'s `pad()` FILLED the field itself, which is the
143
+ * work Yoga does now. The trailing column declares width 0 and takes what is
144
+ * left.
145
+ *
146
+ * GAIA-345 — TWO THINGS BEYOND THAT MAKE THE ROW'S ATTRIBUTES A CONTINUOUS BAR,
147
+ * and neither is layout:
148
+ *
149
+ * 1. Each cell's content is padded with blanks past its own DECLARED width
150
+ * ({@link cellFill}), so the attribute covers the cell's unused columns as
151
+ * well as its glyphs. This is CONTENT GENERATION, not measurement: the pad
152
+ * is as long as a number the column declares, and no value is ever measured
153
+ * to compute it. `lib/vnode.ts`'s "NOTHING HERE MEASURES A STRING" still
154
+ * binds for LAYOUT, and this changes no layout — a padded cell resolves to
155
+ * exactly the columns it resolved to before.
156
+ * 2. The row is a {@link seamlessRowNode}, and the ` ` that was its Yoga `gap`
157
+ * is a leading blank column of the cell that FOLLOWS it. A gap carries no
158
+ * text and so can carry no attribute; a blank column of a cell does. Every
159
+ * cell but the first is therefore ONE column wider than its column declares,
160
+ * bounds included, and the row's total is unchanged: the same
161
+ * `sum(width) + (n - 1)` the gap produced.
162
+ *
163
+ * The two ids are handed in rather than derived from one another, because an
164
+ * entity id contains hyphens: a cell keyed `<row id>-<column>` would be
165
+ * indistinguishable from a row of some other entity, and "which nodes are the
166
+ * rows" is a question the mouse and every test has to be able to answer.
167
+ */
168
+ const tableRow = (ids, columns, value, { selected, bold }) => seamlessRowNode(ids.row, ...columns.map((column, index) => {
169
+ // The separator, owned by the cell that follows it. The first cell has
170
+ // none: a row starts at the row's own left edge.
171
+ const lead = index === 0 ? '' : ' ';
172
+ // A `width: 0` column IS a grower — `textNode` gives it `flexGrow: 1` when
173
+ // it declares none — so both spellings of "this cell can outgrow what it
174
+ // declares" ask for the over-padded fill.
175
+ const grows = column.width === 0 || (column.grow ?? 0) > 0;
176
+ // GAIA-345 (pass 3) — THE WIDTH A NON-GROWING COLUMN REALLY DECLARES,
177
+ // which is not always its `width`, and the single number both the fill
178
+ // and the cut below are derived from.
179
+ //
180
+ // `width` is the Yoga BASIS; the bounds are what Yoga resolves it to. A
181
+ // `maxWidth` clamps the basis whether or not the cell may shrink —
182
+ // MEASURED, and it is the only thing the typings could not have said: a
183
+ // `{ width: 14, max: 6 }` column with `flexShrink: 0` resolved to 7
184
+ // columns (6 plus its separator) in a row with 11 columns of surplus, so
185
+ // the column after it started 8 columns earlier than its basis implied.
186
+ // A `minWidth` then wins over both, which is Yoga's own precedence.
187
+ // `max(min ?? 0, min(width, max ?? width))` is that precedence written
188
+ // out, and it is a NO-OP for every column that declares no bound and for
189
+ // every `min` below / `max` above the basis — which is every column this
190
+ // tree ships (none declares a `max` at all).
191
+ //
192
+ // Deriving it HERE is what keeps the two files from disagreeing, and the
193
+ // disagreement was two live defects rather than an untidiness:
194
+ //
195
+ // - `cellFill` padded to the basis while the cell resolved to the cap,
196
+ // so the content overflowed. Such a cell was not `flexible` in
197
+ // `lib/vnode.ts` — a cap is neither a `grow` nor a `min` — so it kept
198
+ // `truncate: true` with no `wrapMode`, the renderer WORD-WRAPPED the
199
+ // overflow and `height: 1` drew the first wrapped line: measured, a
200
+ // `{ width: 14, max: 6 }` cell holding `qualification` painted BLANK,
201
+ // the break falling after the leading separator. Same shape as the
202
+ // `min` case recorded in {@link cellFill}, one more way in.
203
+ // - `cut` was `column.width`, so the ellipsis was marked at the BASIS
204
+ // while the applied cap was the `max`: a value between the two was
205
+ // silently not marked as cut, and one longer than the basis was marked
206
+ // in a column the operator never sees. Widening `flexible` alone fixes
207
+ // the blank cell and leaves THIS half broken, which is why the fix is
208
+ // here and the predicate is only the backstop for `textNode`'s other
209
+ // callers.
210
+ //
211
+ // The `min > width` case is the same expression's other edge and was
212
+ // cosmetic rather than lost text: such a cell resolves to its FLOOR, and
213
+ // padding it to the basis ended the row's highlight 15 columns before the
214
+ // cell did (measured on a `{ width: 5, min: 20 }` probe: 14 inverse
215
+ // columns where the row paints 29). It cannot wrap, because a `min` makes
216
+ // the cell `flexible` and so already gives it `wrapMode: 'none'`.
217
+ //
218
+ // A GROWER keeps its basis untouched. Its resolved width is a share of
219
+ // the row's surplus rather than a clamp of its basis, its fill is
220
+ // {@link GROWN_CELL_FILL} and not a declared number at all, and lowering
221
+ // its basis would change how the OTHER growers of the row divide that
222
+ // surplus — a real rendering change, on the one shape this ticket ships.
223
+ const declared = grows
224
+ ? column.width
225
+ : Math.max(column.min ?? 0, Math.min(column.width, column.max ?? column.width));
226
+ const budget = column.width > 0 ? declared + lead.length : 0;
227
+ // GAIA-345 — the width the VALUE is cut against, which is not the Yoga
228
+ // basis once a column can grow. A grower's `width` is where its share
229
+ // STARTS, so cutting there would mark an ellipsis at 14 in a cell Yoga
230
+ // resolved to 32 — the projects and conductors lists both have such a
231
+ // column. What a grower can be cut against is its `max`, if it declares
232
+ // one; where it declares none there is no declared width at all, and the
233
+ // cut falls to the renderer's own clip, unmarked (spec AC 5). `0` is
234
+ // `cellText`'s "no budget", which is what the trailing column always
235
+ // passed.
236
+ const cut = grows ? (column.max ?? 0) : declared;
237
+ return textNode({
238
+ id: ids.cell(column.key),
239
+ content: cellFill(`${lead}${cellText(value(column), cut)}`, budget, grows),
240
+ width: budget,
241
+ // GAIA-345 D3 — the column's declared bounds, handed to Yoga to execute.
242
+ // SPREAD rather than assigned, because `exactOptionalPropertyTypes` makes
243
+ // `min: undefined` a different thing from an absent `min`: the absent one
244
+ // is what means "this column keeps today's behaviour", and `textNode` is
245
+ // where that turns into `flexShrink: 0` and no bound at all. Each bound
246
+ // carries the separator too, or a `min` would floor the cell one column
247
+ // below the columns its own column declares.
248
+ ...(column.min === undefined ? {} : { min: column.min + lead.length }),
249
+ ...(column.max === undefined ? {} : { max: column.max + lead.length }),
250
+ ...(column.grow === undefined ? {} : { grow: column.grow }),
251
+ selected,
252
+ // GAIA-345 D5 — STATED ON EVERY ROW, off included, for the reason
253
+ // `lib/vnode.ts` gives about `selected` right beside it: `applyProps`
254
+ // writes only what the next tree declares, so a `bold` emitted for the
255
+ // header alone would be set once and never taken off the row that
256
+ // inherited its node.
257
+ bold,
258
+ });
259
+ }));
260
+ /** A column-driven list's row ids in selection order — see `lib/vnode.ts`'s
261
+ * `rowNodeId`. Beside {@link listTableTree}, which keys the rows under it, and
262
+ * reading the SAME `noun` off the same configuration (GAIA-345 D2). */
263
+ export const listRowIds = (table, rows) => rows.map((row) => rowNodeId(table.noun, row.id));
264
+ /**
265
+ * GAIA-340 D1 — a column-driven list as the renderable tree a screen hands over.
266
+ *
267
+ * One node per row, keyed `<noun>-row-<entity id>` (D4/R4) and never by position:
268
+ * a list re-sorts on every search, page and reload, and a positional key would
269
+ * move one ticket's cells onto another ticket's node with nothing on screen to
270
+ * say so. The cells are keyed within their row by the COLUMN's own `key`, which
271
+ * is what makes the whole set unique by construction.
272
+ *
273
+ * MEASURED, so the choice is not re-litigated: `TextTableRenderable` was tried and
274
+ * rejected. It is ONE renderable whose rows are internal cell views, so no row can
275
+ * carry a `props.id` — the identity `Kernel/mount.ts` patches by, and the identity
276
+ * the mouse will need when it moves onto the renderables — and its column widths
277
+ * are computed from the CONTENT, so the columns would shift every time the page's
278
+ * data changes.
279
+ *
280
+ * THE HIGHLIGHT IS A FULL-WIDTH BAR, and GAIA-345 is where that was decided.
281
+ * Until then this file argued the opposite — that a bar "needs the row's blank
282
+ * columns painted, and the only ways to paint them are a padded string or a fixed
283
+ * colour, and both are worse". The operator overruled it on sight: a highlight
284
+ * covering only the glyphs came out as one fragment per cell, 58 of 120 columns
285
+ * in eight pieces, and read as "only the title of the row is active". The two
286
+ * costs that argument weighed are not the ones that were paid, either. The fill
287
+ * is a blank run as long as a number the COLUMN DECLARES, so no value is measured
288
+ * to build it and `lib/width.ts` is not resurrected under a new name (see
289
+ * {@link tableRow}); and the paint is still the operator's own inverse attribute,
290
+ * so no colour is frozen and the GAIA-326 regression stays fixed. What the
291
+ * argument got right is that the row's blank columns have to be painted — it was
292
+ * wrong that the only ways of doing it were the two bad ones.
293
+ *
294
+ * GAIA-340 Task 8 moved it HERE from `Screen/list-screen.ts`, where Task 7 first
295
+ * wrote it, for the reason {@link ListColumn} gives above it: the run table has a
296
+ * SECOND consumer in the ticket detail's Runs tab (GAIA-255 AC-6 renders a run
297
+ * the same way on both screens), that consumer is a `Component/`, and
298
+ * `tests/boundary.test.ts` G5 forbids anything but an entity-local `Screen/` the
299
+ * `Screen/` directory. Duplicating the renderer instead would have made "one
300
+ * renderer, so the two screens cannot disagree" false.
301
+ */
302
+ export function listTableTree(table, rows, ctx, { selectedIndex }) {
303
+ const { noun, columns, emptyMessage, header = true } = table;
304
+ return columnNode(`${noun}-table`,
305
+ // GAIA-345 D5/D6 — the header is BOLD, which is what distinguishes it from a
306
+ // data row, and it is optional because one list is not a record set: the
307
+ // project picker asks a question and configures it away.
308
+ ...(header
309
+ ? [
310
+ tableRow({
311
+ row: `${noun}-header`,
312
+ cell: (key) => `${noun}-header-${key}`,
313
+ }, columns, (column) => column.label, { selected: false, bold: true }),
314
+ ]
315
+ : []), ...(rows.length === 0
316
+ ? [textNode({ id: `${noun}-empty`, content: emptyMessage })]
317
+ : rows.map((row, index) => tableRow({
318
+ row: rowNodeId(noun, row.id),
319
+ cell: (key) => `${noun}-cell-${row.id}-${key}`,
320
+ }, columns, (column) => column.get(row, ctx), { selected: index === selectedIndex, bold: false }))));
321
+ }
322
+ /**
323
+ * GAIA-345 D7 — the SAME list for the surface that has no layout:
324
+ * `gaia ui GAIA-nnn | cat`.
325
+ *
326
+ * GAIA-340 F5 keeps the pipe's own output, and this keeps its BYTES while
327
+ * retiring the last per-entity column→row mapper (`run-table`'s `runTableRows`):
328
+ * each cell carries the value and the column's own budget exactly as that emitted
329
+ * them, and `Component/section-list`'s `lineText` still joins them with ONE
330
+ * space. A configuration therefore feeds both surfaces, which is what makes "one
331
+ * component renders every list" true of the run table rather than of its terminal
332
+ * half only.
333
+ *
334
+ * The values are RAW here where {@link listTableTree} runs them through
335
+ * {@link cellText}. That is not an oversight and not a second rendering rule: the
336
+ * ellipsis exists because a Yoga budget cuts silently, and a pipe has no budget
337
+ * to cut against — it prints whatever it is given, at whatever width the reader's
338
+ * terminal or file happens to be. Cutting here would be a cut nobody asked for.
339
+ *
340
+ * The header follows `table.header`, so a headerless list prints headerless and
341
+ * the two surfaces cannot disagree about whether this list has one.
342
+ */
343
+ export function listTableRows(table, rows, ctx) {
344
+ const line = (value) => ({
345
+ cells: table.columns.map((column) => {
346
+ const raw = value(column);
347
+ return {
348
+ text: raw === null || raw === undefined ? '' : String(raw),
349
+ width: column.width,
350
+ };
351
+ }),
352
+ });
353
+ const head = table.header === false ? [] : [line((column) => column.label)];
354
+ if (rows.length === 0)
355
+ return [...head, table.emptyMessage];
356
+ return [
357
+ ...head,
358
+ ...rows.map((row) => line((column) => column.get(row, ctx))),
359
+ ];
360
+ }
361
+ /**
362
+ * Read one page of a collection and map it to rows.
363
+ *
364
+ * `mapRows` receives `data` AND `included` so each slice keeps its own sideload
365
+ * indexing — the ticket list indexes every include by id, the run list builds a
366
+ * ticket-only map — without this function knowing either.
367
+ */
368
+ export async function loadPage(client, path, params, mapRows, page, window) {
369
+ const doc = (await client.get(path, params));
370
+ return {
371
+ rows: mapRows(doc?.data ?? [], doc?.included ?? []),
372
+ hasNext: Boolean(doc?.links?.next),
373
+ page,
374
+ total: readTotal(doc),
375
+ window,
376
+ };
377
+ }
@@ -0,0 +1,25 @@
1
+ /** The minimum shape a conductor row needs to render. */
2
+ export interface ConductorRowInput {
3
+ label: string;
4
+ machineId: string | undefined;
5
+ project: string;
6
+ derivedStatus: string;
7
+ ageLabel: string;
8
+ workspaceRoot: string;
9
+ lastPid: number | undefined;
10
+ /** Whether this conductor's workspace exists on THIS machine. */
11
+ isLocal: boolean;
12
+ /** Whether an action on this row is in flight. */
13
+ busy: boolean;
14
+ }
15
+ /**
16
+ * One line per conductor: who, where, alive, since when, and its process.
17
+ *
18
+ * A non-local row is marked rather than hidden. The control plane is
19
+ * user-scoped, not machine-scoped, so a user's other machines' conductors are
20
+ * legitimately in this list — and silently dropping them would make the cockpit
21
+ * disagree with `gaia conductor ls` for no visible reason. Marking them says
22
+ * why their actions are inert here: a pid from another machine means nothing on
23
+ * this one.
24
+ */
25
+ export declare function conductorRows(conductors: ConductorRowInput[]): string[];
@@ -0,0 +1,60 @@
1
+ import { pad } from '../../../../lib/format.js';
2
+ /** How much width the workspace column gets before it is elided. */
3
+ const PATH_WIDTH = 30;
4
+ /**
5
+ * How much width the machine_id column gets before it is elided.
6
+ *
7
+ * A `machine_id` is COMPOSED (`${user_id}-${machine_id}-${project}`), so on a
8
+ * user-scoped control plane every row repeats the same uuid at the head. It is
9
+ * therefore elided from the left like the path: the tail is the only part that
10
+ * tells one conductor from another.
11
+ */
12
+ const MACHINE_ID_WIDTH = 20;
13
+ /**
14
+ * One line per conductor: who, where, alive, since when, and its process.
15
+ *
16
+ * A non-local row is marked rather than hidden. The control plane is
17
+ * user-scoped, not machine-scoped, so a user's other machines' conductors are
18
+ * legitimately in this list — and silently dropping them would make the cockpit
19
+ * disagree with `gaia conductor ls` for no visible reason. Marking them says
20
+ * why their actions are inert here: a pid from another machine means nothing on
21
+ * this one.
22
+ */
23
+ export function conductorRows(conductors) {
24
+ if (conductors.length === 0)
25
+ return ['No conductors on this control plane.'];
26
+ return conductors.map((c) => {
27
+ const name = c.label !== '' ? c.label : (c.machineId ?? '');
28
+ const pid = c.lastPid === undefined ? '-' : String(c.lastPid);
29
+ const marks = [c.busy ? '…' : '', c.isLocal ? '' : 'elsewhere']
30
+ .filter((m) => m !== '')
31
+ .join(' ');
32
+ return [
33
+ pad(name, 20),
34
+ // Its OWN column, not merely the label's fallback (AC-1, spec D2): two
35
+ // conductors of one repo share a project and a workspace_root, and the
36
+ // machine_id is the only thing that separates them.
37
+ pad(elideLeft(c.machineId ?? '', MACHINE_ID_WIDTH), MACHINE_ID_WIDTH),
38
+ pad(c.project, 14),
39
+ pad(c.derivedStatus, 9),
40
+ pad(c.ageLabel, 8),
41
+ pad(elideLeft(c.workspaceRoot, PATH_WIDTH), PATH_WIDTH),
42
+ pad(pid, 7),
43
+ marks,
44
+ ]
45
+ .join(' ')
46
+ .trimEnd();
47
+ });
48
+ }
49
+ /**
50
+ * Shorten a path from the LEFT, keeping its end.
51
+ *
52
+ * Conductor workspaces share a long common head (`/home/<user>/projects/…`) and
53
+ * differ at the tail, which is also the part that identifies the checkout.
54
+ * Eliding the right would leave every row looking identical.
55
+ */
56
+ function elideLeft(path, width) {
57
+ if (path.length <= width)
58
+ return path;
59
+ return `…${path.slice(path.length - (width - 1))}`;
60
+ }