@gaia-ai/addon-gaia-ui 0.9.2 → 0.11.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 (136) 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 +110 -7
  10. package/dist/src/Component/top-tabs/index.js +88 -9
  11. package/dist/src/Entity/collection.js +1 -1
  12. package/dist/src/Entity/conductor/Data/conductors.d.ts +4 -3
  13. package/dist/src/Entity/conductor/Data/conductors.js +76 -10
  14. package/dist/src/Entity/conductor/Data/own-conductor.js +7 -1
  15. package/dist/src/Entity/conductor/Screen/conductors.d.ts +26 -41
  16. package/dist/src/Entity/conductor/Screen/conductors.js +65 -123
  17. package/dist/src/Entity/project/Screen/project-picker.d.ts +20 -31
  18. package/dist/src/Entity/project/Screen/project-picker.js +48 -58
  19. package/dist/src/Entity/project/Screen/projects-screen.d.ts +4 -2
  20. package/dist/src/Entity/project/Screen/projects-screen.js +11 -8
  21. package/dist/src/Entity/run/Component/run-detail/index.js +15 -7
  22. package/dist/src/Entity/run/Data/query.d.ts +10 -0
  23. package/dist/src/Entity/run/Data/query.js +16 -1
  24. package/dist/src/Entity/run/Data/runs.d.ts +2 -0
  25. package/dist/src/Entity/run/Data/runs.js +19 -1
  26. package/dist/src/Entity/run/Screen/run-detail.d.ts +16 -23
  27. package/dist/src/Entity/run/Screen/run-detail.js +27 -34
  28. package/dist/src/Entity/run/Screen/runs.d.ts +7 -3
  29. package/dist/src/Entity/run/Screen/runs.js +31 -48
  30. package/dist/src/Entity/run/Screen/user-runs.d.ts +79 -0
  31. package/dist/src/Entity/run/Screen/user-runs.js +98 -0
  32. package/dist/src/Entity/statistics/Data/statistics.d.ts +111 -9
  33. package/dist/src/Entity/statistics/Data/statistics.js +121 -9
  34. package/dist/src/Entity/statistics/Screen/dashboard.d.ts +10 -2
  35. package/dist/src/Entity/statistics/Screen/dashboard.js +127 -36
  36. package/dist/src/Entity/team/Data/members.d.ts +30 -0
  37. package/dist/src/Entity/team/Data/members.js +83 -0
  38. package/dist/src/Entity/team/Data/team.d.ts +22 -0
  39. package/dist/src/Entity/team/Data/team.js +38 -0
  40. package/dist/src/Entity/team/Data/teams.d.ts +223 -0
  41. package/dist/src/Entity/team/Data/teams.js +267 -0
  42. package/dist/src/Entity/team/Screen/team-users.d.ts +76 -0
  43. package/dist/src/Entity/team/Screen/team-users.js +215 -0
  44. package/dist/src/Entity/team/Screen/teams-screen.d.ts +116 -0
  45. package/dist/src/Entity/team/Screen/teams-screen.js +349 -0
  46. package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +14 -1
  47. package/dist/src/Entity/ticket/Component/comment-item/index.js +15 -1
  48. package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +8 -3
  49. package/dist/src/Entity/ticket/Component/edit-form/index.js +4 -6
  50. package/dist/src/Entity/ticket/Component/sections/index.js +16 -1
  51. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +102 -13
  52. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +440 -72
  53. package/dist/src/Entity/ticket/Data/detail.d.ts +19 -3
  54. package/dist/src/Entity/ticket/Data/detail.js +20 -4
  55. package/dist/src/Entity/ticket/Data/query.d.ts +7 -0
  56. package/dist/src/Entity/ticket/Data/query.js +18 -2
  57. package/dist/src/Entity/ticket/Data/tickets.d.ts +24 -0
  58. package/dist/src/Entity/ticket/Data/tickets.js +41 -1
  59. package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +10 -4
  60. package/dist/src/Entity/ticket/Screen/create-ticket-form.js +37 -16
  61. package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +6 -4
  62. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +16 -16
  63. package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +6 -4
  64. package/dist/src/Entity/ticket/Screen/parent-filter-form.js +14 -11
  65. package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +5 -2
  66. package/dist/src/Entity/ticket/Screen/ticket-agents.js +14 -14
  67. package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +89 -4
  68. package/dist/src/Entity/ticket/Screen/ticket-detail.js +295 -74
  69. package/dist/src/Entity/ticket/Screen/tickets.d.ts +66 -3
  70. package/dist/src/Entity/ticket/Screen/tickets.js +140 -76
  71. package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +91 -0
  72. package/dist/src/Entity/ticket/Screen/user-tickets.js +94 -0
  73. package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +6 -4
  74. package/dist/src/Entity/ticket/Screen/who-filter-form.js +14 -11
  75. package/dist/src/Entity/transport.d.ts +14 -4
  76. package/dist/src/Entity/transport.js +2 -1
  77. package/dist/src/Entity/user/Data/users.d.ts +87 -0
  78. package/dist/src/Entity/user/Data/users.js +60 -0
  79. package/dist/src/Entity/user/Screen/user-overview.d.ts +33 -0
  80. package/dist/src/Entity/user/Screen/user-overview.js +159 -0
  81. package/dist/src/Entity/user/Screen/users.d.ts +39 -0
  82. package/dist/src/Entity/user/Screen/users.js +136 -0
  83. package/dist/src/Extension/contract.d.ts +58 -0
  84. package/dist/src/Extension/contract.js +1 -0
  85. package/dist/src/Extension/load.d.ts +10 -0
  86. package/dist/src/Extension/load.js +21 -0
  87. package/dist/src/Extension/store.d.ts +11 -0
  88. package/dist/src/Extension/store.js +44 -0
  89. package/dist/src/Kernel/chrome.d.ts +49 -12
  90. package/dist/src/Kernel/chrome.js +150 -57
  91. package/dist/src/Kernel/key-input.js +1 -1
  92. package/dist/src/Kernel/mount.d.ts +1 -1
  93. package/dist/src/Kernel/mount.js +1 -1
  94. package/dist/src/Kernel/regions.d.ts +64 -0
  95. package/dist/src/Kernel/regions.js +328 -0
  96. package/dist/src/Kernel/renderer.d.ts +25 -0
  97. package/dist/src/Kernel/renderer.js +142 -0
  98. package/dist/src/Kernel/splash.d.ts +27 -0
  99. package/dist/src/Kernel/splash.js +72 -0
  100. package/dist/src/Kernel/tui-kernel.js +139 -435
  101. package/dist/src/Routing/context.d.ts +5 -0
  102. package/dist/src/Routing/context.js +6 -0
  103. package/dist/src/Routing/registry.js +21 -2
  104. package/dist/src/Routing/router.js +5 -1
  105. package/dist/src/Routing/routes.js +19 -0
  106. package/dist/src/Screen/async-modal-screen.d.ts +26 -0
  107. package/dist/src/Screen/async-modal-screen.js +44 -0
  108. package/dist/src/Screen/async-screen.d.ts +32 -40
  109. package/dist/src/Screen/async-screen.js +45 -41
  110. package/dist/src/Screen/full-screen.d.ts +1 -0
  111. package/dist/src/Screen/full-screen.js +3 -0
  112. package/dist/src/Screen/list-screen.d.ts +7 -1
  113. package/dist/src/Screen/list-screen.js +17 -6
  114. package/dist/src/Screen/modal-screen.d.ts +6 -0
  115. package/dist/src/Screen/modal-screen.js +2 -0
  116. package/dist/src/Screen/screen.d.ts +42 -6
  117. package/dist/src/Screen/screen.js +2 -2
  118. package/dist/src/Widget/textarea.js +10 -14
  119. package/dist/src/Widget/types.d.ts +1 -1
  120. package/dist/src/launcher.d.ts +24 -26
  121. package/dist/src/launcher.js +11 -11
  122. package/dist/src/lib/format.js +1 -1
  123. package/dist/src/lib/markdown.d.ts +53 -0
  124. package/dist/src/lib/markdown.js +522 -0
  125. package/dist/src/lib/refresh-signal.d.ts +56 -0
  126. package/dist/src/lib/refresh-signal.js +85 -0
  127. package/dist/src/lib/theme.d.ts +36 -0
  128. package/dist/src/lib/theme.js +40 -0
  129. package/dist/src/lib/version.d.ts +5 -0
  130. package/dist/src/lib/version.js +46 -0
  131. package/dist/src/lib/vnode.d.ts +86 -63
  132. package/dist/src/lib/vnode.js +112 -85
  133. package/dist/src/types.d.ts +47 -12
  134. package/dist/src/ui.d.ts +2 -0
  135. package/dist/src/ui.js +1 -0
  136. 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: "teams";
