@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
@@ -17,9 +17,9 @@ export declare function buildCreateLaunch(opts: {
17
17
  label?: string | undefined;
18
18
  }): CreateLaunch;
19
19
  /**
20
- * A short human label for the agent list: the description's first line (capped to 40
21
- * incl. the ellipsis), else "New <workflow>". GAIA-236 AC-8: it builds the `label`
22
- * `buildCreateLaunch` takes, so it belongs to the module that builds the launch
23
- * intent moved from `Service/create-form.ts` unchanged.
20
+ * A short human label for the agent list: the ticket's title, else the
21
+ * description's first line (both capped to 40 incl. the ellipsis), else
22
+ * "New <workflow>". GAIA-236 AC-8: it builds the `label` `buildCreateLaunch`
23
+ * takes, so it belongs to the module that builds the launch intent.
24
24
  */
25
25
  export declare function shortName(payload: CreateFormPayload): string;
@@ -32,18 +32,25 @@ export function buildCreateLaunch(opts) {
32
32
  };
33
33
  }
34
34
  /**
35
- * A short human label for the agent list: the description's first line (capped to 40
36
- * incl. the ellipsis), else "New <workflow>". GAIA-236 AC-8: it builds the `label`
37
- * `buildCreateLaunch` takes, so it belongs to the module that builds the launch
38
- * intent moved from `Service/create-form.ts` unchanged.
35
+ * A short human label for the agent list: the ticket's title, else the
36
+ * description's first line (both capped to 40 incl. the ellipsis), else
37
+ * "New <workflow>". GAIA-236 AC-8: it builds the `label` `buildCreateLaunch`
38
+ * takes, so it belongs to the module that builds the launch intent.
39
39
  */
40
40
  export function shortName(payload) {
41
+ // GAIA-309 D9: the title is what the operator NAMED the ticket, so it is the
42
+ // better tab label; the description's first line stays the fallback for a
43
+ // payload that has no title yet. This touches the launch intent's `label`
44
+ // ONLY — never `prompt`, never `env`, which is what AC-6 pins.
45
+ const cap = (line) => line.length > 40 ? `${line.slice(0, 39)}…` : line;
46
+ const title = payload.title.trim();
47
+ if (title)
48
+ return cap(title);
41
49
  const firstLine = payload.description
42
50
  .split('\n')
43
51
  .map((l) => l.trim())
44
52
  .find((l) => l.length > 0);
45
- if (firstLine) {
46
- return firstLine.length > 40 ? `${firstLine.slice(0, 39)}…` : firstLine;
47
- }
53
+ if (firstLine)
54
+ return cap(firstLine);
48
55
  return `New ${payload.workflow?.label ?? 'ticket'}`;
49
56
  }
@@ -1,8 +1,104 @@
1
+ import type { VNode } from '@opentui/core';
2
+ import { type Cell } from '../../lib/vnode.js';
3
+ /**
4
+ * A section body row that is COLUMNS rather than prose — GAIA-340 Task 13.
5
+ *
6
+ * The two producers are the dashboard's bars and the ticket detail's Runs
7
+ * table, and both used to compose their columns into one padded string with
8
+ * `lib/format.ts`'s `pad`. They describe their cells instead: the tree lays them
9
+ * out with Yoga budgets, and a surface with no layout — the pipe — prints
10
+ * {@link lineText}'s join. That is the operator-approved cost of retiring the
11
+ * padding, and it is what lets `pad` (and with it two of `lib/width.ts`'s
12
+ * consumers) go.
13
+ */
14
+ export interface SectionCells {
15
+ readonly cells: readonly Cell[];
16
+ }
17
+ /** One body row of a section: prose, or columns a renderer places itself. */
18
+ export type SectionLine = string | SectionCells;
19
+ /**
20
+ * A line as the text a surface with no layout prints.
21
+ *
22
+ * The separator is ONE space because that is the gap `lib/vnode.ts`'s `rowNode`
23
+ * puts between two cells: the pipe and the tree then say the same thing about
24
+ * what separates two columns, and only the alignment inside a column is the
25
+ * renderer's own.
26
+ */
27
+ export declare const lineText: (line: SectionLine) => string;
1
28
  /** A titled block of already-flattened physical rows. */
