@gaia-ai/addon-gaia-ui 0.9.2 → 0.10.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 (119) hide show
  1. package/dist/src/Component/footer-actions/index.d.ts +58 -0
  2. package/dist/src/Component/footer-actions/index.js +111 -0
  3. package/dist/src/Component/list-chrome/index.d.ts +19 -0
  4. package/dist/src/Component/list-chrome/index.js +61 -0
  5. package/dist/src/Component/markdown/index.d.ts +68 -0
  6. package/dist/src/Component/markdown/index.js +133 -0
  7. package/dist/src/Component/section-list/index.d.ts +115 -23
  8. package/dist/src/Component/section-list/index.js +120 -34
  9. package/dist/src/Component/top-tabs/index.d.ts +65 -6
  10. package/dist/src/Component/top-tabs/index.js +29 -6
  11. package/dist/src/Entity/collection.js +1 -1
  12. package/dist/src/Entity/conductor/Screen/conductors.d.ts +26 -41
  13. package/dist/src/Entity/conductor/Screen/conductors.js +65 -123
  14. package/dist/src/Entity/project/Screen/project-picker.d.ts +20 -31
  15. package/dist/src/Entity/project/Screen/project-picker.js +48 -58
  16. package/dist/src/Entity/project/Screen/projects-screen.d.ts +4 -2
  17. package/dist/src/Entity/project/Screen/projects-screen.js +11 -8
  18. package/dist/src/Entity/run/Component/run-detail/index.js +15 -7
  19. package/dist/src/Entity/run/Data/query.d.ts +10 -0
  20. package/dist/src/Entity/run/Data/query.js +16 -1
  21. package/dist/src/Entity/run/Data/runs.d.ts +2 -0
  22. package/dist/src/Entity/run/Data/runs.js +19 -1
  23. package/dist/src/Entity/run/Screen/run-detail.d.ts +16 -23
  24. package/dist/src/Entity/run/Screen/run-detail.js +27 -34
  25. package/dist/src/Entity/run/Screen/runs.d.ts +7 -3
  26. package/dist/src/Entity/run/Screen/runs.js +31 -48
  27. package/dist/src/Entity/run/Screen/user-runs.d.ts +79 -0
  28. package/dist/src/Entity/run/Screen/user-runs.js +98 -0
  29. package/dist/src/Entity/statistics/Data/statistics.d.ts +31 -6
  30. package/dist/src/Entity/statistics/Data/statistics.js +39 -6
  31. package/dist/src/Entity/statistics/Screen/dashboard.d.ts +10 -2
  32. package/dist/src/Entity/statistics/Screen/dashboard.js +111 -34
  33. package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +14 -1
  34. package/dist/src/Entity/ticket/Component/comment-item/index.js +15 -1
  35. package/dist/src/Entity/ticket/Component/sections/index.js +16 -1
  36. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +102 -13
  37. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +440 -72
  38. package/dist/src/Entity/ticket/Data/detail.d.ts +14 -0
  39. package/dist/src/Entity/ticket/Data/detail.js +15 -1
  40. package/dist/src/Entity/ticket/Data/query.d.ts +7 -0
  41. package/dist/src/Entity/ticket/Data/query.js +18 -2
  42. package/dist/src/Entity/ticket/Data/tickets.d.ts +24 -0
  43. package/dist/src/Entity/ticket/Data/tickets.js +41 -1
  44. package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +10 -4
  45. package/dist/src/Entity/ticket/Screen/create-ticket-form.js +37 -16
  46. package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +6 -4
  47. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +16 -15
  48. package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +6 -4
  49. package/dist/src/Entity/ticket/Screen/parent-filter-form.js +14 -11
  50. package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +5 -2
  51. package/dist/src/Entity/ticket/Screen/ticket-agents.js +14 -14
  52. package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +89 -4
  53. package/dist/src/Entity/ticket/Screen/ticket-detail.js +295 -74
  54. package/dist/src/Entity/ticket/Screen/tickets.d.ts +66 -3
  55. package/dist/src/Entity/ticket/Screen/tickets.js +140 -76
  56. package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +91 -0
  57. package/dist/src/Entity/ticket/Screen/user-tickets.js +94 -0
  58. package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +6 -4
  59. package/dist/src/Entity/ticket/Screen/who-filter-form.js +14 -11
  60. package/dist/src/Entity/user/Data/users.d.ts +49 -0
  61. package/dist/src/Entity/user/Data/users.js +46 -0
  62. package/dist/src/Entity/user/Screen/user-overview.d.ts +33 -0
  63. package/dist/src/Entity/user/Screen/user-overview.js +159 -0
  64. package/dist/src/Entity/user/Screen/users.d.ts +39 -0
  65. package/dist/src/Entity/user/Screen/users.js +136 -0
  66. package/dist/src/Extension/contract.d.ts +58 -0
  67. package/dist/src/Extension/contract.js +1 -0
  68. package/dist/src/Extension/load.d.ts +10 -0
  69. package/dist/src/Extension/load.js +21 -0
  70. package/dist/src/Extension/store.d.ts +11 -0
  71. package/dist/src/Extension/store.js +44 -0
  72. package/dist/src/Kernel/chrome.d.ts +49 -12
  73. package/dist/src/Kernel/chrome.js +150 -57
  74. package/dist/src/Kernel/key-input.js +1 -1
  75. package/dist/src/Kernel/mount.d.ts +1 -1
  76. package/dist/src/Kernel/mount.js +1 -1
  77. package/dist/src/Kernel/regions.d.ts +64 -0
  78. package/dist/src/Kernel/regions.js +328 -0
  79. package/dist/src/Kernel/renderer.d.ts +25 -0
  80. package/dist/src/Kernel/renderer.js +142 -0
  81. package/dist/src/Kernel/splash.d.ts +27 -0
  82. package/dist/src/Kernel/splash.js +72 -0
  83. package/dist/src/Kernel/tui-kernel.js +139 -434
  84. package/dist/src/Routing/context.d.ts +5 -0
  85. package/dist/src/Routing/context.js +6 -0
  86. package/dist/src/Routing/registry.js +4 -2
  87. package/dist/src/Routing/router.js +5 -1
  88. package/dist/src/Routing/routes.js +8 -0
  89. package/dist/src/Screen/async-modal-screen.d.ts +26 -0
  90. package/dist/src/Screen/async-modal-screen.js +44 -0
  91. package/dist/src/Screen/async-screen.d.ts +32 -40
  92. package/dist/src/Screen/async-screen.js +45 -41
  93. package/dist/src/Screen/full-screen.d.ts +1 -0
  94. package/dist/src/Screen/full-screen.js +3 -0
  95. package/dist/src/Screen/list-screen.d.ts +7 -1
  96. package/dist/src/Screen/list-screen.js +17 -6
  97. package/dist/src/Screen/modal-screen.d.ts +6 -0
  98. package/dist/src/Screen/modal-screen.js +2 -0
  99. package/dist/src/Screen/screen.d.ts +42 -6
  100. package/dist/src/Screen/screen.js +2 -2
  101. package/dist/src/Widget/textarea.js +10 -14
  102. package/dist/src/Widget/types.d.ts +1 -1
  103. package/dist/src/launcher.d.ts +6 -0
  104. package/dist/src/launcher.js +2 -0
  105. package/dist/src/lib/format.js +1 -1
  106. package/dist/src/lib/markdown.d.ts +53 -0
  107. package/dist/src/lib/markdown.js +522 -0
  108. package/dist/src/lib/refresh-signal.d.ts +56 -0
  109. package/dist/src/lib/refresh-signal.js +85 -0
  110. package/dist/src/lib/theme.d.ts +36 -0
  111. package/dist/src/lib/theme.js +40 -0
  112. package/dist/src/lib/version.d.ts +5 -0
  113. package/dist/src/lib/version.js +46 -0
  114. package/dist/src/lib/vnode.d.ts +86 -63
  115. package/dist/src/lib/vnode.js +112 -85
  116. package/dist/src/types.d.ts +32 -0
  117. package/dist/src/ui.d.ts +2 -0
  118. package/dist/src/ui.js +1 -0
  119. package/package.json +4 -3
