@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.
- package/dist/src/Agent/launch.d.ts +4 -4
- package/dist/src/Agent/launch.js +14 -7
- package/dist/src/Component/section-list/index.d.ts +117 -8
- package/dist/src/Component/section-list/index.js +118 -46
- package/dist/src/Component/stats-panel/index.d.ts +17 -5
- package/dist/src/Component/stats-panel/index.js +50 -52
- package/dist/src/Component/top-tabs/index.d.ts +95 -23
- package/dist/src/Component/top-tabs/index.js +87 -41
- package/dist/src/Entity/collection.d.ts +279 -0
- package/dist/src/Entity/collection.js +377 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +25 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.js +60 -0
- package/dist/src/Entity/conductor/Data/conductors.d.ts +216 -0
- package/dist/src/Entity/conductor/Data/conductors.js +280 -0
- package/dist/src/Entity/conductor/Data/liveness.d.ts +2 -1
- package/dist/src/Entity/conductor/Data/liveness.js +12 -16
- package/dist/src/Entity/conductor/Data/log-tail.d.ts +25 -0
- package/dist/src/Entity/conductor/Data/log-tail.js +133 -0
- package/dist/src/Entity/conductor/Data/own-conductor.d.ts +30 -0
- package/dist/src/Entity/conductor/Data/own-conductor.js +73 -0
- package/dist/src/Entity/conductor/Data/process.d.ts +78 -0
- package/dist/src/Entity/conductor/Data/process.js +101 -0
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +67 -0
- package/dist/src/Entity/conductor/Screen/conductors.js +233 -0
- package/dist/src/Entity/cycle.d.ts +18 -0
- package/dist/src/Entity/cycle.js +30 -0
- package/dist/src/Entity/project/Data/project.d.ts +8 -0
- package/dist/src/Entity/project/Data/project.js +12 -0
- package/dist/src/Entity/project/Data/projects.d.ts +121 -0
- package/dist/src/Entity/project/Data/projects.js +115 -0
- package/dist/src/Entity/project/Data/with-tickets.d.ts +5 -0
- package/dist/src/Entity/project/Data/with-tickets.js +15 -13
- package/dist/src/Entity/project/Screen/project-picker.d.ts +80 -0
- package/dist/src/Entity/project/Screen/project-picker.js +191 -0
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +52 -0
- package/dist/src/Entity/project/Screen/projects-screen.js +122 -0
- package/dist/src/Entity/run/Component/run-detail/index.d.ts +26 -10
- package/dist/src/Entity/run/Component/run-detail/index.js +147 -39
- package/dist/src/Entity/run/Component/run-table/index.d.ts +5 -7
- package/dist/src/Entity/run/Component/run-table/index.js +9 -18
- package/dist/src/Entity/run/Data/query.d.ts +22 -0
- package/dist/src/Entity/run/Data/query.js +41 -0
- package/dist/src/Entity/run/Data/runs.d.ts +192 -4
- package/dist/src/Entity/run/Data/runs.js +188 -27
- package/dist/src/Entity/run/Screen/run-detail.d.ts +53 -0
- package/dist/src/Entity/run/Screen/run-detail.js +70 -0
- package/dist/src/Entity/run/Screen/runs.d.ts +86 -0
- package/dist/src/Entity/run/Screen/runs.js +169 -0
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +78 -0
- package/dist/src/Entity/statistics/Screen/dashboard.js +228 -0
- package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +12 -9
- package/dist/src/Entity/ticket/Component/comment-item/index.js +18 -13
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +16 -3
- package/dist/src/Entity/ticket/Component/create-form/index.js +28 -14
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +11 -6
- package/dist/src/Entity/ticket/Component/edit-form/index.js +77 -29
- package/dist/src/Entity/ticket/Component/sections/index.d.ts +8 -0
- package/dist/src/Entity/ticket/Component/sections/index.js +42 -0
- package/dist/src/Entity/ticket/Component/tab-bar/index.js +6 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +65 -8
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +171 -84
- package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +4 -2
- package/dist/src/Entity/ticket/Component/ticket-table/index.js +8 -12
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +47 -3
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -22
- package/dist/src/Entity/ticket/Data/parents.d.ts +13 -1
- package/dist/src/Entity/ticket/Data/parents.js +64 -12
- package/dist/src/Entity/ticket/Data/query.d.ts +44 -7
- package/dist/src/Entity/ticket/Data/query.js +60 -26
- package/dist/src/Entity/ticket/Data/tickets.d.ts +170 -4
- package/dist/src/Entity/ticket/Data/tickets.js +200 -12
- package/dist/src/Entity/ticket/Data/url.d.ts +16 -0
- package/dist/src/Entity/ticket/Data/url.js +20 -0
- package/dist/src/Entity/ticket/Data/workflow.d.ts +1 -5
- package/dist/src/Entity/ticket/Data/workflow.js +5 -13
- package/dist/src/Entity/ticket/Data/write.d.ts +32 -0
- package/dist/src/Entity/ticket/Data/write.js +80 -0
- package/dist/src/Entity/ticket/Form/create-data.d.ts +1 -11
- package/dist/src/Entity/ticket/Form/create-data.js +7 -36
- package/dist/src/Entity/ticket/Form/create.d.ts +45 -5
- package/dist/src/Entity/ticket/Form/create.js +45 -8
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +1 -1
- package/dist/src/Entity/ticket/Form/edit-data.js +4 -0
- package/dist/src/Entity/ticket/Form/edit.d.ts +79 -12
- package/dist/src/Entity/ticket/Form/edit.js +165 -18
- package/dist/src/Entity/ticket/Form/keys.d.ts +7 -0
- package/dist/src/Entity/ticket/Form/keys.js +7 -0
- package/dist/src/Entity/ticket/Form/parent-filter.d.ts +36 -0
- package/dist/src/Entity/ticket/Form/parent-filter.js +37 -0
- package/dist/src/Entity/ticket/Form/parent-source.d.ts +27 -0
- package/dist/src/Entity/ticket/Form/parent-source.js +56 -0
- package/dist/src/Entity/ticket/Form/who-filter.d.ts +33 -0
- package/dist/src/Entity/ticket/Form/who-filter.js +33 -0
- package/dist/src/Entity/ticket/Form/who-source.d.ts +18 -0
- package/dist/src/Entity/ticket/Form/who-source.js +38 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +81 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +356 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +92 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +220 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +44 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +200 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +136 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +257 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +125 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +415 -0
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +145 -0
- package/dist/src/Entity/ticket/Screen/tickets.js +538 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +41 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +174 -0
- package/dist/src/Entity/user/Data/users.d.ts +17 -0
- package/dist/src/Entity/user/Data/users.js +66 -6
- package/dist/src/Form/field.d.ts +3 -1
- package/dist/src/Form/field.js +1 -0
- package/dist/src/Form/form.d.ts +1 -1
- package/dist/src/Form/form.js +61 -9
- package/dist/src/Form/keys.d.ts +23 -1
- package/dist/src/Form/keys.js +78 -23
- package/dist/src/Kernel/chrome.d.ts +289 -0
- package/dist/src/Kernel/chrome.js +404 -0
- package/dist/src/Kernel/clipboard.d.ts +30 -0
- package/dist/src/Kernel/clipboard.js +51 -0
- package/dist/src/Kernel/key-input.d.ts +50 -0
- package/dist/src/Kernel/key-input.js +158 -0
- package/dist/src/Kernel/mount.d.ts +187 -0
- package/dist/src/Kernel/mount.js +399 -0
- package/dist/src/Kernel/tui-kernel.d.ts +0 -11
- package/dist/src/Kernel/tui-kernel.js +778 -2367
- package/dist/src/Routing/context.d.ts +31 -0
- package/dist/src/Routing/context.js +52 -0
- package/dist/src/Routing/registry.d.ts +19 -0
- package/dist/src/Routing/registry.js +97 -0
- package/dist/src/Routing/route.d.ts +35 -0
- package/dist/src/Routing/route.js +1 -0
- package/dist/src/Routing/router.d.ts +49 -0
- package/dist/src/Routing/router.js +123 -0
- package/dist/src/Routing/routes.d.ts +17 -0
- package/dist/src/Routing/routes.js +57 -0
- package/dist/src/Screen/async-screen.d.ts +87 -0
- package/dist/src/Screen/async-screen.js +206 -0
- package/dist/src/Screen/list-screen.d.ts +193 -0
- package/dist/src/Screen/list-screen.js +261 -0
- package/dist/src/Screen/screen.d.ts +240 -0
- package/dist/src/Screen/screen.js +34 -0
- package/dist/src/Tui/controller.d.ts +13 -0
- package/dist/src/Tui/controller.js +1794 -0
- package/dist/src/Widget/autocomplete.js +75 -3
- package/dist/src/Widget/index.d.ts +3 -2
- package/dist/src/Widget/index.js +2 -1
- package/dist/src/Widget/option-source.d.ts +12 -1
- package/dist/src/Widget/option-source.js +14 -1
- package/dist/src/Widget/render.d.ts +23 -8
- package/dist/src/Widget/render.js +108 -41
- package/dist/src/Widget/text.js +94 -5
- package/dist/src/Widget/textarea.d.ts +45 -0
- package/dist/src/Widget/textarea.js +115 -0
- package/dist/src/Widget/types.d.ts +64 -1
- package/dist/src/launcher.d.ts +9 -2
- package/dist/src/launcher.js +2 -1
- package/dist/src/lib/ansi.js +13 -5
- package/dist/src/lib/clipboard.d.ts +41 -0
- package/dist/src/lib/clipboard.js +65 -0
- package/dist/src/lib/cursor.d.ts +31 -0
- package/dist/src/lib/cursor.js +45 -0
- package/dist/src/lib/format.d.ts +21 -13
- package/dist/src/lib/format.js +76 -28
- package/dist/src/lib/hyperlink.d.ts +58 -0
- package/dist/src/lib/hyperlink.js +104 -0
- package/dist/src/lib/input-trace.d.ts +48 -0
- package/dist/src/lib/input-trace.js +47 -0
- package/dist/src/lib/key-vocabulary.d.ts +19 -0
- package/dist/src/lib/key-vocabulary.js +25 -0
- package/dist/src/lib/table.d.ts +37 -0
- package/dist/src/lib/table.js +20 -0
- package/dist/src/lib/vnode.d.ts +217 -0
- package/dist/src/lib/vnode.js +456 -0
- package/dist/src/lib/width.d.ts +25 -1
- package/dist/src/lib/width.js +93 -12
- package/dist/src/plugin.js +26 -1
- package/dist/src/types.d.ts +67 -2
- package/dist/src/types.js +0 -4
- 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 =
|
|
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.
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
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
|
-
|
|
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;
|