2
29
  export interface Section {
3
30
  heading: string;
4
- lines: string[];
31
+ lines: SectionLine[];
32
+ }
33
+ /**
34
+ * The MINIMAL MARKDOWN rule: an ATX heading (`#`…`######`) renders as bold with
35
+ * the markers stripped — the same rule a section heading follows. Bullets and
36
+ * code fences stay raw by design (explicit v1 decision).
37
+ *
38
+ * ONE function, because every renderer of a body line goes through it:
39
+ * {@link sectionRows} for a flat section, {@link expandableRows} for a
40
+ * collapsible one, and `Entity/run/Component/run-detail` for a log entry. The
41
+ * Description block is rendered by two of them at once — flat on the piped
42
+ * render, expandable on the Overview tab — and a rule living in only one would
43
+ * make the same body look different depending on which renderer reached it.
44
+ */
45
+ export declare function markdownRow(line: string): {
46
+ text: string;
47
+ bold: boolean;
48
+ };
49
+ /**
50
+ * One flattened row: its text, whether it is bold, and WHICH ROW IT IS
51
+ * (GAIA-340 D3/D4).
52
+ *
53
+ * `key` is what a converted screen turns into its node's `props.id`, so a row
54
+ * has to be able to say what it is rather than where it sits. It is scoped by
55
+ * the section's own HEADING — `per-user-0`, never `row-14` — so a section keeps
56
+ * its keys when the section above it is absent. Two sections sharing a heading
57
+ * would collide, and `Kernel/mount.ts` refuses a repeated id loudly rather than
58
+ * patching one row's content onto another's node.
59
+ */
60
+ export interface SectionRow {
61
+ readonly key: string;
62
+ readonly text: string;
63
+ readonly bold: boolean;
64
+ /**
65
+ * The row's columns, when it has any — see {@link SectionCells}.
66
+ *
67
+ * `text` is the same row for a surface that cannot lay one out, so a consumer
68
+ * that only prints never has to know the difference; {@link sectionRowNode} is
69
+ * where the difference is made.
70
+ */
71
+ readonly cells?: readonly Cell[];
5
72
  }
