@gaia-ai/addon-gaia-ui 0.6.4 → 0.7.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 (134) hide show
  1. package/README.md +1 -1
  2. package/dist/src/Agent/Component/agent-list/index.d.ts +6 -0
  3. package/dist/src/Agent/Component/agent-list/index.js +14 -0
  4. package/dist/src/Agent/launch.d.ts +25 -0
  5. package/dist/src/Agent/launch.js +49 -0
  6. package/dist/src/Component/section-list/index.d.ts +37 -0
  7. package/dist/src/Component/section-list/index.js +60 -13
  8. package/dist/src/Component/stats-panel/index.d.ts +49 -0
  9. package/dist/src/Component/stats-panel/index.js +114 -0
  10. package/dist/src/Component/tab-bar/index.d.ts +11 -0
  11. package/dist/src/Component/tab-bar/index.js +21 -1
  12. package/dist/src/Component/ticket-detail/index.d.ts +8 -0
  13. package/dist/src/Component/ticket-detail/index.js +68 -4
  14. package/dist/src/Component/top-tabs/index.d.ts +118 -3
  15. package/dist/src/Component/top-tabs/index.js +123 -8
  16. package/dist/src/Entity/conductor/Data/conductors.d.ts +28 -0
  17. package/dist/src/Entity/conductor/Data/conductors.js +23 -0
  18. package/dist/src/Entity/conductor/Data/liveness.d.ts +10 -0
  19. package/dist/src/Entity/conductor/Data/liveness.js +25 -0
  20. package/dist/src/Entity/project/Component/dashboard-list/index.d.ts +8 -0
  21. package/dist/src/Entity/project/Component/dashboard-list/index.js +7 -0
  22. package/dist/src/Entity/project/Component/project-list/index.d.ts +8 -0
  23. package/dist/src/Entity/project/Component/project-list/index.js +7 -0
  24. package/dist/src/Entity/project/Component/project-picker/index.d.ts +15 -0
  25. package/dist/src/Entity/project/Component/project-picker/index.js +42 -0
  26. package/dist/src/Entity/project/Data/projects.d.ts +21 -0
  27. package/dist/src/Entity/project/Data/projects.js +81 -0
  28. package/dist/src/Entity/project/Data/with-tickets.d.ts +20 -0
  29. package/dist/src/Entity/project/Data/with-tickets.js +36 -0
  30. package/dist/src/Entity/run/Component/run-detail/index.d.ts +17 -0
  31. package/dist/src/Entity/run/Component/run-detail/index.js +145 -0
  32. package/dist/src/Entity/run/Component/run-table/index.d.ts +10 -0
  33. package/dist/src/Entity/run/Component/run-table/index.js +20 -0
  34. package/dist/src/Entity/run/Data/active-run.d.ts +15 -0
  35. package/dist/src/Entity/run/Data/active-run.js +23 -0
  36. package/dist/src/Entity/run/Data/runs.d.ts +72 -0
  37. package/dist/src/Entity/run/Data/runs.js +125 -0
  38. package/dist/src/Entity/run/Data/status.d.ts +12 -0
  39. package/dist/src/Entity/run/Data/status.js +40 -0
  40. package/dist/src/Entity/statistics/Data/statistics.d.ts +87 -0
  41. package/dist/src/Entity/statistics/Data/statistics.js +144 -0
  42. package/dist/src/Entity/term/Data/terms.d.ts +16 -0
  43. package/dist/src/Entity/term/Data/terms.js +14 -0
  44. package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +21 -0
  45. package/dist/src/Entity/ticket/Component/comment-item/index.js +31 -0
  46. package/dist/src/Entity/ticket/Component/create-form/index.d.ts +15 -0
  47. package/dist/src/Entity/ticket/Component/create-form/index.js +60 -0
  48. package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +28 -0
  49. package/dist/src/Entity/ticket/Component/edit-form/index.js +76 -0
  50. package/dist/src/Entity/ticket/Component/tab-bar/index.d.ts +20 -0
  51. package/dist/src/Entity/ticket/Component/tab-bar/index.js +65 -0
  52. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +106 -0
  53. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +461 -0
  54. package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +5 -0
  55. package/dist/src/Entity/ticket/Component/ticket-table/index.js +14 -0
  56. package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +7 -0
  57. package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -0
  58. package/dist/src/Entity/ticket/Data/detail.d.ts +28 -0
  59. package/dist/src/Entity/ticket/Data/detail.js +29 -0
  60. package/dist/src/Entity/ticket/Data/parents.d.ts +27 -0
  61. package/dist/src/Entity/ticket/Data/parents.js +67 -0
  62. package/dist/src/Entity/ticket/Data/query.d.ts +20 -0
  63. package/dist/src/Entity/ticket/Data/query.js +66 -0
  64. package/dist/src/Entity/ticket/Data/tickets.d.ts +45 -0
  65. package/dist/src/Entity/ticket/Data/tickets.js +80 -0
  66. package/dist/src/Entity/ticket/Data/workflow.d.ts +13 -0
  67. package/dist/src/Entity/ticket/Data/workflow.js +26 -0
  68. package/dist/src/Entity/ticket/Data/write.d.ts +12 -0
  69. package/dist/src/Entity/ticket/Data/write.js +18 -0
  70. package/dist/src/Entity/ticket/Form/create-data.d.ts +64 -0
  71. package/dist/src/Entity/ticket/Form/create-data.js +178 -0
  72. package/dist/src/Entity/ticket/Form/create.d.ts +125 -0
  73. package/dist/src/Entity/ticket/Form/create.js +112 -0
  74. package/dist/src/Entity/ticket/Form/edit-data.d.ts +9 -0
  75. package/dist/src/Entity/ticket/Form/edit-data.js +38 -0
  76. package/dist/src/Entity/ticket/Form/edit.d.ts +65 -0
  77. package/dist/src/Entity/ticket/Form/edit.js +61 -0
  78. package/dist/src/Entity/transport.d.ts +50 -0
  79. package/dist/src/Entity/transport.js +49 -0
  80. package/dist/src/Entity/user/Data/users.d.ts +29 -0
  81. package/dist/src/Entity/user/Data/users.js +28 -0
  82. package/dist/src/Form/field.d.ts +99 -0
  83. package/dist/src/Form/field.js +27 -0
  84. package/dist/src/Form/form.d.ts +53 -0
  85. package/dist/src/Form/form.js +142 -0
  86. package/dist/src/Form/index.d.ts +3 -0
  87. package/dist/src/Form/index.js +5 -0
  88. package/dist/src/Form/keys.d.ts +3 -0
  89. package/dist/src/Form/keys.js +57 -0
  90. package/dist/src/Kernel/tui-kernel.d.ts +11 -0
  91. package/dist/src/Kernel/tui-kernel.js +1566 -320
  92. package/dist/src/Screen/projects.d.ts +1 -0
  93. package/dist/src/Screen/projects.js +1 -0
  94. package/dist/src/Widget/action.d.ts +3 -0
  95. package/dist/src/Widget/action.js +9 -0
  96. package/dist/src/Widget/autocomplete.d.ts +3 -0
  97. package/dist/src/Widget/autocomplete.js +80 -0
  98. package/dist/src/Widget/chrome.d.ts +38 -0
  99. package/dist/src/Widget/chrome.js +60 -0
  100. package/dist/src/Widget/index.d.ts +4 -0
  101. package/dist/src/Widget/index.js +12 -0
  102. package/dist/src/Widget/multiselect.d.ts +3 -0
  103. package/dist/src/Widget/multiselect.js +36 -0
  104. package/dist/src/Widget/option-source.d.ts +58 -0
  105. package/dist/src/Widget/option-source.js +98 -0
  106. package/dist/src/Widget/render.d.ts +42 -0
  107. package/dist/src/Widget/render.js +82 -0
  108. package/dist/src/Widget/select.d.ts +3 -0
  109. package/dist/src/Widget/select.js +26 -0
  110. package/dist/src/Widget/text.d.ts +3 -0
  111. package/dist/src/Widget/text.js +23 -0
  112. package/dist/src/Widget/types.d.ts +59 -0
  113. package/dist/src/Widget/types.js +21 -0
  114. package/dist/src/launcher.d.ts +33 -8
  115. package/dist/src/launcher.js +12 -4
  116. package/dist/src/lib/ansi.d.ts +15 -0
  117. package/dist/src/lib/ansi.js +53 -9
  118. package/dist/src/lib/format.d.ts +21 -0
  119. package/dist/src/lib/format.js +46 -4
  120. package/dist/src/lib/hit-test.d.ts +138 -0
  121. package/dist/src/lib/hit-test.js +138 -0
  122. package/dist/src/lib/jsonapi-error.d.ts +7 -0
  123. package/dist/src/lib/jsonapi-error.js +43 -0
  124. package/dist/src/lib/mouse.d.ts +35 -0
  125. package/dist/src/lib/mouse.js +89 -0
  126. package/dist/src/lib/width-table.d.ts +4 -0
  127. package/dist/src/lib/width-table.js +147 -0
  128. package/dist/src/lib/width.d.ts +61 -0
  129. package/dist/src/lib/width.js +277 -0
  130. package/dist/src/plugin.js +3 -3
  131. package/dist/src/route.d.ts +11 -0
  132. package/dist/src/route.js +215 -0
  133. package/dist/src/types.d.ts +53 -2
  134. package/package.json +3 -3
