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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/dist/src/Agent/launch.d.ts +4 -4
  2. package/dist/src/Agent/launch.js +14 -7
  3. package/dist/src/Component/section-list/index.d.ts +117 -8
  4. package/dist/src/Component/section-list/index.js +118 -46
  5. package/dist/src/Component/stats-panel/index.d.ts +17 -5
  6. package/dist/src/Component/stats-panel/index.js +50 -52
  7. package/dist/src/Component/top-tabs/index.d.ts +95 -23
  8. package/dist/src/Component/top-tabs/index.js +87 -41
  9. package/dist/src/Entity/collection.d.ts +279 -0
  10. package/dist/src/Entity/collection.js +377 -0
  11. package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +25 -0
  12. package/dist/src/Entity/conductor/Component/conductor-list/index.js +60 -0
  13. package/dist/src/Entity/conductor/Data/conductors.d.ts +216 -0
  14. package/dist/src/Entity/conductor/Data/conductors.js +280 -0
  15. package/dist/src/Entity/conductor/Data/liveness.d.ts +2 -1
  16. package/dist/src/Entity/conductor/Data/liveness.js +12 -16
  17. package/dist/src/Entity/conductor/Data/log-tail.d.ts +25 -0
  18. package/dist/src/Entity/conductor/Data/log-tail.js +133 -0
  19. package/dist/src/Entity/conductor/Data/own-conductor.d.ts +30 -0
  20. package/dist/src/Entity/conductor/Data/own-conductor.js +73 -0
  21. package/dist/src/Entity/conductor/Data/process.d.ts +78 -0
  22. package/dist/src/Entity/conductor/Data/process.js +101 -0
  23. package/dist/src/Entity/conductor/Screen/conductors.d.ts +67 -0
  24. package/dist/src/Entity/conductor/Screen/conductors.js +233 -0
  25. package/dist/src/Entity/cycle.d.ts +18 -0
  26. package/dist/src/Entity/cycle.js +30 -0
  27. package/dist/src/Entity/project/Data/project.d.ts +8 -0
  28. package/dist/src/Entity/project/Data/project.js +12 -0
  29. package/dist/src/Entity/project/Data/projects.d.ts +121 -0
  30. package/dist/src/Entity/project/Data/projects.js +115 -0
  31. package/dist/src/Entity/project/Data/with-tickets.d.ts +5 -0
  32. package/dist/src/Entity/project/Data/with-tickets.js +15 -13
  33. package/dist/src/Entity/project/Screen/project-picker.d.ts +80 -0
  34. package/dist/src/Entity/project/Screen/project-picker.js +191 -0
  35. package/dist/src/Entity/project/Screen/projects-screen.d.ts +52 -0
  36. package/dist/src/Entity/project/Screen/projects-screen.js +122 -0
  37. package/dist/src/Entity/run/Component/run-detail/index.d.ts +26 -10
  38. package/dist/src/Entity/run/Component/run-detail/index.js +147 -39
  39. package/dist/src/Entity/run/Component/run-table/index.d.ts +5 -7
  40. package/dist/src/Entity/run/Component/run-table/index.js +9 -18
  41. package/dist/src/Entity/run/Data/query.d.ts +22 -0
  42. package/dist/src/Entity/run/Data/query.js +41 -0
  43. package/dist/src/Entity/run/Data/runs.d.ts +192 -4
  44. package/dist/src/Entity/run/Data/runs.js +188 -27
  45. package/dist/src/Entity/run/Screen/run-detail.d.ts +53 -0
  46. package/dist/src/Entity/run/Screen/run-detail.js +70 -0
  47. package/dist/src/Entity/run/Screen/runs.d.ts +86 -0
  48. package/dist/src/Entity/run/Screen/runs.js +169 -0
  49. package/dist/src/Entity/statistics/Screen/dashboard.d.ts +78 -0
  50. package/dist/src/Entity/statistics/Screen/dashboard.js +228 -0
  51. package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +12 -9
  52. package/dist/src/Entity/ticket/Component/comment-item/index.js +18 -13
  53. package/dist/src/Entity/ticket/Component/create-form/index.d.ts +16 -3
  54. package/dist/src/Entity/ticket/Component/create-form/index.js +28 -14
  55. package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +11 -6
  56. package/dist/src/Entity/ticket/Component/edit-form/index.js +77 -29
  57. package/dist/src/Entity/ticket/Component/sections/index.d.ts +8 -0
  58. package/dist/src/Entity/ticket/Component/sections/index.js +42 -0
  59. package/dist/src/Entity/ticket/Component/tab-bar/index.js +6 -0
  60. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +65 -8
  61. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +171 -84
  62. package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +4 -2
  63. package/dist/src/Entity/ticket/Component/ticket-table/index.js +8 -12
  64. package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +47 -3
  65. package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -22
  66. package/dist/src/Entity/ticket/Data/parents.d.ts +13 -1
  67. package/dist/src/Entity/ticket/Data/parents.js +64 -12
  68. package/dist/src/Entity/ticket/Data/query.d.ts +44 -7
  69. package/dist/src/Entity/ticket/Data/query.js +60 -26
  70. package/dist/src/Entity/ticket/Data/tickets.d.ts +170 -4
  71. package/dist/src/Entity/ticket/Data/tickets.js +200 -12
  72. package/dist/src/Entity/ticket/Data/url.d.ts +16 -0
  73. package/dist/src/Entity/ticket/Data/url.js +20 -0
  74. package/dist/src/Entity/ticket/Data/workflow.d.ts +1 -5
  75. package/dist/src/Entity/ticket/Data/workflow.js +5 -13
  76. package/dist/src/Entity/ticket/Data/write.d.ts +32 -0
  77. package/dist/src/Entity/ticket/Data/write.js +80 -0
  78. package/dist/src/Entity/ticket/Form/create-data.d.ts +1 -11
  79. package/dist/src/Entity/ticket/Form/create-data.js +7 -36
  80. package/dist/src/Entity/ticket/Form/create.d.ts +45 -5
  81. package/dist/src/Entity/ticket/Form/create.js +45 -8
  82. package/dist/src/Entity/ticket/Form/edit-data.d.ts +1 -1
  83. package/dist/src/Entity/ticket/Form/edit-data.js +4 -0
  84. package/dist/src/Entity/ticket/Form/edit.d.ts +79 -12
  85. package/dist/src/Entity/ticket/Form/edit.js +165 -18
  86. package/dist/src/Entity/ticket/Form/keys.d.ts +7 -0
  87. package/dist/src/Entity/ticket/Form/keys.js +7 -0
  88. package/dist/src/Entity/ticket/Form/parent-filter.d.ts +36 -0
  89. package/dist/src/Entity/ticket/Form/parent-filter.js +37 -0
  90. package/dist/src/Entity/ticket/Form/parent-source.d.ts +27 -0
  91. package/dist/src/Entity/ticket/Form/parent-source.js +56 -0
  92. package/dist/src/Entity/ticket/Form/who-filter.d.ts +33 -0
  93. package/dist/src/Entity/ticket/Form/who-filter.js +33 -0
  94. package/dist/src/Entity/ticket/Form/who-source.d.ts +18 -0
  95. package/dist/src/Entity/ticket/Form/who-source.js +38 -0
  96. package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +81 -0
  97. package/dist/src/Entity/ticket/Screen/create-ticket-form.js +356 -0
  98. package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +92 -0
  99. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +220 -0
  100. package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +44 -0
  101. package/dist/src/Entity/ticket/Screen/parent-filter-form.js +200 -0
  102. package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +136 -0
  103. package/dist/src/Entity/ticket/Screen/ticket-agents.js +257 -0
  104. package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +125 -0
  105. package/dist/src/Entity/ticket/Screen/ticket-detail.js +415 -0
  106. package/dist/src/Entity/ticket/Screen/tickets.d.ts +145 -0
  107. package/dist/src/Entity/ticket/Screen/tickets.js +538 -0
  108. package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +41 -0
  109. package/dist/src/Entity/ticket/Screen/who-filter-form.js +174 -0
  110. package/dist/src/Entity/user/Data/users.d.ts +17 -0
  111. package/dist/src/Entity/user/Data/users.js +66 -6
  112. package/dist/src/Form/field.d.ts +3 -1
  113. package/dist/src/Form/field.js +1 -0
  114. package/dist/src/Form/form.d.ts +1 -1
  115. package/dist/src/Form/form.js +61 -9
  116. package/dist/src/Form/keys.d.ts +23 -1
  117. package/dist/src/Form/keys.js +78 -23
  118. package/dist/src/Kernel/chrome.d.ts +289 -0
  119. package/dist/src/Kernel/chrome.js +404 -0
  120. package/dist/src/Kernel/clipboard.d.ts +30 -0
  121. package/dist/src/Kernel/clipboard.js +51 -0
  122. package/dist/src/Kernel/key-input.d.ts +50 -0
  123. package/dist/src/Kernel/key-input.js +158 -0
  124. package/dist/src/Kernel/mount.d.ts +187 -0
  125. package/dist/src/Kernel/mount.js +399 -0
  126. package/dist/src/Kernel/tui-kernel.d.ts +0 -11
  127. package/dist/src/Kernel/tui-kernel.js +778 -2367
  128. package/dist/src/Routing/context.d.ts +31 -0
  129. package/dist/src/Routing/context.js +52 -0
  130. package/dist/src/Routing/registry.d.ts +19 -0
  131. package/dist/src/Routing/registry.js +97 -0
  132. package/dist/src/Routing/route.d.ts +35 -0
  133. package/dist/src/Routing/route.js +1 -0
  134. package/dist/src/Routing/router.d.ts +49 -0
  135. package/dist/src/Routing/router.js +123 -0
  136. package/dist/src/Routing/routes.d.ts +17 -0
  137. package/dist/src/Routing/routes.js +57 -0
  138. package/dist/src/Screen/async-screen.d.ts +87 -0
  139. package/dist/src/Screen/async-screen.js +206 -0
  140. package/dist/src/Screen/list-screen.d.ts +193 -0
  141. package/dist/src/Screen/list-screen.js +261 -0
  142. package/dist/src/Screen/screen.d.ts +240 -0
  143. package/dist/src/Screen/screen.js +34 -0
  144. package/dist/src/Tui/controller.d.ts +13 -0
  145. package/dist/src/Tui/controller.js +1794 -0
  146. package/dist/src/Widget/autocomplete.js +75 -3
  147. package/dist/src/Widget/index.d.ts +3 -2
  148. package/dist/src/Widget/index.js +2 -1
  149. package/dist/src/Widget/option-source.d.ts +12 -1
  150. package/dist/src/Widget/option-source.js +14 -1
  151. package/dist/src/Widget/render.d.ts +23 -8
  152. package/dist/src/Widget/render.js +108 -41
  153. package/dist/src/Widget/text.js +94 -5
  154. package/dist/src/Widget/textarea.d.ts +45 -0
  155. package/dist/src/Widget/textarea.js +115 -0
  156. package/dist/src/Widget/types.d.ts +64 -1
  157. package/dist/src/launcher.d.ts +9 -2
  158. package/dist/src/launcher.js +2 -1
  159. package/dist/src/lib/ansi.js +13 -5
  160. package/dist/src/lib/clipboard.d.ts +41 -0
  161. package/dist/src/lib/clipboard.js +65 -0
  162. package/dist/src/lib/cursor.d.ts +31 -0
  163. package/dist/src/lib/cursor.js +45 -0
  164. package/dist/src/lib/format.d.ts +21 -13
  165. package/dist/src/lib/format.js +76 -28
  166. package/dist/src/lib/hyperlink.d.ts +58 -0
  167. package/dist/src/lib/hyperlink.js +104 -0
  168. package/dist/src/lib/input-trace.d.ts +48 -0
  169. package/dist/src/lib/input-trace.js +47 -0
  170. package/dist/src/lib/key-vocabulary.d.ts +19 -0
  171. package/dist/src/lib/key-vocabulary.js +25 -0
  172. package/dist/src/lib/table.d.ts +37 -0
  173. package/dist/src/lib/table.js +20 -0
  174. package/dist/src/lib/vnode.d.ts +217 -0
  175. package/dist/src/lib/vnode.js +456 -0
  176. package/dist/src/lib/width.d.ts +25 -1
  177. package/dist/src/lib/width.js +93 -12
  178. package/dist/src/plugin.js +26 -1
  179. package/dist/src/types.d.ts +67 -2
  180. package/dist/src/types.js +0 -4
  181. package/package.json +4 -2