@@ -1,4 +1,7 @@
1
- import { cellsRow, textNode } from '../../lib/vnode.js';
1
+ import { cellsRow, textNode, } from '../../lib/vnode.js';
2
+ import { plainLine } from '../markdown/index.js';
3
+ /** Whether a line is columns rather than prose — `cells` is the discriminant. */
4
+ const isCells = (line) => typeof line !== 'string' && 'cells' in line;
2
5
  /**
3
6
  * A line as the text a surface with no layout prints.
4
7
  *
@@ -6,29 +9,58 @@ import { cellsRow, textNode } from '../../lib/vnode.js';
6
9
  * puts between two cells: the pipe and the tree then say the same thing about
7
10
  * what separates two columns, and only the alignment inside a column is the
8
11
  * renderer's own.
12
+ *
13
+ * GAIA-372 D4 — for a {@link StyledLine} this returns the RENDERED text, which is the
14
+ * whole of how parity is achieved: `SectionRow.text` carries it and the pipe
15
+ * prints exactly that, so `gaia ui <id> | cat` and the interactive tree are the
16
+ * same rendering by construction. The tree merely adds the attributes a pipe
17
+ * cannot carry. There is no raw-only pipe path left to disagree (AC-3).
9
18
  */
10
- export const lineText = (line) => typeof line === 'string'
11
- ? line
12
- : line.cells
13
- .map((cell) => cell.text)
14
- .join(' ')
15
- .replace(/\s+$/, '');
19
+ export const lineText = (line) => {
20
+ if (typeof line === 'string')
21
+ return line;
22
+ if (isCells(line)) {
23
+ return line.cells
24
+ .map((cell) => cell.text)
25
+ .join(' ')
26
+ .replace(/\s+$/, '');
27
+ }
28
+ return line.text;
29
+ };
16
30
  /**
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).
31
+ * Any body line as a RENDERED line — GAIA-372 D3/D8.
20
32
  *
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.
33
+ * A plain or columned row is LIFTED (one unparsed span) rather than refused,
34
+ * because a consumer that needs rendered lines — the collapsible Description
35
+ * entry, whose extract reads `line.heading` — is handed a `Description` section
36
+ * whose empty state is the plain string `No description.` Lifting never parses,
37
+ * so nothing here can re-introduce a Markdown rule on the plain path.
27
38
  */