@@ -3,6 +3,121 @@ export interface TopTab {
3
3
  id: string;
4
4
  label: string;
5
5
  }
6
- export declare const TOP_TABS: TopTab[];
7
- /** The top tab bar as a single row; the active tab is bracketed. */
8
- export declare function topTabBar(activeId: string): string;
6
+ /** The root level: what there is before a project is chosen. */
7
+ export declare const ROOT_TABS: readonly [{
8
+ readonly id: "dashboard";
9
+ readonly label: "Dashboard";
10
+ }, {
11
+ readonly id: "projects";
12
+ readonly label: "Projects";
13
+ }];
14
+ /** The project level: the tabs of an opened project. */
15
+ export declare const PROJECT_TABS: readonly [{
16
+ readonly id: "dashboard";
17
+ readonly label: "Dashboard";
18
+ }, {
19
+ readonly id: "tickets";
20
+ readonly label: "Tickets";
21
+ }, {
22
+ readonly id: "ticket-agents";
23
+ readonly label: "Ticket-Agents";
24
+ }, {
25
+ readonly id: "runs";
26
+ readonly label: "Runs";
27
+ }];
28
+ /** The tab bars, keyed by level — the one place a level is named. */
29
+ export declare const TAB_LEVELS: {
30
+ readonly root: readonly [{
31
+ readonly id: "dashboard";
32
+ readonly label: "Dashboard";
33
+ }, {
34
+ readonly id: "projects";
35
+ readonly label: "Projects";
36
+ }];
37
+ readonly project: readonly [{
38
+ readonly id: "dashboard";
39
+ readonly label: "Dashboard";
40
+ }, {
41
+ readonly id: "tickets";
42
+ readonly label: "Tickets";
43
+ }, {
44
+ readonly id: "ticket-agents";
45
+ readonly label: "Ticket-Agents";
46
+ }, {
47
+ readonly id: "runs";
48
+ readonly label: "Runs";
49
+ }];
50
+ };
51
+ /** The name of a level that carries a top-level tab bar. */
52
+ export type TabLevel = keyof typeof TAB_LEVELS;
53
+ /** The ids of one level's tabs, derived from {@link TAB_LEVELS}. */
54
+ export type TabIdOf<L extends TabLevel> = (typeof TAB_LEVELS)[L][number]['id'];
55
+ /**
56
+ * The id of any top-level tab, derived — never re-listed. Before GAIA-239 this
57
+ * union was hand-written in `tui-kernel.ts` alongside a third hardcoded copy of
58
+ * the same ids for the Tab cycle order; the three could drift silently.
59
+ */
60
+ export type TopTabId = {
61
+ [L in TabLevel]: TabIdOf<L>;
62
+ }[TabLevel];
63
+ /**
64
+ * Where a screen sits in the navigation: which bar it draws, and which tab of
65
+ * that bar is its own. A discriminated union over the levels, so a location can
66
+ * never pair one level's name with another level's tab id.
67
+ */
68
+ export type TabLocation = {
69
+ [L in TabLevel]: {
70
+ level: L;
71
+ tab: TabIdOf<L>;
72
+ };
73
+ }[TabLevel];
74
+ /**
75
+ * One level's tab bar as a single row; the active tab is bracketed.
76
+ *
77
+ * The level is part of the signature on purpose: it is what keeps the id checked
78
+ * at compile time now that there is more than one bar. `topTabBar('root', 'runs')`
79
+ * is a TS2345 naming the root level's valid ids — not a bar that silently
80
+ * brackets nothing.
81
+ */
82
+ export declare function topTabBar<L extends TabLevel>(level: L, activeId: TabIdOf<L>): string;
83
+ /**
84
+ * The same bar, for a caller holding a whole {@link TabLocation} rather than a
85
+ * literal level. The pairing was already checked when the location was built, so
86
+ * this needs no second check — and unlike `topTabBar(loc.level, loc.tab)` it does
87
+ * not ask the compiler to correlate two independently narrowed properties.
88
+ */
89
+ export declare function tabBarFor(location: TabLocation): string;
90
+ /** Where one tab sits inside the bar: `[start, end)` in display columns from the
91
+ * bar's own first column. */
92
+ export interface TopTabSpan<Id extends string = TopTabId> {
93
+ id: Id;
94
+ start: number;
95
+ end: number;
96
+ }
97
+ /**
98
+ * One level's own hit map, built from the SAME `segment`/`SEPARATOR` that level's
99
+ * rendered bar is joined from — so the layout and the click mapping are two
100
+ * readings of one expression and cannot drift apart. That is the argument
101
+ * `lib/hit-test.ts` makes for the body rows, applied to a column axis.
102
+ *
103
+ * Offsets are display columns, which is the unit a mouse report carries. For the
104
+ * current ASCII labels that coincides with string offsets, which is what lets
105
+ * the test slice `topTabBar()` with a span and compare.
106
+ *
107
+ * GAIA-239: the LEVEL is part of the signature, exactly as it is for
108
+ * {@link topTabBar}, and for the same reason twice over. GAIA-241 shipped one
109
+ * `topTabSpans(activeId)` over the single flat bar; read level-blind it would
110
+ * publish the project bar's three spans on the root screen too, so a click at
111
+ * the column where `Ticket-Agents` used to sit would open a tab that screen does
112
+ * not draw — the very defect this ticket removes, arriving through the mouse.
113
+ * A map and a bar of two different levels is now a TS2345, not a silent
114
+ * mis-click.
115
+ */
116
+ export declare function topTabSpans<L extends TabLevel>(level: L, activeId: TabIdOf<L>): TopTabSpan<TabIdOf<L>>[];
117
+ /**
118
+ * The same hit map, for a caller holding a whole {@link TabLocation} — the
119
+ * companion of {@link tabBarFor}, and the pairing it needs was already checked
120
+ * when the location was built. Keeping the two in step is what makes "the bar you
121
+ * see is the bar you click" true by construction: both read `location`.
122
+ */
123
+ export declare function tabSpansFor(location: TabLocation): TopTabSpan[];
@@ -1,13 +1,128 @@
1
- // The top-level navigation tabs. `tickets` is the project/ticket drill-down;
2
- // `ticket-agents` is the managed create-agent list (GAIA-190); `runs` is the
3
- // project-scoped run overview (GAIA-192). `conductors` is a planned follow-up
4
- // (tracked separately) — not wired yet.
5
- export const TOP_TABS = [
1
+ import { displayWidth } from '../../lib/width.js';
2
+ // GAIA-239: THE registry — the single place the set, the order and the labels of
3
+ // the top-level tabs are written. Navigation has THREE levels, and every screen
4
+ // shows exactly ONE tab bar: the one of its own level.
5
+ //
6
+ // root dashboard, projects, project-picker → Dashboard · Projects
7
+ // project dashboard, project, ticket-agents, → Dashboard · Tickets ·
8
+ // project-runs Ticket-Agents · Runs
9
+ // ticket ticket → the DETAIL tabs, drawn as the
10
+ // header box's meta row, so no
11
+ // top-level bar at all
12
+ //
13
+ // `tickets` is the project/ticket drill-down; `ticket-agents` is the managed
14
+ // create-agent list (GAIA-190); `runs` is the project-scoped run overview
15
+ // (GAIA-192) — all three project-scoped, which is why they sit on the project
16
+ // level and not on the root one.
17
+ //
18
+ // GAIA-256 spec D2: `dashboard` is declared on BOTH levels, on purpose. There is
19
+ // ONE Dashboard screen — the root one and the project one are the same screen
20
+ // with its project filter unset or set, and its LEVEL follows that same bit, so
21
+ // the bar and the filter can never disagree: they read one value.
22
+ //
23
+ // That is why tab ids are no longer globally unique, and why nothing here
24
+ // requires them to be. The requirement was never a property of the model — every
25
+ // function in this file is already level-parameterised (`topTabBar(level, id)`,
26
+ // `topTabSpans(level, id)`, `tabBarFor(location)`, `TabIdOf<L>`). It was a
27
+ // property of ONE function, the level-blind `locateTab`, which walked the levels
28
+ // in key order and would now resolve the project level's `dashboard` to the ROOT
29
+ // location. `gotoTopTab` was its only caller, and it only ever moves within one
30
+ // level — the bar you Tab through or click IS the current level's bar — so it
31
+ // pairs the target with `here.level` directly and `locateTab` is deleted.
32
+ //
33
+ // Only tabs that are actually navigable are listed. A planned screen (`Users` on
34
+ // the root level, `Conductors` on the project level) arrives with its own ticket
35
+ // and adds its entry here then — a tab drawn before its screen exists would
36
+ // advertise navigation the operator cannot take, which is the very defect
37
+ // GAIA-239 removes.
38
+ //
39
+ // `as const satisfies` is load-bearing on both halves: `satisfies` still
40
+ // shape-checks every entry against `TopTab`, while `as const` keeps the literal
41
+ // ids so the id unions below can be DERIVED from them. A plain `: TopTab[]`
42
+ // annotation would widen `id` to `string` and the derivation would collapse to
43
+ // `string`, taking the compile-time check with it.
44
+ /** The root level: what there is before a project is chosen. */
45
+ export const ROOT_TABS = [
46
+ { id: 'dashboard', label: 'Dashboard' },
47
+ { id: 'projects', label: 'Projects' },
48
+ ];
49
+ /** The project level: the tabs of an opened project. */
50
+ export const PROJECT_TABS = [
51
+ { id: 'dashboard', label: 'Dashboard' },
6
52
  { id: 'tickets', label: 'Tickets' },
7
53
  { id: 'ticket-agents', label: 'Ticket-Agents' },
8
54
  { id: 'runs', label: 'Runs' },
9
55
  ];
10
- /** The top tab bar as a single row; the active tab is bracketed. */
11
- export function topTabBar(activeId) {
12
- return TOP_TABS.map((t) => t.id === activeId ? `[${t.label}]` : ` ${t.label} `).join(' ');
56
+ /** The tab bars, keyed by level — the one place a level is named. */
57
+ export const TAB_LEVELS = {
58
+ root: ROOT_TABS,
59
+ project: PROJECT_TABS,
60
+ };
61
+ /** What joins two tabs in the rendered bar — part of neither tab's span. */
62
+ const SEPARATOR = ' ';
63
+ /** One tab's rendered segment: bracketed when active, space-padded otherwise. */
64
+ const segment = (tab, activeId) => tab.id === activeId ? `[${tab.label}]` : ` ${tab.label} `;
65
+ const renderBar = (tabs, activeId) => tabs.map((t) => segment(t, activeId)).join(SEPARATOR);
66
+ /**
67
+ * One level's tab bar as a single row; the active tab is bracketed.
68
+ *
69
+ * The level is part of the signature on purpose: it is what keeps the id checked
70
+ * at compile time now that there is more than one bar. `topTabBar('root', 'runs')`
71
+ * is a TS2345 naming the root level's valid ids — not a bar that silently
72
+ * brackets nothing.
73
+ */
74
+ export function topTabBar(level, activeId) {
75
+ return renderBar(TAB_LEVELS[level], activeId);
76
+ }
77
+ /**
78
+ * The same bar, for a caller holding a whole {@link TabLocation} rather than a
79
+ * literal level. The pairing was already checked when the location was built, so
80
+ * this needs no second check — and unlike `topTabBar(loc.level, loc.tab)` it does
81
+ * not ask the compiler to correlate two independently narrowed properties.
82
+ */
83
+ export function tabBarFor(location) {
84
+ return renderBar(TAB_LEVELS[location.level], location.tab);
85
+ }
86
+ const renderSpans = (tabs, activeId) => {
87
+ const spans = [];
88
+ let start = 0;
89
+ tabs.forEach((tab, index) => {
90
+ if (index > 0)
91
+ start += displayWidth(SEPARATOR);
92
+ const width = displayWidth(segment(tab, activeId));
93
+ spans.push({ id: tab.id, start, end: start + width });
94
+ start += width;
95
+ });
96
+ return spans;
97
+ };
98
+ /**
99
+ * One level's own hit map, built from the SAME `segment`/`SEPARATOR` that level's
100
+ * rendered bar is joined from — so the layout and the click mapping are two
101
+ * readings of one expression and cannot drift apart. That is the argument
102
+ * `lib/hit-test.ts` makes for the body rows, applied to a column axis.
103
+ *
104
+ * Offsets are display columns, which is the unit a mouse report carries. For the
105
+ * current ASCII labels that coincides with string offsets, which is what lets
106
+ * the test slice `topTabBar()` with a span and compare.
107
+ *
108
+ * GAIA-239: the LEVEL is part of the signature, exactly as it is for
109
+ * {@link topTabBar}, and for the same reason twice over. GAIA-241 shipped one
110
+ * `topTabSpans(activeId)` over the single flat bar; read level-blind it would
111
+ * publish the project bar's three spans on the root screen too, so a click at
112
+ * the column where `Ticket-Agents` used to sit would open a tab that screen does
113
+ * not draw — the very defect this ticket removes, arriving through the mouse.
114
+ * A map and a bar of two different levels is now a TS2345, not a silent
115
+ * mis-click.
116
+ */
117
+ export function topTabSpans(level, activeId) {
118
+ return renderSpans(TAB_LEVELS[level], activeId);
119
+ }
120
+ /**
121
+ * The same hit map, for a caller holding a whole {@link TabLocation} — the
122
+ * companion of {@link tabBarFor}, and the pairing it needs was already checked
123
+ * when the location was built. Keeping the two in step is what makes "the bar you
124
+ * see is the bar you click" true by construction: both read `location`.
125
+ */
126
+ export function tabSpansFor(location) {
127
+ return renderSpans(TAB_LEVELS[location.level], location.tab);
13
128
  }
@@ -0,0 +1,28 @@
1
+ import type { DropshClient } from '../../../types.js';
2
+ import { type ListOutcome, type OptionClients } from '../../transport.js';
3
+ /** One conductor of the control plane, as this renderer needs it. */
4
+ export interface Conductor {
5
+ id: string;
6
+ /** Never empty — the uuid is the last fallback, so nothing renders as blank. */
7
+ label: string;
8
+ /** The machine the conductor runs on. */
9
+ machineId: string | undefined;
10
+ /** Liveness, as the control plane reports it. */
11
+ status: string | undefined;
12
+ /** The conductor's project, `null` when unlinked. */
13
+ projectId: string | null;
14
+ }
15
+ /**
16
+ * The conductor collection through one client, in server order.
17
+ *
18
+ * Yields the OUTCOME rather than a swallowed array (GAIA-257 D4) — see the same
19
+ * note on `listUsers`. `listConductorsPreferringWrite` keeps the swallowing
20
+ * contract, because it already answers a failure by falling back.
21
+ */
22
+ export declare const listConductors: (client: DropshClient) => Promise<ListOutcome<Conductor>>;
23
+ /**
24
+ * The same collection read through the WRITE client where there is one — still just
25
+ * reading, but the write scope is the one that decides what a write may reference,
26
+ * and `pm` sees the FULL conductor set where `session` sees a subset.
27
+ */
28
+ export declare const listConductorsPreferringWrite: (clients: OptionClients) => Promise<Conductor[]>;
@@ -0,0 +1,23 @@
1
+ import { listOutcome, listPreferringWrite, } from '../../transport.js';
2
+ const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
3
+ const toConductor = (r) => ({
4
+ id: r.id,
5
+ label: str(r.attr('label')) ?? str(r.attr('machine_id')) ?? r.id,
6
+ machineId: str(r.attr('machine_id')),
7
+ status: str(r.attr('status')),
8
+ projectId: r.rel('project_id'),
9
+ });
10
+ /**
11
+ * The conductor collection through one client, in server order.
12
+ *
13
+ * Yields the OUTCOME rather than a swallowed array (GAIA-257 D4) — see the same
14
+ * note on `listUsers`. `listConductorsPreferringWrite` keeps the swallowing
15
+ * contract, because it already answers a failure by falling back.
16
+ */
17
+ export const listConductors = (client) => listOutcome(client, 'gaia_conductor', toConductor);
18
+ /**
19
+ * The same collection read through the WRITE client where there is one — still just
20
+ * reading, but the write scope is the one that decides what a write may reference,
21
+ * and `pm` sees the FULL conductor set where `session` sees a subset.
22
+ */
23
+ export const listConductorsPreferringWrite = (clients) => listPreferringWrite(clients, 'gaia_conductor', toConductor);
@@ -0,0 +1,10 @@
1
+ /** The derived liveness state of a conductor. */
2
+ export type ConductorStatus = 'running' | 'stale' | 'offline' | 'registry-only';
3
+ /** The raw liveness inputs derived from a `gaia_conductor` record. */
4
+ export interface ConductorInput {
5
+ present?: boolean;
6
+ status?: string | null;
7
+ lastSeen?: number | null;
8
+ leaseExpiresAt?: number | null;
9
+ }
10
+ export declare function deriveConductorStatus(conductor: ConductorInput, nowSeconds: number): ConductorStatus;
@@ -0,0 +1,25 @@
1
+ // Conductor liveness derivation. Mirrors the server-side ConductorDeriver
2
+ // (web/modules/custom/gaia_core/modules/gaia_dashboard/src/Dashboard/ConductorDeriver.php)
3
+ // and the CLI `classify()` in gaia-cli/conductor/src/commands/conductor.ts:
4
+ // no gaia_conductor record → registry-only; status 'offline' → offline; else
5
+ // stale when last_seen older than 600s OR the lease has expired; else running.
6
+ const STALE_WINDOW_SECONDS = 600;
7
+ export function deriveConductorStatus(conductor, nowSeconds) {
8
+ if (!conductor?.present)
9
+ return 'registry-only';
10
+ if (conductor.status === 'offline')
11
+ return 'offline';
12
+ if (conductor.lastSeen == null)
13
+ return 'stale';
14
+ if (nowSeconds - conductor.lastSeen > STALE_WINDOW_SECONDS)
15
+ return 'stale';
16
+ if (conductor.leaseExpiresAt != null &&
17
+ conductor.leaseExpiresAt < nowSeconds) {
18
+ return 'stale';
19
+ }
20
+ return 'running';
21
+ }
22
+ // `humanizeAge` moved to `lib/format.ts` (GAIA-236, entity-slice Phase 0): it is a
23
+ // generic relative-time formatter with no conductor vocabulary, and keeping it here
24
+ // is what made `Component/comment-item` and `Deriver/run-status` import a CONDUCTOR
25
+ // module. Its test moved with it, to `tests/lib/format.test.ts`.
@@ -0,0 +1,8 @@
1
+ /** The minimum shape a dashboard row needs to render. */
2
+ export interface DashboardRowInput {
3
+ project: string;
4
+ derivedStatus: string;
5
+ ageLabel: string;
6
+ }
7
+ /** One line per project: name, derived status, relative age. */
8
+ export declare function dashboardRows(projects: DashboardRowInput[]): string[];
@@ -0,0 +1,7 @@
1
+ import { pad } from '../../../../lib/format.js';
2
+ /** One line per project: name, derived status, relative age. */
3
+ export function dashboardRows(projects) {
4
+ if (projects.length === 0)
5
+ return ['No projects.'];
6
+ return projects.map((p) => `${pad(p.project, 24)} ${pad(p.derivedStatus, 14)} ${p.ageLabel}`);
7
+ }
@@ -0,0 +1,8 @@
1
+ /** The minimum shape a project row needs to render. */
2
+ export interface ProjectRowInput {
3
+ project: string;
4
+ derivedStatus: string;
5
+ ageLabel: string;
6
+ }
7
+ /** One line per project: name, derived status, relative age. */
8
+ export declare function projectRows(projects: ProjectRowInput[]): string[];
@@ -0,0 +1,7 @@
1
+ import { pad } from '../../../../lib/format.js';
2
+ /** One line per project: name, derived status, relative age. */
3
+ export function projectRows(projects) {
4
+ if (projects.length === 0)
5
+ return ['No projects.'];
6
+ return projects.map((p) => `${pad(p.project, 24)} ${pad(p.derivedStatus, 14)} ${p.ageLabel}`);
7
+ }
@@ -0,0 +1,15 @@
1
+ /** A project that claims the repo. */
2
+ export interface PickerCandidate {
3
+ id: string;
4
+ name: string;
5
+ }
6
+ /**
7
+ * The picker screen's rows: a heading naming the repo, then one row per
8
+ * candidate with `▸` on the selected one.
9
+ */
10
+ export declare function projectPickerRows(candidates: PickerCandidate[], repo: string, selected: number): string[];
11
+ /**
12
+ * The non-TTY form (spec Decision 11): the candidate list plus the direct
13
+ * command, printed once so `gaia ui --here | cat` stays scriptable — no prompt.
14
+ */
15
+ export declare function projectPickerHint(candidates: PickerCandidate[], repo: string): string;
@@ -0,0 +1,42 @@
1
+ // GAIA-223 AC-8 (spec Decision 14) — the picker `gaia ui --here` shows when the
2
+ // cwd's repo is claimed by MORE THAN ONE gaia project.
3
+ //
4
+ // One repo mapping to several projects is designed for, not an edge case:
5
+ // GAIA-126/137 let `.gaia/` hold one conductor config per project, which is how
6
+ // a Drupal multisite repo maps each site to its own GAIA project. Erroring on
7
+ // N>1 would fail permanently, by design, in exactly the repos the feature is
8
+ // most useful in — so the ambiguity is shown rather than resolved behind the
9
+ // user's back.
10
+ //
11
+ // Pure, like every other Component: candidates in, rows out. The kernel owns
12
+ // selection state, Enter and Esc.
13
+ /** Clamp a selection index into the candidate range (0 when there are none). */
14
+ function clamp(selected, length) {
15
+ if (length <= 0)
16
+ return 0;
17
+ return Math.max(0, Math.min(Math.trunc(selected), length - 1));
18
+ }
19
+ /**
20
+ * The picker screen's rows: a heading naming the repo, then one row per
21
+ * candidate with `▸` on the selected one.
22
+ */
23
+ export function projectPickerRows(candidates, repo, selected) {
24
+ const heading = [`Several gaia projects claim ${repo} — which one?`, ''];
25
+ if (candidates.length === 0) {
26
+ return [...heading, 'No projects.'];
27
+ }
28
+ const index = clamp(selected, candidates.length);
29
+ return [
30
+ ...heading,
31
+ ...candidates.map((c, i) => `${i === index ? '▸' : ' '} ${c.name}`),
32
+ ];
33
+ }
34
+ /**
35
+ * The non-TTY form (spec Decision 11): the candidate list plus the direct
36
+ * command, printed once so `gaia ui --here | cat` stays scriptable — no prompt.
37
+ */
38
+ export function projectPickerHint(candidates, repo) {
39
+ const names = candidates.map((c) => ` ${c.name}`).join('\n');
40
+ return (`Several gaia projects claim ${repo}:\n${names}\n` +
41
+ "Open one directly with 'gaia ui project:<name>'.");
42
+ }
@@ -0,0 +1,21 @@
1
+ import type { DropshClient } from '../../../types.js';
2
+ import { type ConductorStatus } from '../../conductor/Data/liveness.js';
3
+ /** A derived project row: one distinct project + its liveliest conductor. */
4
+ export interface ProjectRow {
5
+ project: string;
6
+ derivedStatus: ConductorStatus;
7
+ lastSeen: number | null;
8
+ ageLabel: string;
9
+ }
10
+ /**
11
+ * Resolve the projects screen's rows straight from the control plane: list the
12
+ * `gaia_conductor` entities (read is owner-gated, so this is already scoped to
13
+ * the current user's conductors — no local registry needed), sideload their
14
+ * `project_id`, group by project name, and report each project's liveliest
15
+ * conductor (running > stale > offline). Everything is a Drupal entity read
16
+ * through `services.client` (AC-5); the screen itself stays a screen/route.
17
+ */
18
+ export declare function loadProjectRows({ client, nowSeconds, }: {
19
+ client: DropshClient;
20
+ nowSeconds: () => number;
21
+ }): Promise<ProjectRow[]>;
@@ -0,0 +1,81 @@
1
+ import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
2
+ import { humanizeAge } from '../../../lib/format.js';
3
+ import { deriveConductorStatus, } from '../../conductor/Data/liveness.js';
4
+ /** JSON:API timestamps arrive as ISO-8601 strings; liveness works in seconds. */
5
+ function toEpochSeconds(value) {
6
+ if (value == null)
7
+ return null;
8
+ if (typeof value === 'number')
9
+ return value;
10
+ if (typeof value !== 'string')
11
+ return null;
12
+ const ms = Date.parse(value);
13
+ return Number.isNaN(ms) ? null : Math.floor(ms / 1000);
14
+ }
15
+ /** Id of a single-valued relationship linkage (`data` object), or null. */
16
+ function singleRelId(rel) {
17
+ const data = rel?.data;
18
+ if (!data || Array.isArray(data))
19
+ return null;
20
+ return data.id ?? null;
21
+ }
22
+ // Most-alive-first ranking so a project with several conductors reports its
23
+ // liveliest one, not whichever the collection happened to return first.
24
+ const STATUS_RANK = {
25
+ running: 0,
26
+ stale: 1,
27
+ offline: 2,
28
+ 'registry-only': 3,
29
+ };
30
+ /**
31
+ * Resolve the projects screen's rows straight from the control plane: list the
32
+ * `gaia_conductor` entities (read is owner-gated, so this is already scoped to
33
+ * the current user's conductors — no local registry needed), sideload their
34
+ * `project_id`, group by project name, and report each project's liveliest
35
+ * conductor (running > stale > offline). Everything is a Drupal entity read
36
+ * through `services.client` (AC-5); the screen itself stays a screen/route.
37
+ */
38
+ export async function loadProjectRows({ client, nowSeconds, }) {
39
+ const params = new DrupalJsonApiParams().addInclude(['project_id']);
40
+ let doc;
41
+ try {
42
+ doc = (await client.get('gaia_conductor/gaia_conductor', params));
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ // Resolve each conductor's project name from the sideloaded gaia_project.
48
+ const projectNameById = new Map();
49
+ for (const inc of doc.included ?? []) {
50
+ if (inc.id && typeof inc.attributes?.name === 'string') {
51
+ projectNameById.set(inc.id, inc.attributes.name);
52
+ }
53
+ }
54
+ const now = nowSeconds();
55
+ const best = new Map();
56
+ for (const c of doc.data ?? []) {
57
+ const a = c.attributes ?? {};
58
+ const projectId = singleRelId(c.relationships?.project_id);
59
+ const project = projectId ? projectNameById.get(projectId) : undefined;
60
+ // A conductor whose project cannot be resolved is not a project row.
61
+ if (!project)
62
+ continue;
63
+ const lastSeen = toEpochSeconds(a.last_seen ?? null);
64
+ const derivedStatus = deriveConductorStatus({
65
+ present: true,
66
+ status: typeof a.status === 'string' ? a.status : null,
67
+ lastSeen,
68
+ leaseExpiresAt: toEpochSeconds(a.lease_expires_at ?? null),
69
+ }, now);
70
+ const prev = best.get(project);
71
+ if (!prev || STATUS_RANK[derivedStatus] < STATUS_RANK[prev.derivedStatus]) {
72
+ best.set(project, { derivedStatus, lastSeen });
73
+ }
74
+ }
75
+ return [...best.entries()].map(([project, b]) => ({
76
+ project,
77
+ derivedStatus: b.derivedStatus,
78
+ lastSeen: b.lastSeen,
79
+ ageLabel: humanizeAge(b.lastSeen, now),
80
+ }));
81
+ }
@@ -0,0 +1,20 @@
1
+ import type { DropshClient } from '../../../types.js';
2
+ import { type TicketRow } from '../../ticket/Data/tickets.js';
3
+ /** The resolved project header + its ticket rows. */
4
+ export interface ProjectWithTickets {
5
+ project: {
6
+ id: string;
7
+ name: string;
8
+ slug: string;
9
+ };
10
+ tickets: TicketRow[];
11
+ }
12
+ /**
13
+ * Resolve a `gaia_project` by its slug (the registry `project`, which matches
14
+ * the project's `name`) and list its tickets, most-recently-changed first.
15
+ *
16
+ * The ticket listing delegates to {@link loadFilteredTickets} with empty
17
+ * criteria — one include-aware `client.get` path shared with the search/filter
18
+ * load, so both resolve parent/conductor identically (no dual-mapper drift).
19
+ */
20
+ export declare function loadProjectWithTickets(client: DropshClient, projectSlug: string): Promise<ProjectWithTickets>;
@@ -0,0 +1,36 @@
1
+ // Resolving ONE project by slug together with its ticket rows. The project half of
2
+ // the former `Service/project-data.ts` (GAIA-236 phase 3).
3
+ //
4
+ // This is one of exactly two enumerated `project/Data -> ticket/Data` edges the
5
+ // boundary guard allows: the ticket listing deliberately delegates to
6
+ // `loadFilteredTickets` so the search/filter path and this one resolve
7
+ // parent/conductor through the same mapper, with no dual-mapper drift.
8
+ import { loadFilteredTickets, } from '../../ticket/Data/tickets.js';
9
+ /**
10
+ * Resolve a `gaia_project` by its slug (the registry `project`, which matches
11
+ * the project's `name`) and list its tickets, most-recently-changed first.
12
+ *
13
+ * The ticket listing delegates to {@link loadFilteredTickets} with empty
14
+ * criteria — one include-aware `client.get` path shared with the search/filter
15
+ * load, so both resolve parent/conductor identically (no dual-mapper drift).
16
+ */
17
+ export async function loadProjectWithTickets(client, projectSlug) {
18
+ const projectRes = await client
19
+ .collection('gaia_project')
20
+ .where('name', '=', projectSlug)
21
+ .first();
22
+ const projectId = projectRes?.id ?? projectSlug;
23
+ const tickets = await loadFilteredTickets(client, projectId, {
24
+ query: '',
25
+ state: null,
26
+ type: null,
27
+ });
28
+ return {
29
+ project: {
30
+ id: projectId,
31
+ name: projectRes?.attr('name') ?? projectSlug,
32
+ slug: projectSlug,
33
+ },
34
+ tickets,
35
+ };
36
+ }
@@ -0,0 +1,17 @@
1
+ import type { Section } from '../../../../Component/section-list/index.js';
2
+ import type { JsonApiResource, TicketDetailDocument } from '../../../../types.js';
3
+ /** The sideloaded `gaia_run` with this id, or undefined. */
4
+ export declare function runResource(document: TicketDetailDocument, runId: string | null | undefined): JsonApiResource | undefined;
5
+ /** `#<id> · <ticket> · <state>` — the run screen's header-box title row. */
6
+ export declare function runDetailTitle(document: TicketDetailDocument, runId: string | null | undefined): string;
7
+ /**
8
+ * The run's sections, in the order the screen draws them. An unresolvable run
9
+ * yields ONE section saying so, rather than an empty screen that looks like a
10
+ * failed render.
11
+ */
12
+ export declare function runDetailSections(document: TicketDetailDocument, runId: string | null | undefined): Section[];
13
+ /** The run block flattened — the screen's body, and the piped render (D7). */
14
+ export declare function runDetailLines(document: TicketDetailDocument, runId: string | null | undefined): {
15
+ lines: string[];
16
+ boldRows: Set<number>;
17
+ };