18
+ readonly label: "Teams";
16
19
  }];
17
20
  /** The project level: the tabs of an opened project. */
18
21
  export declare const PROJECT_TABS: readonly [{
@@ -62,7 +65,54 @@ export declare const TICKET_TABS: readonly [{
62
65
  readonly id: "runs";
63
66
  readonly label: "Runs";
64
67
  }];
65
- /** The tab bars, keyed by level — the one place a level is named. */
68
+ /**
69
+ * The team level: Overview · Users for one crew — GAIA-391 AC4, by operator
70
+ * ruling.
71
+ *
72
+ * The level the `users` root tab BECAME. It sits between root and user, so the
73
+ * navigation reads root → team → user: which crews are there, who is in this
74
+ * one, what has that person done. Destinations, not modes — each tab is its own
75
+ * screen, so Tab runs a real `Router.navigate` and `teamId` is preserved
76
+ * (`Routing/registry.ts` keeps the params for this scope for the same reason it
77
+ * keeps them for `project` and `user`).
78
+ *
79
+ * `overview` is also declared on the user level, which GAIA-256 D2 already made
80
+ * legal: ids are not globally unique, every function here is level-parameterised,
81
+ * and one bar is on screen at a time.
82
+ */
83
+ export declare const TEAM_TABS: readonly [{
84
+ readonly id: "overview";
85
+ readonly label: "Overview";
86
+ }, {
87
+ readonly id: "users";
88
+ readonly label: "Users";
89
+ }];
90
+ /**
91
+ * The user level: Overview · Tickets · Runs for one person (GAIA-377 D1).
92
+ *
93
+ * Destinations, not modes — each tab is its own screen in the slice that owns
94
+ * its query (D2), so Tab runs a real `Router.navigate` and preserves `userId`.
95
+ *
96
+ * UNCHANGED by GAIA-391: the ruling moved this level's PARENT (it hangs off
97
+ * {@link TEAM_TABS} now instead of off a root `users` tab), not its contents.
98
+ */
99
+ export declare const USER_TABS: readonly [{
100
+ readonly id: "overview";
101
+ readonly label: "Overview";
102
+ }, {
103
+ readonly id: "tickets";
104
+ readonly label: "Tickets";
105
+ }, {
106
+ readonly id: "runs";
107
+ readonly label: "Runs";
108
+ }];
109
+ /**
110
+ * The tab bars, keyed by level — the one place a level is named.
111
+ *
112
+ * Declared in DEPTH order (root → project → ticket, then team → user), which is
113
+ * documentation rather than mechanism: nothing reads this object in key order
114
+ * since GAIA-256 D2 deleted `locateTab`, the one function that did.
115
+ */
66
116
  export declare const TAB_LEVELS: {
67
117
  readonly root: readonly [{
68
118
  readonly id: "dashboard";
@@ -73,6 +123,9 @@ export declare const TAB_LEVELS: {
73
123
  }, {
74
124
  readonly id: "conductors";
75
125
  readonly label: "Conductors";
126
+ }, {
127
+ readonly id: "teams";
128
+ readonly label: "Teams";
76
129
  }];
77
130
  readonly project: readonly [{
78
131
  readonly id: "dashboard";
@@ -100,6 +153,23 @@ export declare const TAB_LEVELS: {
100
153
  readonly id: "runs";
101
154
  readonly label: "Runs";
102
155
  }];
156
+ readonly team: readonly [{
157
+ readonly id: "overview";
158
+ readonly label: "Overview";
159
+ }, {
160
+ readonly id: "users";
161
+ readonly label: "Users";
162
+ }];
163
+ readonly user: readonly [{
164
+ readonly id: "overview";
165
+ readonly label: "Overview";
166
+ }, {
167
+ readonly id: "tickets";
168
+ readonly label: "Tickets";
169
+ }, {
170
+ readonly id: "runs";
171
+ readonly label: "Runs";
172
+ }];
103
173
  };
104
174
  /** The name of a level that carries a top-level tab bar. */
105
175
  export type TabLevel = keyof typeof TAB_LEVELS;
@@ -117,13 +187,40 @@ export type TopTabId = {
117
187
  * Where a screen sits in the navigation: which bar it draws, and which tab of
118
188
  * that bar is its own. A discriminated union over the levels, so a location can
119
189
  * never pair one level's name with another level's tab id.
190
+ *
191
+ * GAIA-373 D5/D6 — the `ticket` arm is the one that is NOT a literal union, and
192
+ * it is also the one that carries an optional bar of its own. Both halves have
193
+ * the same cause: the ticket level is not a DESTINATION level.
194
+ * `router.navigateTab` is never reached for it, because `selectTopTab` claims
195
+ * every ticket tab (GAIA-346 D4), so the literal union that protects root/project
196
+ * NAVIGATION has no target here — while a contributed section mode
197
+ * (`gaia/artifacts`) is data from the contribution store and cannot be a literal
198
+ * in this file at all. Root and project keep theirs intact, so
199
+ * `topTabBar('root', 'runs')` is still a TS2345.
200
+ *
201
+ * `tabs` is read by BOTH bar builders below and by the kernel's Tab cycle, so
202
+ * the bar drawn, the bar clicked and the bar cycled are three readings of one
203
+ * field rather than three answers.
120
204
  */
121
205
  export type TabLocation = {
122
- [L in TabLevel]: {
123
- level: L;
124
- tab: TabIdOf<L>;
125
- };
126
- }[TabLevel];
206
+ level: 'root';
207
+ tab: TabIdOf<'root'>;
208
+ } | {
209
+ level: 'project';
210
+ tab: TabIdOf<'project'>;
211
+ } | {
212
+ level: 'ticket';
213
+ tab: string;
214
+ tabs?: readonly TopTab[];
215
+ } | {
216
+ level: 'team';
217
+ tab: TabIdOf<'team'>;
218
+ } | {
219
+ level: 'user';
220
+ tab: TabIdOf<'user'>;
221
+ };
222
+ /** The entries a location draws: its own, else its level's registry. */
223
+ export declare const tabsOfLocation: (location: TabLocation) => readonly TopTab[];
127
224
  /**
128
225
  * The bar as the PIECES it is drawn from: one part per tab, carrying that tab's
129
226
  * id, with the joins between them carrying none.
@@ -191,5 +288,11 @@ export declare function topTabParts<L extends TabLevel>(level: L, activeId: TabI
191
288
  * companion of {@link tabBarFor}, and the pairing it needs was already checked
192
289
  * when the location was built. Keeping the two in step is what makes "the bar you
193
290
  * see is the bar you click" true by construction: both read `location`.
291
+ *
292
+ * GAIA-373 — `TabBarPart<string>` rather than the `TopTabId` default, and that
293
+ * is an honest widening rather than a lost check: a location may carry a bar of
294
+ * its own now, whose entries are CONTRIBUTED (`gaia/artifacts`) and by
295
+ * construction outside every union derived from `TAB_LEVELS`. The level-typed
296
+ * {@link topTabParts} keeps the narrow ids for callers that name a level.
194
297
  */
195
- export declare function tabPartsFor(location: TabLocation): TabBarPart[];
298
+ export declare function tabPartsFor(location: TabLocation): TabBarPart<string>[];
@@ -1,13 +1,17 @@
1
1
  // GAIA-239: THE registry — the single place the set, the order and the labels of
2
- // the top-level tabs are written. Navigation has THREE levels, and every screen
2
+ // the top-level tabs are written. Navigation has FIVE levels, and every screen
3
3
  // shows exactly ONE tab bar: the one of its own level.
4
4
  //
5
- // root dashboard, projects, project-picker → Dashboard · Projects
5
+ // root dashboard, projects, project-picker → Dashboard · Projects ·
6
+ // Conductors · Teams
6
7
  // project dashboard, project, ticket-agents, → Dashboard · Tickets ·
7
8
  // project-runs Ticket-Agents · Runs
8
9
  // ticket overview, comments, related, runs → the sections of one open
9
10
  // ticket, in the bar row every
10
11
  // other level uses (GAIA-346)
12
+ // team one crew → Overview · Users (GAIA-391)
13
+ // user one person → Overview · Tickets · Runs
14
+ // (GAIA-377)
11
15
  //
12
16
  // `tickets` is the project/ticket drill-down; `ticket-agents` is the managed
13
17
  // create-agent list (GAIA-190); `runs` is the project-scoped run overview
@@ -29,10 +33,12 @@
29
33
  // level — the bar you Tab through or click IS the current level's bar — so it
30
34
  // pairs the target with `here.level` directly and `locateTab` is deleted.
31
35
  //
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.
36
+ // Only tabs that are actually navigable are listed. GAIA-377 added `Users` with
37
+ // its screens; a tab drawn before its screen exists would advertise navigation
38
+ // the operator cannot take, which is the very defect GAIA-239 removes. The same
39
+ // rule runs the other way, and GAIA-391 is where it did: a LEVEL is declared
40
+ // with the screens that occupy it, so `team` arrives with the crew overview and
41
+ // the crew's Users list, not before them.
36
42
  //
37
43
  // GAIA-232 added `conductors`, and put it on the ROOT level although the note
38
44
  // above once reserved it for the project level. A conductor is an operator
@@ -53,6 +59,28 @@ export const ROOT_TABS = [
53
59
  { id: 'dashboard', label: 'Dashboard' },
54
60
  { id: 'projects', label: 'Projects' },
55
61
  { id: 'conductors', label: 'Conductors' },
62
+ // GAIA-391 AC4 — `teams` is the LAST root tab, and there is deliberately no
63
+ // `users` beside it any more.
64
+ //
65
+ // ROOT level for teams, by GAIA-232's argument for Conductors: a team is the
66
+ // level that cuts ACROSS customer projects (GAIA-321), so "which crews are
67
+ // there" has to be answerable without first choosing a project.
68
+ //
69
+ // THIS REVISES AN INFORMATION-ARCHITECTURE DECISION THAT BELONGS TO GAIA-377,
70
+ // NOT TO GAIA-391. GAIA-377 D1 shipped `users` as a root tab and declared
71
+ // {@link USER_TABS} as the level below it; GAIA-391 moved that whole level
72
+ // under the team level by OPERATOR RULING, so the root bar reads
73
+ // `Dashboard · Projects · Conductors · Teams` and a person is reached through
74
+ // the crew they are in: Teams → one crew → Users → one person. Recorded here
75
+ // because a silent removal would later read as a regression against 377, and
76
+ // because the previous comment on this list argued the OPPOSITE order
77
+ // ("people, then the crews they are in") — an argument this ruling overturns
78
+ // rather than an accident.
79
+ //
80
+ // Users did NOT become unreachable, which is what makes the move a move: the
81
+ // team level carries the tab ({@link TEAM_TABS}), and the Dashboard's PER USER
82
+ // rows still open one person directly.
83
+ { id: 'teams', label: 'Teams' },
56
84
  ];
57
85
  /** The project level: the tabs of an opened project. */
58
86
  export const PROJECT_TABS = [
@@ -88,12 +116,57 @@ export const TICKET_TABS = [
88
116
  { id: 'related', label: 'Related' },
89
117
  { id: 'runs', label: 'Runs' },
90
118
  ];
91
- /** The tab bars, keyed by level — the one place a level is named. */
119
+ /**
120
+ * The team level: Overview · Users for one crew — GAIA-391 AC4, by operator
121
+ * ruling.
122
+ *
123
+ * The level the `users` root tab BECAME. It sits between root and user, so the
124
+ * navigation reads root → team → user: which crews are there, who is in this
125
+ * one, what has that person done. Destinations, not modes — each tab is its own
126
+ * screen, so Tab runs a real `Router.navigate` and `teamId` is preserved
127
+ * (`Routing/registry.ts` keeps the params for this scope for the same reason it
128
+ * keeps them for `project` and `user`).
129
+ *
130
+ * `overview` is also declared on the user level, which GAIA-256 D2 already made
131
+ * legal: ids are not globally unique, every function here is level-parameterised,
132
+ * and one bar is on screen at a time.
133
+ */
134
+ export const TEAM_TABS = [
135
+ { id: 'overview', label: 'Overview' },
136
+ { id: 'users', label: 'Users' },
137
+ ];
138
+ /**
139
+ * The user level: Overview · Tickets · Runs for one person (GAIA-377 D1).
140
+ *
141
+ * Destinations, not modes — each tab is its own screen in the slice that owns
142
+ * its query (D2), so Tab runs a real `Router.navigate` and preserves `userId`.
143
+ *
144
+ * UNCHANGED by GAIA-391: the ruling moved this level's PARENT (it hangs off
145
+ * {@link TEAM_TABS} now instead of off a root `users` tab), not its contents.
146
+ */
147
+ export const USER_TABS = [
148
+ { id: 'overview', label: 'Overview' },
149
+ { id: 'tickets', label: 'Tickets' },
150
+ { id: 'runs', label: 'Runs' },
151
+ ];
152
+ /**
153
+ * The tab bars, keyed by level — the one place a level is named.
154
+ *
155
+ * Declared in DEPTH order (root → project → ticket, then team → user), which is
156
+ * documentation rather than mechanism: nothing reads this object in key order
157
+ * since GAIA-256 D2 deleted `locateTab`, the one function that did.
158
+ */
92
159
  export const TAB_LEVELS = {
93
160
  root: ROOT_TABS,
94
161
  project: PROJECT_TABS,
95
162
  ticket: TICKET_TABS,
163
+ team: TEAM_TABS,
164
+ user: USER_TABS,
96
165
  };
166
+ /** The entries a location draws: its own, else its level's registry. */
167
+ export const tabsOfLocation = (location) => 'tabs' in location && location.tabs
168
+ ? location.tabs
169
+ : TAB_LEVELS[location.level];
97
170
  /** What joins two tabs in the rendered bar — part of neither tab's span. */
98
171
  const SEPARATOR = ' ';
99
172
  /** One tab's rendered segment: bracketed when active, space-padded otherwise. */
@@ -133,7 +206,7 @@ export function topTabBar(level, activeId) {
133
206
  * not ask the compiler to correlate two independently narrowed properties.
134
207
  */
135
208
  export function tabBarFor(location) {
136
- return renderBar(TAB_LEVELS[location.level], location.tab);
209
+ return renderBar(tabsOfLocation(location), location.tab);
137
210
  }
138
211
  // GAIA-256 spec D2 — `locateTab` USED TO LIVE HERE and is deliberately gone.
139
212
  //
@@ -168,7 +241,13 @@ export function topTabParts(level, activeId) {
168
241
  * companion of {@link tabBarFor}, and the pairing it needs was already checked
169
242
  * when the location was built. Keeping the two in step is what makes "the bar you
170
243
  * see is the bar you click" true by construction: both read `location`.
244
+ *
245
+ * GAIA-373 — `TabBarPart<string>` rather than the `TopTabId` default, and that
246
+ * is an honest widening rather than a lost check: a location may carry a bar of
247
+ * its own now, whose entries are CONTRIBUTED (`gaia/artifacts`) and by
248
+ * construction outside every union derived from `TAB_LEVELS`. The level-typed
249
+ * {@link topTabParts} keeps the narrow ids for callers that name a level.
171
250
  */
172
251
  export function tabPartsFor(location) {
173
- return renderParts(TAB_LEVELS[location.level], location.tab);
252
+ return renderParts(tabsOfLocation(location), location.tab);
174
253
  }
@@ -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.
@@ -223,9 +223,10 @@ export interface ConductorRowContext {
223
223
  /**
224
224
  * The conductor collection as cockpit rows, in server order.
225
225
  *
226
- * Swallows a read failure into an empty list on purpose: the Conductors screen
227
- * re-reads every 5 s, so one failed poll must leave the screen navigable rather
228
- * than replace it with an error.
226
+ * Sideloads `machine` so capacity (GAIA-353 D5/D10) comes from the host budget,
227
+ * not removed conductor fields. Swallows a read failure into an empty list on
228
+ * purpose: the Conductors screen re-reads every 5 s, so one failed poll must
229
+ * leave the screen navigable rather than replace it with an error.
229
230
  */
230
231
  export declare const listConductorRows: (client: DropshClient, ctx: ConductorRowContext) => Promise<ConductorRow[]>;
231
232
  /**
@@ -4,14 +4,19 @@
4
4
  // service that applied them would be unusable for any other purpose. The two read
5
5
  // strategies are both just reading, so both live here.
6
6
  import { toUnixSeconds } from '@gaia-ai/core';
7
+ import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
7
8
  import { humanizeAge } from '../../../lib/format.js';
8
9
  import { listOutcome, listPreferringWrite, } from '../../transport.js';
9
10
  import { deriveConductorStatus } from './liveness.js';
10
11
  const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
11
12
  const toConductor = (r) => ({
12
13
  id: r.id,
13
- label: str(r.attr('label')) ?? str(r.attr('machine_id')) ?? r.id,
14
- machineId: str(r.attr('machine_id')),
14
+ label: str(r.attr('label')) ??
15
+ str(r.attr('conductor_id')) ??
16
+ str(r.attr('machine_id')) ??
17
+ r.id,
18
+ // Registration key (GAIA-353 renamed machine_id → conductor_id).
19
+ machineId: str(r.attr('conductor_id')) ?? str(r.attr('machine_id')),
15
20
  status: str(r.attr('status')),
16
21
  projectId: r.rel('project_id'),
17
22
  });
@@ -242,19 +247,68 @@ export const CONDUCTOR_TABLE = {
242
247
  ],
243
248
  };
244
249
  const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
245
- const toConductorRow = (r, ctx) => {
250
+ /** Id of a single-valued relationship linkage (`data` object), or null. */
251
+ function singleRelId(rel) {
252
+ const data = rel?.data;
253
+ if (!data || Array.isArray(data))
254
+ return null;
255
+ return data.id ?? null;
256
+ }
257
+ /**
258
+ * Dropsh's collection path hands Resource accessors; the cockpit row path uses
259
+ * client.get so it can sideload `machine`. Map the raw JSON:API resource into
260
+ * the same attr/rel surface toConductor already speaks.
261
+ */
262
+ function asResource(raw) {
263
+ return {
264
+ id: raw.id ?? '',
265
+ type: raw.type ?? '',
266
+ attr: (key) => raw.attributes?.[key],
267
+ rel: (key) => singleRelId(raw.relationships?.[key]),
268
+ rels: (key) => {
269
+ const data = raw.relationships?.[key]?.data;
270
+ if (!data)
271
+ return [];
272
+ if (Array.isArray(data)) {
273
+ return data
274
+ .map((d) => d.id)
275
+ .filter((id) => typeof id === 'string');
276
+ }
277
+ return data.id ? [data.id] : [];
278
+ },
279
+ };
280
+ }
281
+ /** Host capacity from the sideloaded machine, with cutover fallbacks (GAIA-353). */
282
+ function capacityOf(raw, machinesById) {
283
+ const machineId = singleRelId(raw.relationships?.machine);
284
+ const machine = machineId ? machinesById.get(machineId) : undefined;
285
+ const attrs = raw.attributes ?? {};
286
+ return {
287
+ // Prefer machine.current_load; process telemetry / legacy conductor attrs
288
+ // only when the host row is missing during cutover.
289
+ currentLoad: num(machine?.attributes?.current_load) ??
290
+ num(attrs.reported_load) ??
291
+ num(attrs.current_load) ??
292
+ 0,
293
+ // max_parallel is machine-owned SoT; legacy conductor attr is last resort.
294
+ maxParallel: num(machine?.attributes?.max_parallel) ?? num(attrs.max_parallel) ?? 0,
295
+ };
296
+ }
297
+ const toConductorRow = (raw, ctx, machinesById) => {
298
+ const r = asResource(raw);
246
299
  const workspaceRoot = str(r.attr('workspace_root')) ?? '';
247
300
  // ISO-8601 over the wire, not the unix integer the field name implies — a
248
301
  // `typeof === 'number'` guard here is what made a live conductor read `stale`.
249
302
  const lastSeen = toUnixSeconds(r.attr('last_seen')) ?? 0;
250
303
  const leaseExpiresAt = toUnixSeconds(r.attr('lease_expires_at')) ?? 0;
304
+ const { currentLoad, maxParallel } = capacityOf(raw, machinesById);
251
305
  return {
252
306
  ...toConductor(r),
253
307
  workspaceRoot,
254
308
  lastSeen,
255
309
  leaseExpiresAt,
256
- currentLoad: num(r.attr('current_load')) ?? 0,
257
- maxParallel: num(r.attr('max_parallel')) ?? 0,
310
+ currentLoad,
311
+ maxParallel,
258
312
  lastPid: num(r.attr('last_pid')),
259
313
  conductorName: str(r.attr('conductor_name')),
260
314
  // `present: true` because the row IS a control-plane record — which is why
@@ -273,13 +327,25 @@ const toConductorRow = (r, ctx) => {
273
327
  /**
274
328
  * The conductor collection as cockpit rows, in server order.
275
329
  *
276
- * Swallows a read failure into an empty list on purpose: the Conductors screen
277
- * re-reads every 5 s, so one failed poll must leave the screen navigable rather
278
- * than replace it with an error.
330
+ * Sideloads `machine` so capacity (GAIA-353 D5/D10) comes from the host budget,
331
+ * not removed conductor fields. Swallows a read failure into an empty list on
332
+ * purpose: the Conductors screen re-reads every 5 s, so one failed poll must
333
+ * leave the screen navigable rather than replace it with an error.
279
334
  */
280
335
  export const listConductorRows = async (client, ctx) => {
281
- const outcome = await listOutcome(client, 'gaia_conductor', (r) => toConductorRow(r, ctx));
282
- return outcome.rows;
336
+ const params = new DrupalJsonApiParams().addInclude(['machine']);
337
+ try {
338
+ const doc = (await client.get('gaia_conductor/gaia_conductor', params));
339
+ const machinesById = new Map();
340
+ for (const inc of doc.included ?? []) {
341
+ if (inc.id)
342
+ machinesById.set(inc.id, inc);
343
+ }
344
+ return (doc.data ?? []).map((raw) => toConductorRow(raw, ctx, machinesById));
345
+ }
346
+ catch {
347
+ return [];
348
+ }
283
349
  };
284
350
  /**
285
351
  * Project uuid → name, for the Conductors screen's project column.