28
- export function markdownRow(line) {
29
- const md = /^(#{1,6})\s+(.*)$/.exec(line);
30
- return md ? { text: md[2] ?? '', bold: true } : { text: line, bold: false };
31
- }
39
+ export const styledOf = (line) => typeof line === 'string' || isCells(line) ? plainLine(lineText(line)) : line;
40
+ /**
41
+ * One body line as the row it is — the ONE place D3's three variants are read
42
+ * apart, so a flat section and a collapsible entry cannot answer differently for
43
+ * the same line.
44
+ */
45
+ const bodyRow = (key, line) => {
46
+ // Columns are never read as Markdown: a run number opens with `#`, and the ATX
47
+ // rule would both bold the row and eat the number.
48
+ if (isCells(line)) {
49
+ return { key, text: lineText(line), bold: false, cells: line.cells };
50
+ }
51
+ // A plain string is PLAIN. Nothing parses it (D3) — which is what puts
52
+ // `Assignment`, `Branch`, `Links` and `Labels` structurally out of reach.
53
+ if (typeof line === 'string')
54
+ return { key, text: line, bold: false };
55
+ return {
56
+ key,
57
+ text: line.text,
58
+ // An ATX heading inside a body is the ROW's weight, exactly as it was; what
59
+ // decides it is now the rendered line's own flag rather than a re-match.
60
+ bold: line.heading,
61
+ runs: line.runs,
62
+ };
63
+ };
32
64
  /**
33
65
  * A heading as an id fragment: `PER USER` → `per-user`.
34
66
  *
@@ -41,6 +73,57 @@ export const sectionKey = (heading) => heading
41
73
  .toLowerCase()
42
74
  .replace(/[^a-z0-9]+/g, '-')
43
75
  .replace(/^-|-$/g, '');
76
+ /**
77
+ * The same, for a heading that is DATA rather than one of the built-in set —
78
+ * GAIA-373 AC-10.
79
+ *
80
+ * {@link sectionKey} is LOSSY: it lowercases and collapses every run of
81
+ * non-alphanumeric characters onto one `-`. That is injective over the closed
82
+ * built-in headings (`Per user`, `Latest summary`), and it stops being injective
83
+ * the moment a contributed panel heads its sections with whatever it is
84
+ * listing. The shipped artifacts viewer heads them with paths under the ticket's
85
+ * tree, so `/`, `.` and `-` all land on the same character and two ordinary
86
+ * captures collapse onto one key:
87
+ *
88
+ * playwright/logs/walk.txt -> playwright-logs-walk-txt
89
+ * playwright/logs-walk.txt -> playwright-logs-walk-txt
90
+ *
91
+ * A key is an IDENTITY — `Kernel/mount.ts` refuses a repeated id, and it refuses
92
+ * it while MOUNTING, past the host's guard around `panel.sections()` and on a
93
+ * draw path with no `catch` — so a lossy key is not a cosmetic collision, it is
94
+ * the cockpit going down on a message naming neither the addon nor the file.
95
+ *
96
+ * SO THE SEPARATORS ARE ENCODED RATHER THAN COLLAPSED. Every UTF-8 byte outside
97
+ * `[a-z0-9]` becomes `_<hex>`, two digits, lower case. The escape has a fixed
98
+ * length and `_` is itself escaped (`_5f`), so the mapping is reversible and
99
+ * therefore injective — including over case (`README.txt` and `readme.txt` are
100
+ * two files on this filesystem) and over a heading that already looks like an
101
+ * escape.
102
+ *
103
+ * WHY THE OUTPUT CARRIES NO `-`, which is a second property and not a side
104
+ * effect. A key never becomes an id on its own: it is always suffixed
105
+ * (`-heading`, `-gap`, `-empty`, `-header`, `-<index>`). Injectivity of the key
106
+ * alone would not settle the id, because the suffix boundary would be
107
+ * ambiguous — with `-` allowed inside a key, section `a` at body index 12 and
108
+ * section `a-1` at body index 2 would both be `a-12`. With no `-` inside a key
109
+ * the first `-` after the screen's own prefix always ends the key, so a key and
110
+ * its suffix are both recovered from an id and two ids can only be equal when
111
+ * both halves are. Within one section the suffixes are distinct by construction:
112
+ * the indices are digits and the four literals are not.
113
+ *
114
+ * NOT THE BUILT-IN PATH. `sectionRows` and the ticket detail's tabs keep
115
+ * {@link sectionKey} as their default, so no built-in node id moves (AC-2) — a
116
+ * `_` in a key is exactly the mark of a heading that came from a contribution.
117
+ *
118
+ * WHAT THIS STILL DOES NOT FIX, said plainly: two panel sections carrying the
119
+ * SAME heading. No injective map over headings can separate two identical
120
+ * headings, so that case remains the loud refusal it is today. It is out of
121
+ * scope for GAIA-373 and belongs to whatever gives a panel section an identity
122
+ * distinct from its title.
123
+ */
124
+ export const panelSectionKey = (heading) => Array.from(new TextEncoder().encode(heading), (byte) => (byte >= 0x30 && byte <= 0x39) || (byte >= 0x61 && byte <= 0x7a)
125
+ ? String.fromCharCode(byte)
126
+ : `_${byte.toString(16).padStart(2, '0')}`).join('');
44
127
  /**
45
128
  * Flatten `{heading, lines}` sections into KEYED rows: the heading, its body,
46
129
  * and the blank line that closes the section.
@@ -51,21 +134,18 @@ export const sectionKey = (heading) => heading
51
134
  * property that lets the Dashboard hand over renderables while its pipe keeps
52
135
  * emitting the bytes it always did (spec F5).
53
136
  */
54
- export function sectionRows(sections) {
137
+ export function sectionRows(sections,
138
+ // GAIA-373 — HOW a heading becomes a key, as an argument. The built-in
139
+ // derivation stays the default so every existing caller keys exactly as it
140
+ // did (AC-2); a caller whose headings are data passes
141
+ // {@link panelSectionKey}, which is injective over them.
142
+ keyOf = sectionKey) {
55
143
  const out = [];
56
144
  for (const section of sections) {
57
- const id = sectionKey(section.heading);
145
+ const id = keyOf(section.heading);
58
146
  out.push({ key: `${id}-heading`, text: section.heading, bold: true });
59
147
  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 });
148
+ out.push(bodyRow(`${id}-${index}`, line));
69
149
  });