73
+ /**
74
+ * A heading as an id fragment: `PER USER` → `per-user`.
75
+ *
76
+ * Exported because a tab that renders one of its sections as a COLLAPSIBLE entry
77
+ * has to key that entry the same way `sectionRows` keys a flat one — otherwise
78
+ * the same section would carry different node ids depending on which tab reached
79
+ * it, and `patchTree` would rebuild it instead of writing to it (GAIA-340 D4).
80
+ */
81
+ export declare const sectionKey: (heading: string) => string;
82
+ /**
83
+ * Flatten `{heading, lines}` sections into KEYED rows: the heading, its body,
84
+ * and the blank line that closes the section.
85
+ *
86
+ * The one description of what a section renders as. {@link flattenSections}
87
+ * derives the row surface from it and a converted screen derives its nodes from
88
+ * it, so the two cannot answer differently for the same document — which is the
89
+ * property that lets the Dashboard hand over renderables while its pipe keeps
90
+ * emitting the bytes it always did (spec F5).
91
+ */
92
+ export declare function sectionRows(sections: Section[]): SectionRow[];
93
+ /**
94
+ * One flattened row as the node a converted screen hands over — GAIA-340 D1.
95
+ *
96
+ * The ONE place a `SectionRow` becomes a renderable, so the two screens that
97
+ * render section rows (the dashboard and the ticket detail's tabs) cannot
98
+ * disagree about what a columned row looks like. `prefix` is the screen's own
99
+ * body id, which is what keeps two screens' rows apart in one tree.
100
+ */
101
+ export declare const sectionRowNode: (prefix: string, row: SectionRow, selected?: boolean) => VNode;
6
102
  export declare function flattenSections(sections: Section[]): {
7
103
  lines: string[];
8
104
  boldRows: Set<number>;
@@ -16,6 +112,12 @@ export declare function flattenSections(sections: Section[]): {
16
112
  * and structural sharing is the only form of that claim a test can hold.
17
113
  */
18
114
  export interface ExpandableEntry {
115
+ /**
116
+ * What this entry IS, as an id fragment (GAIA-340 D4/R4) — a comment's uuid,
117
+ * a section's slug. Its rows are keyed under it, so an entry's content and its
118
+ * identity move together when the list re-sorts.
119
+ */
120
+ key: string;
19
121
  /** The header row's text, after the caret. */
20
122
  header: string;
21
123
  /** Rows shown while collapsed (indented like the expanded body). */
@@ -25,22 +127,29 @@ export interface ExpandableEntry {
25
127
  }
26
128
  /** The rendered rows of a list of expandable entries. */
27
129
  export interface ExpandableView {
28
- lines: string[];
29
- boldRows: Set<number>;
130
+ rows: SectionRow[];
30
131
  /** `headerRows[i]` is the row index of entry i's header — the selection map. */
31
132
  headerRows: number[];
32
133
  selectable: number;
33
134
  }
34
135
  /**
35
- * Flatten expandable entries: one caret header row per entry (`▸` collapsed,
36
- * `▾` expanded) followed by that state's rows. `headerRows[i]` is entry i's
37
- * header row, which is both the highlight target and the click map (the rows are
38
- * NOT uniformly spaced — an expanded entry appends its body).
136
+ * Expandable entries as KEYED rows: one caret header row per entry (`▸`
137
+ * collapsed, `▾` expanded) followed by that state's rows. `headerRows[i]` is
138
+ * entry i's header row, which is both the highlight target and the click map
139
+ * (the rows are NOT uniformly spaced — an expanded entry appends its body).
39
140
  *
40
141
  * Zero entries render `emptyMessage` and nothing selectable, so an empty list is
41
142
  * an explicit statement rather than a blank screen.
143
+ *
144
+ * GAIA-340 D1/D3 — rows rather than lines-plus-a-`boldRows`-set, and the two
145
+ * halves of that are the same change: the caller turns each row into a node, so
146
+ * "this row is a heading" is a prop on the node and no longer an index into a
147
+ * surface somebody else styles. `SectionRow` is the shape {@link sectionRows}
148
+ * already produces, so a tab that mixes flat sections with expandable ones has
149
+ * one row type rather than two.
42
150
  */
43
- export declare function flattenExpandable(entries: ExpandableEntry[], { expanded, emptyMessage, }: {
151
+ export declare function expandableRows(entries: ExpandableEntry[], { expanded, emptyMessage, emptyKey, }: {
44
152
  expanded?: ReadonlySet<number>;
45
153
  emptyMessage: string;
154
+ emptyKey: string;
46
155
  }): ExpandableView;
@@ -1,51 +1,120 @@
1
+ import { cellsRow, textNode } from '../../lib/vnode.js';
1
2
  /**
2
- * Append one body row, applying the MINIMAL MARKDOWN rule: an ATX heading
3
- * (`#`…`######`) renders as bold with the markers stripped — the same rule a
4
- * section heading follows, styled after fit(). Bullets and code fences stay raw
5
- * by design (explicit v1 decision).
3
+ * A line as the text a surface with no layout prints.
6
4
  *
7
- * Extracted so `flattenSections` and `flattenExpandable` cannot answer "what does
8
- * a `## Context` line render as" differently: the Description block is rendered
9
- * by BOTH (flat on the piped render, expandable on the Overview tab), and a rule
10
- * living in only one of them would make the same body look different depending on
11
- * which renderer reached it.
5
+ * The separator is ONE space because that is the gap `lib/vnode.ts`'s `rowNode`
6
+ * puts between two cells: the pipe and the tree then say the same thing about
7
+ * what separates two columns, and only the alignment inside a column is the
8
+ * renderer's own.
12
9
  */
13
- function pushRow(lines, boldRows, line) {
10
+ export const lineText = (line) => typeof line === 'string'
11
+ ? line
12
+ : line.cells
13
+ .map((cell) => cell.text)
14
+ .join(' ')
15
+ .replace(/\s+$/, '');
16
+ /**
17
+ * The MINIMAL MARKDOWN rule: an ATX heading (`#`…`######`) renders as bold with
18
+ * the markers stripped — the same rule a section heading follows. Bullets and
19
+ * code fences stay raw by design (explicit v1 decision).
20
+ *
21
+ * ONE function, because every renderer of a body line goes through it:
22
+ * {@link sectionRows} for a flat section, {@link expandableRows} for a
23
+ * collapsible one, and `Entity/run/Component/run-detail` for a log entry. The
24
+ * Description block is rendered by two of them at once — flat on the piped
25
+ * render, expandable on the Overview tab — and a rule living in only one would
26
+ * make the same body look different depending on which renderer reached it.
27
+ */
28
+ export function markdownRow(line) {
14
29
  const md = /^(#{1,6})\s+(.*)$/.exec(line);
15
- if (md) {
16
- boldRows.add(lines.length);
17
- lines.push(md[2] ?? '');
18
- }
19
- else {
20
- lines.push(line);
21
- }
30
+ return md ? { text: md[2] ?? '', bold: true } : { text: line, bold: false };
22
31
  }
23
- // Flatten `{heading, lines}` sections into plain rows plus the indices that are
24
- // bold (section headings + markdown ATX headings). Headings carry NO ANSI here —
25
- // the draw step applies bold after fit(), so sanitizeDisplay never sees (and
26
- // never strips) the ESC.
27
- export function flattenSections(sections) {
28
- const lines = [];
29
- const boldRows = new Set();
32
+ /**
33
+ * A heading as an id fragment: `PER USER` `per-user`.
34
+ *
35
+ * Exported because a tab that renders one of its sections as a COLLAPSIBLE entry
36
+ * has to key that entry the same way `sectionRows` keys a flat one — otherwise
37
+ * the same section would carry different node ids depending on which tab reached
38
+ * it, and `patchTree` would rebuild it instead of writing to it (GAIA-340 D4).
39
+ */
40
+ export const sectionKey = (heading) => heading
41
+ .toLowerCase()
42
+ .replace(/[^a-z0-9]+/g, '-')
43
+ .replace(/^-|-$/g, '');
44
+ /**
45
+ * Flatten `{heading, lines}` sections into KEYED rows: the heading, its body,
46
+ * and the blank line that closes the section.
47
+ *
48
+ * The one description of what a section renders as. {@link flattenSections}
49
+ * derives the row surface from it and a converted screen derives its nodes from
50
+ * it, so the two cannot answer differently for the same document — which is the
51
+ * property that lets the Dashboard hand over renderables while its pipe keeps
52
+ * emitting the bytes it always did (spec F5).
53
+ */
54
+ export function sectionRows(sections) {
55
+ const out = [];
30
56
  for (const section of sections) {
31
- boldRows.add(lines.length);
32
- lines.push(section.heading);
33
- for (const line of section.lines)
34
- pushRow(lines, boldRows, line);
35
- lines.push('');
57
+ const id = sectionKey(section.heading);
58
+ out.push({ key: `${id}-heading`, text: section.heading, bold: true });
59
+ section.lines.forEach((line, index) => {
60
+ const key = `${id}-${index}`;
61
+ // Columns are never read as markdown: a run number opens with `#`, and the
62
+ // ATX rule would both bold the row and eat the number.
63
+ if (typeof line !== 'string') {
64
+ out.push({ key, text: lineText(line), bold: false, cells: line.cells });
65
+ return;
66
+ }
67
+ const { text, bold } = markdownRow(line);
68
+ out.push({ key, text, bold });
69
+ });
70
+ out.push({ key: `${id}-gap`, text: '', bold: false });
36
71
  }
37
- return { lines, boldRows };
72
+ return out;
38
73
  }
39
74
  /**
40
- * Flatten expandable entries: one caret header row per entry (`▸` collapsed,
41
- * `▾` expanded) followed by that state's rows. `headerRows[i]` is entry i's
42
- * header row, which is both the highlight target and the click map (the rows are
43
- * NOT uniformly spaced an expanded entry appends its body).
75
+ * One flattened row as the node a converted screen hands over — GAIA-340 D1.
76
+ *
77
+ * The ONE place a `SectionRow` becomes a renderable, so the two screens that
78
+ * render section rows (the dashboard and the ticket detail's tabs) cannot
79
+ * disagree about what a columned row looks like. `prefix` is the screen's own
80
+ * body id, which is what keeps two screens' rows apart in one tree.
81
+ */
82
+ export const sectionRowNode = (prefix, row, selected = false) => row.cells === undefined
83
+ ? textNode({
84
+ id: `${prefix}-${row.key}`,
85
+ content: row.text,
86
+ bold: row.bold,
87
+ selected,
88
+ })
89
+ : cellsRow(`${prefix}-${row.key}`, row.cells, selected);
90
+ // The same rows as plain text plus the INDICES that are bold (section headings +
91
+ // markdown ATX headings) — the shape the row surface needs. Headings carry NO
92
+ // ANSI here: the draw step applies bold after fit(), so sanitizeDisplay never
93
+ // sees (and never strips) the ESC.
94
+ export function flattenSections(sections) {
95
+ const rows = sectionRows(sections);
96
+ return {
97
+ lines: rows.map((row) => row.text),
98
+ boldRows: new Set(rows.flatMap((row, index) => (row.bold ? [index] : []))),
99
+ };
100
+ }
101
+ /**
102
+ * Expandable entries as KEYED rows: one caret header row per entry (`▸`
103
+ * collapsed, `▾` expanded) followed by that state's rows. `headerRows[i]` is
104
+ * entry i's header row, which is both the highlight target and the click map
105
+ * (the rows are NOT uniformly spaced — an expanded entry appends its body).
44
106
  *
45
107
  * Zero entries render `emptyMessage` and nothing selectable, so an empty list is
46
108
  * an explicit statement rather than a blank screen.
109
+ *
110
+ * GAIA-340 D1/D3 — rows rather than lines-plus-a-`boldRows`-set, and the two
111
+ * halves of that are the same change: the caller turns each row into a node, so
112
+ * "this row is a heading" is a prop on the node and no longer an index into a
113
+ * surface somebody else styles. `SectionRow` is the shape {@link sectionRows}
114
+ * already produces, so a tab that mixes flat sections with expandable ones has
115
+ * one row type rather than two.
47
116
  */
48
- export function flattenExpandable(entries, { expanded = new Set(), emptyMessage,
117
+ export function expandableRows(entries, { expanded = new Set(), emptyMessage, emptyKey,
49
118
  // GAIA-255 AC-11 review: `ReadonlySet` because the render path is handed ONE
50
119
  // shared empty set for every not-yet-expanded tab (`expandedFor`) — growing
51
120
  // it here would leak one tab's expansion into all the others. The renderer
@@ -53,22 +122,25 @@ export function flattenExpandable(entries, { expanded = new Set(), emptyMessage,
53
122
  }) {
54
123
  if (!entries.length) {
55
124
  return {
56
- lines: [emptyMessage],
57
- boldRows: new Set(),
125
+ rows: [{ key: emptyKey, text: emptyMessage, bold: false }],
58
126
  headerRows: [],
59
127
  selectable: 0,
60
128
  };
61
129
  }
62
- const lines = [];
63
- const boldRows = new Set();
130
+ const rows = [];
64
131
  const headerRows = [];
65
132
  entries.forEach((entry, i) => {
66
- headerRows.push(lines.length);
67
- boldRows.add(lines.length);
68
- lines.push(`${expanded.has(i) ? '▾' : '▸'} ${entry.header}`);
69
- for (const line of expanded.has(i) ? entry.expanded : entry.collapsed) {
70
- pushRow(lines, boldRows, line);
71
- }
133
+ headerRows.push(rows.length);
134
+ rows.push({
135
+ key: `${entry.key}-header`,
136
+ text: `${expanded.has(i) ? '▾' : '▸'} ${entry.header}`,
137
+ bold: true,
138
+ });
139
+ const body = expanded.has(i) ? entry.expanded : entry.collapsed;
140
+ body.forEach((line, index) => {
141
+ const { text, bold } = markdownRow(line);
142
+ rows.push({ key: `${entry.key}-${index}`, text, bold });
143
+ });
72
144
  });
73
- return { lines, boldRows, headerRows, selectable: entries.length };
145
+ return { rows, headerRows, selectable: entries.length };
74
146
  }
@@ -1,3 +1,4 @@
1
+ import type { SectionLine } from '../section-list/index.js';
1
2
  /** A filled bar cell. U+2588 FULL BLOCK. */
2
3
  export declare const BAR_FULL = "\u2588";
3
4
  /** An empty bar cell. U+2591 LIGHT SHADE. */
@@ -36,14 +37,25 @@ export interface StatsPanelOptions {
36
37
  * components keep their boundary.
37
38
  */
38
39
  columns?: readonly string[] | undefined;
40
+ /**
41
+ * The group's name, printed in the header row's already-empty label field
42
+ * (GAIA-324 D4). A caller that renders several groups under one heading uses
43
+ * it to say which group each block of bars is.
44
+ *
45
+ * The component stays ENTITY-NEUTRAL: this is a string, and nothing here knows
46
+ * a ticket from a run. It declares exactly the data label's budget, so the
47
+ * title, the labels beneath it and the column titles keep one geometry.
48
+ */
49
+ title?: string | undefined;
39
50
  /** What to render instead of rows when there are none (spec R7). */
40
51
  emptyMessage?: string | undefined;
41
52
  }
42
53
  /**
43
- * One block's body rows: an optional column header, then one row per entry.
54
+ * One block's body rows: an optional header row (the group's title, the column
55
+ * titles, or both), then one row per entry.
44
56
  *
45
- * Every column is measured with `displayWidth` (through `pad`/`padNumber`), so a
46
- * wide-character label cannot shift the numbers out of alignment — the failure
47
- * that makes a table of counts unreadable exactly when the data is interesting.
57
+ * Every row declares the SAME cell budgets in the same order, which is what
58
+ * keeps the header's titles over the columns they name — the structural form of
59
+ * the claim the padded rows used to make by measuring each field.
48
60
  */
49
- export declare function statsPanelRows(rows: readonly StatsPanelRow[], { columns, emptyMessage }?: StatsPanelOptions): string[];
61
+ export declare function statsPanelRows(rows: readonly StatsPanelRow[], { columns, title, emptyMessage }?: StatsPanelOptions): SectionLine[];
@@ -1,5 +1,3 @@
1
- import { pad } from '../../lib/format.js';
2
- import { displayWidth } from '../../lib/width.js';
3
1
  // GAIA-256 spec D11 — the Dashboard's `/usage`-style bar rows.
4
2
  //
5
3
  // A SHARED component (the third in `src/Component/`), and it earns that place by
@@ -11,6 +9,14 @@ import { displayWidth } from '../../lib/width.js';
11
9
  // It produces the ROWS OF ONE BLOCK. Stacking blocks under bold headings is
12
10
  // `flattenSections`' job (`Component/section-list`), unchanged: this component
13
11
  // only fills a block's body.
12
+ //
13
+ // GAIA-340 Task 13 — a row is CELLS AND THEIR BUDGETS, not a padded string.
14
+ // `lib/format.ts`'s `pad` used to fill the label field and a local `padNumber`
15
+ // the count columns, both of which had to measure the value in display columns
16
+ // first (`lib/width.ts`). The renderer places a value inside its budget, so the
17
+ // alignment is Yoga's and nothing here measures. The operator approved what that
18
+ // costs: the pipe prints the same values with one space between them and no
19
+ // column alignment, because a pipe runs no layout (spec F5, A4 re-pinned).
14
20
  /** A filled bar cell. U+2588 FULL BLOCK. */
15
21
  export const BAR_FULL = '█';
16
22
  /** An empty bar cell. U+2591 LIGHT SHADE. */
@@ -28,43 +34,25 @@ export const BAR_EMPTY = '░';
28
34
  * ever bites, the fix is an ASCII bar glyph, never a width-table change.
29
35
  */
30
36
  export const BAR_WIDTH = 20;
37
+ /** The indent every row of a block carries, matching the other body renderers. */
38
+ const INDENT = ' ';
31
39
  /**
32
- * The label column, in display columns: 26 for the text plus {@link LABEL_GAP}.
33
- *
34
- * The text budget is wide enough for the longest label the Dashboard actually
35
- * builds: a breakdown row suffixes its entity type (`gaia · tickets`), the
36
- * unattributed bucket's `— unattributed · tickets` is 24 columns and a real
37
- * project name reaches 26 (`gaia-e2e-project · tickets`). At 18 that suffix was
38
- * cut to `— unattributed · t…`, which silently made the two entity types
39
- * indistinguishable in exactly the row where attribution is already unclear.
40
- */
41
- const LABEL_WIDTH = 27;
42
- /**
43
- * Columns of the label field reserved as the gap before the bar.
40
+ * The label column's budget: {@link INDENT} plus 27 columns of text.
44
41
  *
45
- * Load-bearing, not decoration. `pad` fills a label to exactly `LABEL_WIDTH`, so
46
- * a label of precisely that width left NO separator and the row rendered
47
- * `gaia-e2e-project · tickets████████` a real project name, at exactly 26
48
- * columns. A truncated label collided the same way, ending in `…` against the
49
- * bar. Every fixture in the tests was shorter, which is why the suite was green
50
- * while the screen was wrong; the gap is part of the geometry now so the label
51
- * length cannot decide whether it exists.
42
+ * The text budget is what the field has to hold, and after GAIA-324 that is two
43
+ * different kinds of text sharing one column: a breakdown row's bare entity name
44
+ * (a real project name reaches 26 columns on its own `gaia-e2e-project` plus a
45
+ * suffix used to; a long one still does), and the group TITLE printed in the
46
+ * header row's label field above it. Both declare the same budget, which is what
47
+ * keeps a title, the labels it captions and the column titles on one geometry.
52
48
  *
53
- * It is ADDED to the field rather than taken out of it: the 26 columns above are
54
- * a content budget with a stated reason, and spending one of them on the gap
55
- * would cut `gaia-e2e-project · tickets` to `gaia-e2e-project · ticke…` losing
56
- * exactly the entity-type suffix that tells the two rows of a project apart.
49
+ * The indent rides INSIDE the cell's text rather than beside it as a cell of its
50
+ * own, so a row's first column starts where the other body renderers' rows do
51
+ * and the row's own `gap` is not spent separating an empty cell from the label.
57
52
  */
58
- const LABEL_GAP = 1;
59
- /** One number column, in display columns. */
53
+ const LABEL_WIDTH = INDENT.length + 27;
54
+ /** One number column's budget, in display columns. */
60
55
  const NUMBER_WIDTH = 8;
61
- /** The indent every row of a block carries, matching the other body renderers. */
62
- const INDENT = ' ';
63
- /** `value` right-aligned to `width` DISPLAY columns — the number columns' rule. */
64
- function padNumber(value, width) {
65
- const gap = Math.max(0, width - displayWidth(value));
66
- return ' '.repeat(gap) + value;
67
- }
68
56
  /**
69
57
  * The bar for `value` within a group whose largest bar value is `max`.
70
58
  *
@@ -81,34 +69,44 @@ function bar(value, max) {
81
69
  return BAR_FULL.repeat(filled) + BAR_EMPTY.repeat(BAR_WIDTH - filled);
82
70
  }
83
71
  /**
84
- * One block's body rows: an optional column header, then one row per entry.
72
+ * One block's body rows: an optional header row (the group's title, the column
73
+ * titles, or both), then one row per entry.
85
74
  *
86
- * Every column is measured with `displayWidth` (through `pad`/`padNumber`), so a
87
- * wide-character label cannot shift the numbers out of alignment — the failure
88
- * that makes a table of counts unreadable exactly when the data is interesting.
75
+ * Every row declares the SAME cell budgets in the same order, which is what
76
+ * keeps the header's titles over the columns they name — the structural form of
77
+ * the claim the padded rows used to make by measuring each field.
89
78
  */
90
- export function statsPanelRows(rows, { columns, emptyMessage } = {}) {
79
+ export function statsPanelRows(rows, { columns, title, emptyMessage } = {}) {
91
80
  if (rows.length === 0) {
92
81
  return emptyMessage === undefined ? [] : [`${INDENT}${emptyMessage}`];
93
82
  }
94
83
  const barValue = (row) => row.numbers.at(-1) ?? 0;
95
84
  const max = Math.max(...rows.map(barValue));
85
+ const number = (text) => ({
86
+ text,
87
+ width: NUMBER_WIDTH,
88
+ right: true,
89
+ });
96
90
  const out = [];
97
- if (columns && columns.length > 0) {
98
- out.push(INDENT +
99
- pad('', LABEL_WIDTH) +
100
- ' '.repeat(BAR_WIDTH) +
101
- columns.map((c) => padNumber(c, NUMBER_WIDTH)).join(''));
91
+ if ((columns && columns.length > 0) || title !== undefined) {
92
+ out.push({
93
+ cells: [
94
+ { text: `${INDENT}${title ?? ''}`, width: LABEL_WIDTH },
95
+ // The bar's column, empty: the header row has no bar, and the cell is
96
+ // what holds the column titles over the numbers rather than over it.
97
+ { text: '', width: BAR_WIDTH },
98
+ ...(columns ?? []).map(number),
99
+ ],
100
+ });
102
101
  }
103
102
  for (const row of rows) {
104
- out.push(INDENT +
105
- // Padded to the field width MINUS the gap, then the gap: the label
106
- // column stays `LABEL_WIDTH` wide either way, so the header row above
107
- // and every bar below still line up.
108
- pad(row.label, LABEL_WIDTH - LABEL_GAP) +
109
- ' '.repeat(LABEL_GAP) +
110
- bar(barValue(row), max) +
111
- row.numbers.map((n) => padNumber(String(n), NUMBER_WIDTH)).join(''));
103
+ out.push({
104
+ cells: [
105
+ { text: `${INDENT}${row.label}`, width: LABEL_WIDTH },
106
+ { text: bar(barValue(row), max), width: BAR_WIDTH },
107
+ ...row.numbers.map((n) => number(String(n))),
108
+ ],
109
+ });
112
110
  }
113
111
  return out;
114
112
  }