@@ -1,5 +1,6 @@
1
1
  import type { DropshClient } from '../../../types.js';
2
2
  import { type ListOutcome, type OptionClients } from '../../transport.js';
3
+ import { type ConductorStatus } from './liveness.js';
3
4
  /** One conductor of the control plane, as this renderer needs it. */
4
5
  export interface Conductor {
5
6
  id: string;
@@ -26,3 +27,218 @@ export declare const listConductors: (client: DropshClient) => Promise<ListOutco
26
27
  * and `pm` sees the FULL conductor set where `session` sees a subset.
27
28
  */
28
29
  export declare const listConductorsPreferringWrite: (clients: OptionClients) => Promise<Conductor[]>;
30
+ /** A conductor as the Conductors screen renders it. */
31
+ export interface ConductorRow extends Conductor {
32
+ /** The `.gaia` dir it last registered from; '' when it never reported one. */
33
+ workspaceRoot: string;
34
+ /** Unix seconds of the last heartbeat; 0 when it never reported one. */
35
+ lastSeen: number;
36
+ /** Unix seconds the lease runs out; 0 when unset. */
37
+ leaseExpiresAt: number;
38
+ currentLoad: number;
39
+ maxParallel: number;
40
+ /** The pid its serve loop last reported, if any. */
41
+ lastPid: number | undefined;
42
+ /** The config stem it was started from, as `--conductor <name>` takes it. */
43
+ conductorName: string | undefined;
44
+ /** Liveness, derived — never the raw `status`. */
45
+ derivedStatus: ConductorStatus;
46
+ /**
47
+ * Whether this conductor's workspace exists on THIS machine.
48
+ *
49
+ * The control plane is user-scoped, not machine-scoped, so a user's other
50
+ * machines' conductors appear in this list. They are shown rather than hidden
51
+ * — filtering by machine_id would mean substring-matching a composed string —
52
+ * but only a local row can be started or stopped from here, because a pid
53
+ * from another machine means nothing on this one.
54
+ */
55
+ isLocal: boolean;
56
+ }
57
+ /** What a conductor ROW cannot know on its own, handed to the columns per frame. */
58
+ export interface ConductorListContext {
59
+ /** The clock the AGE column reads — one `now` for the whole frame. */
60
+ readonly nowSeconds: number;
61
+ /** The conductors with an action in flight, so a second keypress reads as
62
+ * visibly redundant. */
63
+ readonly busyIds: ReadonlySet<string>;
64
+ /** Project uuid → name, from the screen's own small read. */
65
+ readonly projectNames: ReadonlyMap<string, string>;
66
+ }
67
+ /**
68
+ * The conductors list, declared once (GAIA-345 D2/D10) — its eight columns, the
69
+ * noun every one of its node ids is prefixed with, and what it says when it is
70
+ * empty.
71
+ *
72
+ * It GAINED A HEADER with this ticket (D6). This is the list a header earns its
73
+ * keep in most: eight columns of ids, paths and numbers are not self-describing
74
+ * the way three named ones were. The consequence is stated because it is a
75
+ * behaviour change — the list grows one row at the top and its selection
76
+ * highlight moves down one physical row. Nothing computes that offset:
77
+ * selection is a node attribute and the cursor is found through `rowIds`, both
78
+ * of which GAIA-340 made position-free.
79
+ *
80
+ * `fields` names what each column is built from, as everywhere. This loader
81
+ * reads the whole `gaia_conductor` collection with no sparse fieldset, so there
82
+ * is no `AssertExhaustive` proof binding these — declaring them anyway keeps
83
+ * that a fact about this loader rather than a hole in the column model.
84
+ *
85
+ * WHICH COLUMNS GROW, AND WHY THOSE TWO. The rule is the operator's and it is
86
+ * about the CONTENT: a free-text column grows. `NAME` and `PROJECT` are the two
87
+ * here — a conductor's label and a project's name are operator-written prose
88
+ * with no vocabulary and no shared head, so both benefit from a surplus and
89
+ * neither loses anything to Yoga's right-hand cut. They SHARE it through
90
+ * `grow`, which is what the mechanism exists for, and the shares are 2 : 1 in
91
+ * `NAME`'s favour: the label is the row's identity and the column an operator
92
+ * reads first, while the project name is a secondary attribute that repeats
93
+ * across every row of one project. `PROJECT` keeps its 14 as its BASIS rather
94
+ * than as a bound, so a narrow terminal leaves it exactly the columns it had
95
+ * before this ticket and a wide one hands it a share.
96
+ *
97
+ * The other six do not grow, and each for its own reason. `MACHINE` and
98
+ * `WORKSPACE` are free text too, and are the ONE exception to the rule: they
99
+ * are elided from the LEFT (see {@link elideLeft}), which needs a declared
100
+ * width to cut against and keeps the distinguishing tail — a grower would hand
101
+ * the cut back to the renderer's right-hand clip and lose exactly what that
102
+ * elision preserves. `STATUS`, `AGE` and `PID` are closed vocabularies that
103
+ * cannot use a surplus at all. `MARKS` is a closed vocabulary too and keeps a
104
+ * real budget for a measured reason: `width: 0, grow: 0` collapses it to zero
105
+ * columns and the mark vanishes.
106
+ */
107
+ export declare const CONDUCTOR_TABLE: {
108
+ readonly noun: "conductor";
109
+ readonly emptyMessage: "No conductors on this control plane.";
110
+ readonly columns: readonly [{
111
+ /**
112
+ * A GROWER — free text, right-truncated, and the column an operator reads
113
+ * first. Declared FIRST and growing all the same: `flexGrow` is a
114
+ * property of the cell, not of its position in the row (measured, spec
115
+ * R2). Its `grow: 2` against `PROJECT`'s 1 is the identity column's larger
116
+ * claim on the surplus, argued above.
117
+ *
118
+ * `min: 20` is the ONE floor on this list, and it is preservation rather
119
+ * than invention: 20 is the budget this column has carried since
120
+ * GAIA-232, and without it a narrow terminal squeezes the grower to
121
+ * nothing while seven fixed budgets keep theirs — measured at 80 columns,
122
+ * NAME came out ONE column wide and the label read `c…`. The other seven
123
+ * columns need 106 columns between them, so below ~126 the row runs past
124
+ * the right edge and the renderer clips it there. That is exactly what
125
+ * this list did before this ticket, at every width; the floor keeps the
126
+ * narrow case as it was and lets the wide case improve.
127
+ */
128
+ readonly key: "name";
129
+ readonly label: "NAME";
130
+ readonly width: 0;
131
+ readonly min: 20;
132
+ readonly grow: 2;
133
+ readonly fields: readonly ["label", "machine_id"];
134
+ readonly get: (row: ConductorRow) => string;
135
+ }, {
136
+ readonly key: "machine";
137
+ readonly label: "MACHINE";
138
+ readonly width: 20;
139
+ readonly fields: readonly ["machine_id"];
140
+ readonly get: (row: ConductorRow) => string;
141
+ }, {
142
+ /**
143
+ * A project NAME, so free text, so a GROWER — the operator's correction to
144
+ * Task 6, which shipped this column fixed at 14 and named the cut an
145
+ * accepted residual. Measured at 160 columns with that configuration: 15
146
+ * columns unused at the right edge while an ordinary project name was cut
147
+ * to `keytec-gaia-p…`, which is finding 3 of this very ticket reproduced
148
+ * one column further left.
149
+ *
150
+ * The 14 stays as the flex BASIS, not as a bound: it is what the column
151
+ * starts from and what a terminal too narrow to give anything leaves it,
152
+ * so the narrow case is unchanged and only the wide one improves. No
153
+ * `min`, deliberately — without one the cell keeps `flexShrink: 0` and an
154
+ * overflowing row takes its columns from `NAME`, whose `min: 20` is the
155
+ * floor this list has always had.
156
+ */
157
+ readonly key: "project";
158
+ readonly label: "PROJECT";
159
+ readonly width: 14;
160
+ readonly grow: 1;
161
+ readonly fields: readonly ["project_id"];
162
+ readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
163
+ }, {
164
+ /** A CLOSED VOCABULARY. `ConductorStatus`'s reachable members here are
165
+ * `running | stale | offline` — this screen can never produce
166
+ * `registry-only`, because every row IS a control-plane record — whose
167
+ * longest is 7, so 9 shows each of them whole. */
168
+ readonly key: "status";
169
+ readonly label: "STATUS";
170
+ readonly width: 9;
171
+ readonly fields: readonly ["status", "lease_expires_at", "last_seen"];
172
+ readonly get: (row: ConductorRow) => ConductorStatus;
173
+ }, {
174
+ /** `humanizeAge`'s own vocabulary, exactly as on the projects list: `—`,
175
+ * `just now`, `<n>m|h|d ago`. 8 is the longest of them. */
176
+ readonly key: "age";
177
+ readonly label: "AGE";
178
+ readonly width: 8;
179
+ readonly fields: readonly ["last_seen"];
180
+ readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
181
+ }, {
182
+ readonly key: "workspace";
183
+ readonly label: "WORKSPACE";
184
+ readonly width: 24;
185
+ readonly fields: readonly ["workspace_root"];
186
+ readonly get: (row: ConductorRow) => string;
187
+ }, {
188
+ /** A pid is at most 7 digits — Linux's `pid_max` ceiling is 4 194 304 —
189
+ * and a conductor that never reported one shows `-` rather than a
190
+ * fabricated number. */
191
+ readonly key: "pid";
192
+ readonly label: "PID";
193
+ readonly width: 7;
194
+ readonly fields: readonly ["last_pid"];
195
+ readonly get: (row: ConductorRow) => string;
196
+ }, {
197
+ /**
198
+ * A closed vocabulary too, which is why it takes a budget instead of the
199
+ * trailing slot it used to hold: everything it can say is `…`,
200
+ * `elsewhere` or both, and 11 is `… elsewhere`.
201
+ *
202
+ * A non-local conductor is MARKED rather than hidden. The control plane
203
+ * is user-scoped, not machine-scoped, so a user's other machines'
204
+ * conductors are legitimately in this list — and silently dropping them
205
+ * would make the cockpit disagree with `gaia conductor ls` for no visible
206
+ * reason. The mark says why their actions are inert here: a pid from
207
+ * another machine means nothing on this one.
208
+ */
209
+ readonly key: "marks";
210
+ readonly label: "MARKS";
211
+ readonly width: 11;
212
+ readonly fields: readonly ["workspace_root"];
213
+ readonly get: (row: ConductorRow, ctx: ConductorListContext) => string;
214
+ }];
215
+ };
216
+ /** What the row projection needs from the outside world, injected for tests. */
217
+ export interface ConductorRowContext {
218
+ /** Unix seconds; the liveness derivation's clock. */
219
+ now: number;
220
+ /** Whether a path exists on this machine. */
221
+ exists: (path: string) => boolean;
222
+ }
223
+ /**
224
+ * The conductor collection as cockpit rows, in server order.
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.
229
+ */
230
+ export declare const listConductorRows: (client: DropshClient, ctx: ConductorRowContext) => Promise<ConductorRow[]>;
231
+ /**
232
+ * Project uuid → name, for the Conductors screen's project column.
233
+ *
234
+ * A conductor references its project by uuid; the screen shows the name. This
235
+ * is a separate small read rather than a widened `ProjectRow`, because
236
+ * `loadProjectRows` builds the PROJECTS screen (one row per project, derived
237
+ * from conductors) and adding an id to it would change a shape four screens
238
+ * depend on to serve one column.
239
+ *
240
+ * A failed read yields an empty map: an unnamed project renders blank, which is
241
+ * better than a screen that refuses to list conductors because their projects
242
+ * could not be resolved.
243
+ */
244
+ export declare const listProjectNames: (client: DropshClient) => Promise<Map<string, string>>;
@@ -1,4 +1,12 @@
1
+ // GAIA-236 — the CONDUCTOR data service. It returns CONDUCTORS and knows nothing
2
+ // about forms: no `Option`, no project filter, no `(status)` in a label. Those are
3
+ // one popup's rules and live with that popup's adapter (`edit-form-data.ts`); a
4
+ // service that applied them would be unusable for any other purpose. The two read
5
+ // strategies are both just reading, so both live here.
6
+ import { toUnixSeconds } from '@gaia-ai/core';
7
+ import { humanizeAge } from '../../../lib/format.js';
1
8
  import { listOutcome, listPreferringWrite, } from '../../transport.js';
9
+ import { deriveConductorStatus } from './liveness.js';
2
10
  const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
3
11
  const toConductor = (r) => ({
4
12
  id: r.id,
@@ -21,3 +29,275 @@ export const listConductors = (client) => listOutcome(client, 'gaia_conductor',
21
29
  * and `pm` sees the FULL conductor set where `session` sees a subset.
22
30
  */
23
31
  export const listConductorsPreferringWrite = (clients) => listPreferringWrite(clients, 'gaia_conductor', toConductor);
32
+ /**
33
+ * Shorten a value from the LEFT, keeping its end — and the ONE exception to
34
+ * this ticket's "free text grows" rule (GAIA-345 D10).
35
+ *
36
+ * A CONTENT decision, which is why it is a cell accessor and not a layout
37
+ * member: a cell's `truncate` can only drop the RIGHT, and both values it is
38
+ * applied to carry their distinguishing part in the TAIL. A composed
39
+ * `machine_id` is `${user_id}-${machine_id}-${project}`, so every row of a
40
+ * user-scoped control plane repeats the same user uuid at the head; conductor
41
+ * workspaces all share `/home/<user>/projects/…`. Eliding the right would leave
42
+ * every row looking identical.
43
+ *
44
+ * THAT IS WHY `MACHINE` AND `WORKSPACE` KEEP FIXED BUDGETS while `NAME` grows.
45
+ * Making either a grower would hand its cut back to the renderer's
46
+ * right-truncation — head kept, tail dropped — which is strictly worse than
47
+ * today and loses the exact information this function exists to preserve. A
48
+ * grower also declares no width for this to cut against: `elideLeft` marks its
49
+ * cut with a leading `…` precisely because it is told, in advance, how many
50
+ * columns it has.
51
+ */
52
+ const elideLeft = (value, width) => value.length <= width ? value : `…${value.slice(value.length - (width - 1))}`;
53
+ /** How much width the machine_id column gets before {@link elideLeft} cuts it. */
54
+ const MACHINE_ID_WIDTH = 20;
55
+ /**
56
+ * The same, for the workspace path column — and the column that PAYS for this
57
+ * list fitting the cockpit's own default terminal.
58
+ *
59
+ * 30 made the row want 126 columns (NAME's floor 20, MACHINE 20, PROJECT 14,
60
+ * STATUS 9, AGE 8, WORKSPACE 30, PID 7, MARKS 11, plus seven separators) while
61
+ * `gaia ui` opens at 120, so the terminal cut the LAST column: measured on a
62
+ * real pty, the header ended `PID MAR` and a non-local conductor's
63
+ * `elsewhere` came out `elsew`. 24 brings the row to exactly 120.
64
+ *
65
+ * WHY THIS COLUMN GIVES THE SIX. `WORKSPACE` is the only column in the row whose
66
+ * cut is free: it is elided from the LEFT, so it loses six columns of the HEAD —
67
+ * the `/home/<user>/projects/…` prefix every conductor workspace on a machine
68
+ * shares — and keeps the distinguishing tail either way. Every other column
69
+ * would pay in information: `MACHINE` elides left too but is already the
70
+ * narrower of the pair, the three closed vocabularies are sized to their own
71
+ * longest member, and `NAME`/`PROJECT` are the growers this ticket widened.
72
+ *
73
+ * A DECLARED BUDGET, never a measurement (spec D4). 24 is not the width of
74
+ * anyone's path — no page's data is consulted to arrive at it, so the columns do
75
+ * not move when the list does. It is shared by the column's `width` and by the
76
+ * {@link elideLeft} its accessor applies for exactly that reason: two numbers
77
+ * could drift, and a marker cut at one width inside a cell laid out at another
78
+ * is the defect the single constant makes unspellable.
79
+ */
80
+ const PATH_WIDTH = 24;
81
+ /**
82
+ * The conductors list, declared once (GAIA-345 D2/D10) — its eight columns, the
83
+ * noun every one of its node ids is prefixed with, and what it says when it is
84
+ * empty.
85
+ *
86
+ * It GAINED A HEADER with this ticket (D6). This is the list a header earns its
87
+ * keep in most: eight columns of ids, paths and numbers are not self-describing
88
+ * the way three named ones were. The consequence is stated because it is a
89
+ * behaviour change — the list grows one row at the top and its selection
90
+ * highlight moves down one physical row. Nothing computes that offset:
91
+ * selection is a node attribute and the cursor is found through `rowIds`, both
92
+ * of which GAIA-340 made position-free.
93
+ *
94
+ * `fields` names what each column is built from, as everywhere. This loader
95
+ * reads the whole `gaia_conductor` collection with no sparse fieldset, so there
96
+ * is no `AssertExhaustive` proof binding these — declaring them anyway keeps
97
+ * that a fact about this loader rather than a hole in the column model.
98
+ *
99
+ * WHICH COLUMNS GROW, AND WHY THOSE TWO. The rule is the operator's and it is
100
+ * about the CONTENT: a free-text column grows. `NAME` and `PROJECT` are the two
101
+ * here — a conductor's label and a project's name are operator-written prose
102
+ * with no vocabulary and no shared head, so both benefit from a surplus and
103
+ * neither loses anything to Yoga's right-hand cut. They SHARE it through
104
+ * `grow`, which is what the mechanism exists for, and the shares are 2 : 1 in
105
+ * `NAME`'s favour: the label is the row's identity and the column an operator
106
+ * reads first, while the project name is a secondary attribute that repeats
107
+ * across every row of one project. `PROJECT` keeps its 14 as its BASIS rather
108
+ * than as a bound, so a narrow terminal leaves it exactly the columns it had
109
+ * before this ticket and a wide one hands it a share.
110
+ *
111
+ * The other six do not grow, and each for its own reason. `MACHINE` and
112
+ * `WORKSPACE` are free text too, and are the ONE exception to the rule: they
113
+ * are elided from the LEFT (see {@link elideLeft}), which needs a declared
114
+ * width to cut against and keeps the distinguishing tail — a grower would hand
115
+ * the cut back to the renderer's right-hand clip and lose exactly what that
116
+ * elision preserves. `STATUS`, `AGE` and `PID` are closed vocabularies that
117
+ * cannot use a surplus at all. `MARKS` is a closed vocabulary too and keeps a
118
+ * real budget for a measured reason: `width: 0, grow: 0` collapses it to zero
119
+ * columns and the mark vanishes.
120
+ */
121
+ export const CONDUCTOR_TABLE = {
122
+ noun: 'conductor',
123
+ emptyMessage: 'No conductors on this control plane.',
124
+ columns: [
125
+ {
126
+ /**
127
+ * A GROWER — free text, right-truncated, and the column an operator reads
128
+ * first. Declared FIRST and growing all the same: `flexGrow` is a
129
+ * property of the cell, not of its position in the row (measured, spec
130
+ * R2). Its `grow: 2` against `PROJECT`'s 1 is the identity column's larger
131
+ * claim on the surplus, argued above.
132
+ *
133
+ * `min: 20` is the ONE floor on this list, and it is preservation rather
134
+ * than invention: 20 is the budget this column has carried since
135
+ * GAIA-232, and without it a narrow terminal squeezes the grower to
136
+ * nothing while seven fixed budgets keep theirs — measured at 80 columns,
137
+ * NAME came out ONE column wide and the label read `c…`. The other seven
138
+ * columns need 106 columns between them, so below ~126 the row runs past
139
+ * the right edge and the renderer clips it there. That is exactly what
140
+ * this list did before this ticket, at every width; the floor keeps the
141
+ * narrow case as it was and lets the wide case improve.
142
+ */
143
+ key: 'name',
144
+ label: 'NAME',
145
+ width: 0,
146
+ min: 20,
147
+ grow: 2,
148
+ fields: ['label', 'machine_id'],
149
+ get: (row) => row.label !== '' ? row.label : (row.machineId ?? ''),
150
+ },
151
+ {
152
+ // Its OWN column, not merely the label's fallback (GAIA-232 AC-1): two
153
+ // conductors of one repo share a project and a workspace_root, and the
154
+ // machine_id is the only thing that separates them.
155
+ key: 'machine',
156
+ label: 'MACHINE',
157
+ width: MACHINE_ID_WIDTH,
158
+ fields: ['machine_id'],
159
+ get: (row) => elideLeft(row.machineId ?? '', MACHINE_ID_WIDTH),
160
+ },
161
+ {
162
+ /**
163
+ * A project NAME, so free text, so a GROWER — the operator's correction to
164
+ * Task 6, which shipped this column fixed at 14 and named the cut an
165
+ * accepted residual. Measured at 160 columns with that configuration: 15
166
+ * columns unused at the right edge while an ordinary project name was cut
167
+ * to `keytec-gaia-p…`, which is finding 3 of this very ticket reproduced
168
+ * one column further left.
169
+ *
170
+ * The 14 stays as the flex BASIS, not as a bound: it is what the column
171
+ * starts from and what a terminal too narrow to give anything leaves it,
172
+ * so the narrow case is unchanged and only the wide one improves. No
173
+ * `min`, deliberately — without one the cell keeps `flexShrink: 0` and an
174
+ * overflowing row takes its columns from `NAME`, whose `min: 20` is the
175
+ * floor this list has always had.
176
+ */
177
+ key: 'project',
178
+ label: 'PROJECT',
179
+ width: 14,
180
+ grow: 1,
181
+ fields: ['project_id'],
182
+ get: (row, ctx) => row.projectId ? (ctx.projectNames.get(row.projectId) ?? '') : '',
183
+ },
184
+ {
185
+ /** A CLOSED VOCABULARY. `ConductorStatus`'s reachable members here are
186
+ * `running | stale | offline` — this screen can never produce
187
+ * `registry-only`, because every row IS a control-plane record — whose
188
+ * longest is 7, so 9 shows each of them whole. */
189
+ key: 'status',
190
+ label: 'STATUS',
191
+ width: 9,
192
+ fields: ['status', 'lease_expires_at', 'last_seen'],
193
+ get: (row) => row.derivedStatus,
194
+ },
195
+ {
196
+ /** `humanizeAge`'s own vocabulary, exactly as on the projects list: `—`,
197
+ * `just now`, `<n>m|h|d ago`. 8 is the longest of them. */
198
+ key: 'age',
199
+ label: 'AGE',
200
+ width: 8,
201
+ fields: ['last_seen'],
202
+ get: (row, ctx) => humanizeAge(row.lastSeen || null, ctx.nowSeconds),
203
+ },
204
+ {
205
+ key: 'workspace',
206
+ label: 'WORKSPACE',
207
+ width: PATH_WIDTH,
208
+ fields: ['workspace_root'],
209
+ get: (row) => elideLeft(row.workspaceRoot, PATH_WIDTH),
210
+ },
211
+ {
212
+ /** A pid is at most 7 digits — Linux's `pid_max` ceiling is 4 194 304 —
213
+ * and a conductor that never reported one shows `-` rather than a
214
+ * fabricated number. */
215
+ key: 'pid',
216
+ label: 'PID',
217
+ width: 7,
218
+ fields: ['last_pid'],
219
+ get: (row) => row.lastPid === undefined ? '-' : String(row.lastPid),
220
+ },
221
+ {
222
+ /**
223
+ * A closed vocabulary too, which is why it takes a budget instead of the
224
+ * trailing slot it used to hold: everything it can say is `…`,
225
+ * `elsewhere` or both, and 11 is `… elsewhere`.
226
+ *
227
+ * A non-local conductor is MARKED rather than hidden. The control plane
228
+ * is user-scoped, not machine-scoped, so a user's other machines'
229
+ * conductors are legitimately in this list — and silently dropping them
230
+ * would make the cockpit disagree with `gaia conductor ls` for no visible
231
+ * reason. The mark says why their actions are inert here: a pid from
232
+ * another machine means nothing on this one.
233
+ */
234
+ key: 'marks',
235
+ label: 'MARKS',
236
+ width: 11,
237
+ fields: ['workspace_root'],
238
+ get: (row, ctx) => [ctx.busyIds.has(row.id) ? '…' : '', row.isLocal ? '' : 'elsewhere']
239
+ .filter((mark) => mark !== '')
240
+ .join(' '),
241
+ },
242
+ ],
243
+ };
244
+ const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
245
+ const toConductorRow = (r, ctx) => {
246
+ const workspaceRoot = str(r.attr('workspace_root')) ?? '';
247
+ // ISO-8601 over the wire, not the unix integer the field name implies — a
248
+ // `typeof === 'number'` guard here is what made a live conductor read `stale`.
249
+ const lastSeen = toUnixSeconds(r.attr('last_seen')) ?? 0;
250
+ const leaseExpiresAt = toUnixSeconds(r.attr('lease_expires_at')) ?? 0;
251
+ return {
252
+ ...toConductor(r),
253
+ workspaceRoot,
254
+ lastSeen,
255
+ leaseExpiresAt,
256
+ currentLoad: num(r.attr('current_load')) ?? 0,
257
+ maxParallel: num(r.attr('max_parallel')) ?? 0,
258
+ lastPid: num(r.attr('last_pid')),
259
+ conductorName: str(r.attr('conductor_name')),
260
+ // `present: true` because the row IS a control-plane record — which is why
261
+ // `registry-only` can never come out of this screen.
262
+ derivedStatus: deriveConductorStatus({
263
+ present: true,
264
+ status: str(r.attr('status')) ?? null,
265
+ lastSeen: lastSeen === 0 ? null : lastSeen,
266
+ leaseExpiresAt: leaseExpiresAt === 0 ? null : leaseExpiresAt,
267
+ }, ctx.now),
268
+ // A conductor that never reported a workspace cannot be local, whatever the
269
+ // filesystem would say about an empty path.
270
+ isLocal: workspaceRoot !== '' && ctx.exists(workspaceRoot),
271
+ };
272
+ };
273
+ /**
274
+ * The conductor collection as cockpit rows, in server order.
275
+ *
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.
279
+ */
280
+ export const listConductorRows = async (client, ctx) => {
281
+ const outcome = await listOutcome(client, 'gaia_conductor', (r) => toConductorRow(r, ctx));
282
+ return outcome.rows;
283
+ };
284
+ /**
285
+ * Project uuid → name, for the Conductors screen's project column.
286
+ *
287
+ * A conductor references its project by uuid; the screen shows the name. This
288
+ * is a separate small read rather than a widened `ProjectRow`, because
289
+ * `loadProjectRows` builds the PROJECTS screen (one row per project, derived
290
+ * from conductors) and adding an id to it would change a shape four screens
291
+ * depend on to serve one column.
292
+ *
293
+ * A failed read yields an empty map: an unnamed project renders blank, which is
294
+ * better than a screen that refuses to list conductors because their projects
295
+ * could not be resolved.
296
+ */
297
+ export const listProjectNames = async (client) => {
298
+ const outcome = await listOutcome(client, 'gaia_project', (r) => ({
299
+ id: r.id,
300
+ name: str(r.attr('name')) ?? str(r.attr('label')) ?? '',
301
+ }));
302
+ return new Map(outcome.rows.map((p) => [p.id, p.name]));
303
+ };
@@ -1,5 +1,6 @@
1
+ import { type ConductorLiveness } from '@gaia-ai/core';
1
2
  /** The derived liveness state of a conductor. */
2
- export type ConductorStatus = 'running' | 'stale' | 'offline' | 'registry-only';
3
+ export type ConductorStatus = ConductorLiveness | 'registry-only';
3
4
  /** The raw liveness inputs derived from a `gaia_conductor` record. */
4
5
  export interface ConductorInput {
5
6
  present?: boolean;
@@ -1,23 +1,19 @@
1
- // Conductor liveness derivation. Mirrors the server-side ConductorDeriver
1
+ // Conductor liveness derivation.
2
+ //
3
+ // The rule itself lives in `@gaia-ai/core` (`deriveConductorLiveness`) so the
4
+ // CLI listing, this cockpit and the server-side ConductorDeriver
2
5
  // (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;
6
+ // cannot drift into describing one conductor three ways. What stays here is the
7
+ // one thing core cannot answer: whether a gaia_conductor record exists at all.
8
+ import { deriveConductorLiveness } from '@gaia-ai/core';
7
9
  export function deriveConductorStatus(conductor, nowSeconds) {
10
+ // `registry-only` means "something names this conductor but the control plane
11
+ // has no record of it". The Conductors screen reads the control plane itself,
12
+ // so every row it builds is present by construction and this branch cannot
13
+ // fire there; it remains for callers that join a record against another list.
8
14
  if (!conductor?.present)
9
15
  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';
16
+ return deriveConductorLiveness(conductor, nowSeconds);
21
17
  }
22
18
  // `humanizeAge` moved to `lib/format.ts` (GAIA-236, entity-slice Phase 0): it is a
23
19
  // generic relative-time formatter with no conductor vocabulary, and keeping it here
@@ -0,0 +1,25 @@
1
+ export interface LogTail {
2
+ /** Whether the log file exists at all. */
3
+ present: boolean;
4
+ /** Formatted lines, oldest first. */
5
+ lines: string[];
6
+ /** Whether earlier content was cut off by the byte window. */
7
+ truncated: boolean;
8
+ }
9
+ export interface LogTailOptions {
10
+ maxLines?: number;
11
+ }
12
+ /**
13
+ * The tail of a conductor's `log.txt`, formatted for reading.
14
+ *
15
+ * The file sink writes raw pino NDJSON (`pino-pretty` serves only the stdout
16
+ * sink), so a dump would be a wall of JSON. Each record becomes
17
+ * `HH:MM:SS LEVEL msg (k=v …)`; anything that is not a pino record — a stack
18
+ * trace, a stray `console.log`, a line the window cut — is passed through
19
+ * verbatim rather than hidden, because a log view that silently drops what it
20
+ * cannot parse is worse than one that shows it.
21
+ *
22
+ * Never throws: this runs inside a TUI refresh, where an unreadable log must
23
+ * leave the screen navigable.
24
+ */
25
+ export declare function tailConductorLog(path: string, options?: LogTailOptions): LogTail;