70
150
  out.push({ key: `${id}-gap`, text: '', bold: false });
71
151
  }
@@ -85,10 +165,17 @@ export const sectionRowNode = (prefix, row, selected = false) => row.cells === u
85
165
  content: row.text,
86
166
  bold: row.bold,
87
167
  selected,
168
+ // GAIA-372 D5 — spread rather than passed as `undefined`, for
169
+ // `exactOptionalPropertyTypes`: a row with no runs declares none, and
170
+ // `linked()` then returns the plain string it always did.
171
+ ...(row.runs === undefined ? {} : { runs: row.runs }),
88
172
  })
89
173
  : cellsRow(`${prefix}-${row.key}`, row.cells, selected);
90
174
  // 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
175
+ // a rendered body's own ATX headings) — the shape the row surface needs. The
176
+ // RUNS are dropped on this path by definition: a pipe carries no styling, and
177
+ // `row.text` is already the rendered text, so what it prints is what the tree
178
+ // draws minus the attributes (GAIA-372 D4). Headings carry NO
92
179
  // ANSI here: the draw step applies bold after fit(), so sanitizeDisplay never
93
180
  // sees (and never strips) the ESC.
94
181
  export function flattenSections(sections) {
@@ -138,8 +225,7 @@ export function expandableRows(entries, { expanded = new Set(), emptyMessage, em
138
225
  });
139
226
  const body = expanded.has(i) ? entry.expanded : entry.collapsed;
140
227
  body.forEach((line, index) => {
141
- const { text, bold } = markdownRow(line);
142
- rows.push({ key: `${entry.key}-${index}`, text, bold });
228
+ rows.push(bodyRow(`${entry.key}-${index}`, line));
143
229
  });
144
230
  });
145
231
  return { rows, headerRows, selectable: entries.length };
@@ -13,6 +13,9 @@ export declare const ROOT_TABS: readonly [{
13
13
  }, {
14
14
  readonly id: "conductors";
15
15
  readonly label: "Conductors";
16
+ }, {
17
+ readonly id: "users";
18
+ readonly label: "Users";
16
19
  }];
17
20
  /** The project level: the tabs of an opened project. */
18
21
  export declare const PROJECT_TABS: readonly [{
@@ -62,6 +65,22 @@ export declare const TICKET_TABS: readonly [{
62
65
  readonly id: "runs";
63
66
  readonly label: "Runs";
64
67
  }];
68
+ /**
69
+ * The user level: Overview · Tickets · Runs for one person (GAIA-377 D1).
70
+ *
71
+ * Destinations, not modes — each tab is its own screen in the slice that owns
72
+ * its query (D2), so Tab runs a real `Router.navigate` and preserves `userId`.
73
+ */
74
+ export declare const USER_TABS: readonly [{
75
+ readonly id: "overview";
76
+ readonly label: "Overview";
77
+ }, {
78
+ readonly id: "tickets";
79
+ readonly label: "Tickets";
80
+ }, {
81
+ readonly id: "runs";
82
+ readonly label: "Runs";
83
+ }];
65
84
  /** The tab bars, keyed by level — the one place a level is named. */
66
85
  export declare const TAB_LEVELS: {
67
86
  readonly root: readonly [{
@@ -73,6 +92,9 @@ export declare const TAB_LEVELS: {
73
92
  }, {
74
93
  readonly id: "conductors";
75
94
  readonly label: "Conductors";
95
+ }, {
96
+ readonly id: "users";
97
+ readonly label: "Users";
76
98
  }];
77
99
  readonly project: readonly [{
78
100
  readonly id: "dashboard";
@@ -100,6 +122,16 @@ export declare const TAB_LEVELS: {
100
122
  readonly id: "runs";
101
123
  readonly label: "Runs";
102
124
  }];
125
+ readonly user: readonly [{
126
+ readonly id: "overview";
127
+ readonly label: "Overview";
128
+ }, {
129
+ readonly id: "tickets";
130
+ readonly label: "Tickets";
131
+ }, {
132
+ readonly id: "runs";
133
+ readonly label: "Runs";
134
+ }];
103
135
  };
104
136
  /** The name of a level that carries a top-level tab bar. */
105
137
  export type TabLevel = keyof typeof TAB_LEVELS;
@@ -117,13 +149,34 @@ export type TopTabId = {
117
149
  * Where a screen sits in the navigation: which bar it draws, and which tab of
118
150
  * that bar is its own. A discriminated union over the levels, so a location can
119
151
  * never pair one level's name with another level's tab id.
152
+ *
153
+ * GAIA-373 D5/D6 — the `ticket` arm is the one that is NOT a literal union, and
154
+ * it is also the one that carries an optional bar of its own. Both halves have
155
+ * the same cause: the ticket level is not a DESTINATION level.
156
+ * `router.navigateTab` is never reached for it, because `selectTopTab` claims
157
+ * every ticket tab (GAIA-346 D4), so the literal union that protects root/project
158
+ * NAVIGATION has no target here — while a contributed section mode
159
+ * (`gaia/artifacts`) is data from the contribution store and cannot be a literal
160
+ * in this file at all. Root and project keep theirs intact, so
161
+ * `topTabBar('root', 'runs')` is still a TS2345.
162
+ *
163
+ * `tabs` is read by BOTH bar builders below and by the kernel's Tab cycle, so
164
+ * the bar drawn, the bar clicked and the bar cycled are three readings of one
165
+ * field rather than three answers.
120
166
  */
121
167
  export type TabLocation = {
122
- [L in TabLevel]: {
123
- level: L;
124
- tab: TabIdOf<L>;
125
- };
126
- }[TabLevel];
168
+ level: 'root';
169
+ tab: TabIdOf<'root'>;
170
+ } | {
171
+ level: 'project';
172
+ tab: TabIdOf<'project'>;
173
+ } | {
174
+ level: 'ticket';
175
+ tab: string;
176
+ tabs?: readonly TopTab[];
177
+ };
178
+ /** The entries a location draws: its own, else its level's registry. */
179
+ export declare const tabsOfLocation: (location: TabLocation) => readonly TopTab[];
127
180
  /**
128
181
  * The bar as the PIECES it is drawn from: one part per tab, carrying that tab's
129
182
  * id, with the joins between them carrying none.
@@ -191,5 +244,11 @@ export declare function topTabParts<L extends TabLevel>(level: L, activeId: TabI
191
244
  * companion of {@link tabBarFor}, and the pairing it needs was already checked
192
245
  * when the location was built. Keeping the two in step is what makes "the bar you
193
246
  * see is the bar you click" true by construction: both read `location`.
247
+ *
248
+ * GAIA-373 — `TabBarPart<string>` rather than the `TopTabId` default, and that
249
+ * is an honest widening rather than a lost check: a location may carry a bar of
250
+ * its own now, whose entries are CONTRIBUTED (`gaia/artifacts`) and by
251
+ * construction outside every union derived from `TAB_LEVELS`. The level-typed
252
+ * {@link topTabParts} keeps the narrow ids for callers that name a level.
194
253
  */
195
- export declare function tabPartsFor(location: TabLocation): TabBarPart[];
254
+ export declare function tabPartsFor(location: TabLocation): TabBarPart<string>[];
@@ -29,10 +29,9 @@
29
29
  // level — the bar you Tab through or click IS the current level's bar — so it
30
30
  // pairs the target with `here.level` directly and `locateTab` is deleted.
31
31
  //
32
- // Only tabs that are actually navigable are listed. A planned screen (`Users`)
33
- // arrives with its own ticket and adds its entry here then — a tab drawn before
34
- // its screen exists would advertise navigation the operator cannot take, which
35
- // is the very defect GAIA-239 removes.
32
+ // Only tabs that are actually navigable are listed. GAIA-377 added `Users` with
33
+ // its screens; a tab drawn before its screen exists would advertise navigation
34
+ // the operator cannot take, which is the very defect GAIA-239 removes.
36
35
  //
37
36
  // GAIA-232 added `conductors`, and put it on the ROOT level although the note
38
37
  // above once reserved it for the project level. A conductor is an operator
@@ -53,6 +52,8 @@ export const ROOT_TABS = [
53
52
  { id: 'dashboard', label: 'Dashboard' },
54
53
  { id: 'projects', label: 'Projects' },
55
54
  { id: 'conductors', label: 'Conductors' },
55
+ // GAIA-377 D1 — arrives with its screens; see USER_TABS.
56
+ { id: 'users', label: 'Users' },
56
57
  ];
57
58
  /** The project level: the tabs of an opened project. */
58
59
  export const PROJECT_TABS = [
@@ -88,12 +89,28 @@ export const TICKET_TABS = [
88
89
  { id: 'related', label: 'Related' },
89
90
  { id: 'runs', label: 'Runs' },
90
91
  ];
92
+ /**
93
+ * The user level: Overview · Tickets · Runs for one person (GAIA-377 D1).
94
+ *
95
+ * Destinations, not modes — each tab is its own screen in the slice that owns
96
+ * its query (D2), so Tab runs a real `Router.navigate` and preserves `userId`.
97
+ */
98
+ export const USER_TABS = [
99
+ { id: 'overview', label: 'Overview' },
100
+ { id: 'tickets', label: 'Tickets' },
101
+ { id: 'runs', label: 'Runs' },
102
+ ];
91
103
  /** The tab bars, keyed by level — the one place a level is named. */
92
104
  export const TAB_LEVELS = {
93
105
  root: ROOT_TABS,
94
106
  project: PROJECT_TABS,
95
107
  ticket: TICKET_TABS,
108
+ user: USER_TABS,
96
109
  };
110
+ /** The entries a location draws: its own, else its level's registry. */
111
+ export const tabsOfLocation = (location) => 'tabs' in location && location.tabs
112
+ ? location.tabs
113
+ : TAB_LEVELS[location.level];
97
114
  /** What joins two tabs in the rendered bar — part of neither tab's span. */
98
115
  const SEPARATOR = ' ';
99
116
  /** One tab's rendered segment: bracketed when active, space-padded otherwise. */
@@ -133,7 +150,7 @@ export function topTabBar(level, activeId) {
133
150
  * not ask the compiler to correlate two independently narrowed properties.
134
151
  */
135
152
  export function tabBarFor(location) {
136
- return renderBar(TAB_LEVELS[location.level], location.tab);
153
+ return renderBar(tabsOfLocation(location), location.tab);
137
154
  }
138
155
  // GAIA-256 spec D2 — `locateTab` USED TO LIVE HERE and is deliberately gone.
139
156
  //
@@ -168,7 +185,13 @@ export function topTabParts(level, activeId) {
168
185
  * companion of {@link tabBarFor}, and the pairing it needs was already checked
169
186
  * when the location was built. Keeping the two in step is what makes "the bar you
170
187
  * see is the bar you click" true by construction: both read `location`.
188
+ *
189
+ * GAIA-373 — `TabBarPart<string>` rather than the `TopTabId` default, and that
190
+ * is an honest widening rather than a lost check: a location may carry a bar of
191
+ * its own now, whose entries are CONTRIBUTED (`gaia/artifacts`) and by
192
+ * construction outside every union derived from `TAB_LEVELS`. The level-typed
193
+ * {@link topTabParts} keeps the narrow ids for callers that name a level.
171
194
  */
172
195
  export function tabPartsFor(location) {
173
- return renderParts(TAB_LEVELS[location.level], location.tab);
196
+ return renderParts(tabsOfLocation(location), location.tab);
174
197
  }
@@ -50,7 +50,7 @@ function readTotal(doc) {
50
50
  * back is `pad`'s reason for existing: `cellFill` fills to a width the COLUMN
51
51
  * DECLARES, never to one measured off the page.
52
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`)
53
+ * A2 already being reported as missed (`docs/evidence/GAIA-340/teardown-a2.txt`, in git history since GAIA-371)
54
54
  * — the module survives this ticket, so the alternative is not "no measurement"
55
55
  * but "an illegible cut", and R5's rule against re-implementing it under another
56
56
  * name is honoured by calling it rather than copying it.
@@ -1,5 +1,21 @@
1
- import { BaseScreen, type ScreenContext } from '../../../Screen/screen.js';
2
- export declare class ConductorsScreen extends BaseScreen {
1
+ import type { VNode } from '@opentui/core';
2
+ import { AsyncScreen } from '../../../Screen/async-screen.js';
3
+ import type { FullScreenChrome } from '../../../Screen/full-screen.js';
4
+ import type { ScreenContext } from '../../../Screen/screen.js';
5
+ import { type ConductorRow } from '../Data/conductors.js';
6
+ /** Loaded conductors page — rows plus project-name lookup for the table. */
7
+ interface ConductorsData {
8
+ readonly rows: readonly ConductorRow[];
9
+ readonly projectNames: ReadonlyMap<string, string>;
10
+ }
11
+ /**
12
+ * Conductors list — a normal {@link AsyncScreen}.
13
+ *
14
+ * Refresh is the shared mechanism (GAIA-376 `ui-refresh` file + `R`), not a
15
+ * screen-private timer. Enter loads; navigation away cancels in-flight reads
16
+ * via the async lifecycle.
17
+ */
18
+ export declare class ConductorsScreen extends AsyncScreen<ConductorsData> {
3
19
  static readonly route: {
4
20
  id: string;
5
21
  path: string;
@@ -12,56 +28,25 @@ export declare class ConductorsScreen extends BaseScreen {
12
28
  };
13
29
  readonly id: string;
14
30
  readonly frameId = "conductors";
15
- private rows;
16
- private projectNames;
17
31
  private selectedIndex;
18
32
  private logFor;
19
- private status;
20
- private reading;
21
- private timer;
22
33
  private readonly busyIds;
23
- private activation;
24
34
  topTab(): {
25
35
  readonly level: "root";
26
36
  readonly tab: "conductors";
27
37
  };
28
- private quietRead;
29
- get busy(): boolean;
30
38
  reload(context: ScreenContext): void;
31
- enter(context: ScreenContext): void;
32
- leave(): void;
33
- /**
34
- * GAIA-340 D1 — the list as the renderable tree the screen hands over, and
35
- * since GAIA-345 that tree is the SHARED table's (AC 1/2).
36
- *
37
- * There is no `conductor-list` component any more: the eight columns, the
38
- * noun and the empty message are `CONDUCTOR_TABLE`, declared beside the row
39
- * mapper that produces the rows. What this screen still owns is the CONTEXT
40
- * the columns read — the frame's clock, which conductors have an action in
41
- * flight, and the project names its own small read resolved — because none of
42
- * those is a property of a `gaia_conductor` record.
43
- *
44
- * THE OPEN LOG IS NOT A ROW OF THE LIST, so it is not in the table: it is
45
- * neither keyed on a conductor nor laid out in the columns, and a log line
46
- * keyed under the list's ids could collide with a row's. It stays its own
47
- * block below the table, whose lines are keyed by position — honest for a
48
- * block replaced wholesale on every read that indexes nothing outside itself.
49
- * The wrapper is stated on EVERY frame, log or no log, so the body's root
50
- * node keeps one identity across the keypress that opens one.
51
- *
52
- * MOUNTED WHOLE, so the bound on its nodes is the data's own: one box of 8
53
- * cells per conductor, over the conductors of one user on one control plane,
54
- * plus at most 40 log lines. `@opentui/core@0.5.1` hands out 16 382 before it
55
- * throws, so this screen windows nothing of its own — deliberately, not by
56
- * omission.
57
- */
58
- private surface;
59
- present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
39
+ get rows(): readonly ConductorRow[];
40
+ protected load(context: ScreenContext): Promise<ConductorsData>;
41
+ protected renderLoading(): string[];
42
+ protected renderReady(data: ConductorsData, context: ScreenContext): VNode;
43
+ protected didLoad(data: ConductorsData): void;
44
+ protected chrome(_context: ScreenContext): FullScreenChrome;
45
+ protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
60
46
  select(index: number, context: ScreenContext): boolean;
61
47
  activate(index: number, context: ScreenContext): boolean;
62
48
  handleInput(input: string, context: ScreenContext): boolean;
63
49
  reset(): void;
64
- private refresh;
65
50
  private act;
66
- private stopTimer;
67
51
  }
52
+ export {};