@gaia-ai/addon-gaia-ui 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Component/footer-actions/index.d.ts +58 -0
- package/dist/src/Component/footer-actions/index.js +111 -0
- package/dist/src/Component/list-chrome/index.d.ts +19 -0
- package/dist/src/Component/list-chrome/index.js +61 -0
- package/dist/src/Component/markdown/index.d.ts +68 -0
- package/dist/src/Component/markdown/index.js +133 -0
- package/dist/src/Component/section-list/index.d.ts +115 -23
- package/dist/src/Component/section-list/index.js +120 -34
- package/dist/src/Component/top-tabs/index.d.ts +65 -6
- package/dist/src/Component/top-tabs/index.js +29 -6
- package/dist/src/Entity/collection.js +1 -1
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +26 -41
- package/dist/src/Entity/conductor/Screen/conductors.js +65 -123
- package/dist/src/Entity/project/Screen/project-picker.d.ts +20 -31
- package/dist/src/Entity/project/Screen/project-picker.js +48 -58
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +4 -2
- package/dist/src/Entity/project/Screen/projects-screen.js +11 -8
- package/dist/src/Entity/run/Component/run-detail/index.js +15 -7
- package/dist/src/Entity/run/Data/query.d.ts +10 -0
- package/dist/src/Entity/run/Data/query.js +16 -1
- package/dist/src/Entity/run/Data/runs.d.ts +2 -0
- package/dist/src/Entity/run/Data/runs.js +19 -1
- package/dist/src/Entity/run/Screen/run-detail.d.ts +16 -23
- package/dist/src/Entity/run/Screen/run-detail.js +27 -34
- package/dist/src/Entity/run/Screen/runs.d.ts +7 -3
- package/dist/src/Entity/run/Screen/runs.js +31 -48
- package/dist/src/Entity/run/Screen/user-runs.d.ts +79 -0
- package/dist/src/Entity/run/Screen/user-runs.js +98 -0
- package/dist/src/Entity/statistics/Data/statistics.d.ts +31 -6
- package/dist/src/Entity/statistics/Data/statistics.js +39 -6
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +10 -2
- package/dist/src/Entity/statistics/Screen/dashboard.js +111 -34
- package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +14 -1
- package/dist/src/Entity/ticket/Component/comment-item/index.js +15 -1
- package/dist/src/Entity/ticket/Component/sections/index.js +16 -1
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +102 -13
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +440 -72
- package/dist/src/Entity/ticket/Data/detail.d.ts +14 -0
- package/dist/src/Entity/ticket/Data/detail.js +15 -1
- package/dist/src/Entity/ticket/Data/query.d.ts +7 -0
- package/dist/src/Entity/ticket/Data/query.js +18 -2
- package/dist/src/Entity/ticket/Data/tickets.d.ts +24 -0
- package/dist/src/Entity/ticket/Data/tickets.js +41 -1
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +10 -4
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +37 -16
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +6 -4
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +16 -15
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +6 -4
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +14 -11
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +5 -2
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +14 -14
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +89 -4
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +295 -74
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +66 -3
- package/dist/src/Entity/ticket/Screen/tickets.js +140 -76
- package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +91 -0
- package/dist/src/Entity/ticket/Screen/user-tickets.js +94 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +6 -4
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +14 -11
- package/dist/src/Entity/user/Data/users.d.ts +49 -0
- package/dist/src/Entity/user/Data/users.js +46 -0
- package/dist/src/Entity/user/Screen/user-overview.d.ts +33 -0
- package/dist/src/Entity/user/Screen/user-overview.js +159 -0
- package/dist/src/Entity/user/Screen/users.d.ts +39 -0
- package/dist/src/Entity/user/Screen/users.js +136 -0
- package/dist/src/Extension/contract.d.ts +58 -0
- package/dist/src/Extension/contract.js +1 -0
- package/dist/src/Extension/load.d.ts +10 -0
- package/dist/src/Extension/load.js +21 -0
- package/dist/src/Extension/store.d.ts +11 -0
- package/dist/src/Extension/store.js +44 -0
- package/dist/src/Kernel/chrome.d.ts +49 -12
- package/dist/src/Kernel/chrome.js +150 -57
- package/dist/src/Kernel/key-input.js +1 -1
- package/dist/src/Kernel/mount.d.ts +1 -1
- package/dist/src/Kernel/mount.js +1 -1
- package/dist/src/Kernel/regions.d.ts +64 -0
- package/dist/src/Kernel/regions.js +328 -0
- package/dist/src/Kernel/renderer.d.ts +25 -0
- package/dist/src/Kernel/renderer.js +142 -0
- package/dist/src/Kernel/splash.d.ts +27 -0
- package/dist/src/Kernel/splash.js +72 -0
- package/dist/src/Kernel/tui-kernel.js +139 -434
- package/dist/src/Routing/context.d.ts +5 -0
- package/dist/src/Routing/context.js +6 -0
- package/dist/src/Routing/registry.js +4 -2
- package/dist/src/Routing/router.js +5 -1
- package/dist/src/Routing/routes.js +8 -0
- package/dist/src/Screen/async-modal-screen.d.ts +26 -0
- package/dist/src/Screen/async-modal-screen.js +44 -0
- package/dist/src/Screen/async-screen.d.ts +32 -40
- package/dist/src/Screen/async-screen.js +45 -41
- package/dist/src/Screen/full-screen.d.ts +1 -0
- package/dist/src/Screen/full-screen.js +3 -0
- package/dist/src/Screen/list-screen.d.ts +7 -1
- package/dist/src/Screen/list-screen.js +17 -6
- package/dist/src/Screen/modal-screen.d.ts +6 -0
- package/dist/src/Screen/modal-screen.js +2 -0
- package/dist/src/Screen/screen.d.ts +42 -6
- package/dist/src/Screen/screen.js +2 -2
- package/dist/src/Widget/textarea.js +10 -14
- package/dist/src/Widget/types.d.ts +1 -1
- package/dist/src/launcher.d.ts +6 -0
- package/dist/src/launcher.js +2 -0
- package/dist/src/lib/format.js +1 -1
- package/dist/src/lib/markdown.d.ts +53 -0
- package/dist/src/lib/markdown.js +522 -0
- package/dist/src/lib/refresh-signal.d.ts +56 -0
- package/dist/src/lib/refresh-signal.js +85 -0
- package/dist/src/lib/theme.d.ts +36 -0
- package/dist/src/lib/theme.js +40 -0
- package/dist/src/lib/version.d.ts +5 -0
- package/dist/src/lib/version.js +46 -0
- package/dist/src/lib/vnode.d.ts +86 -63
- package/dist/src/lib/vnode.js +112 -85
- package/dist/src/types.d.ts +32 -0
- package/dist/src/ui.d.ts +2 -0
- package/dist/src/ui.js +1 -0
- package/package.json +4 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import { actionItem } from '../../../Component/footer-actions/index.js';
|
|
2
3
|
import { columnNode, textNode } from '../../../lib/vnode.js';
|
|
3
|
-
import {
|
|
4
|
+
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
4
5
|
import { listRowIds, listTableTree } from '../../collection.js';
|
|
5
6
|
import { CONDUCTOR_TABLE, listConductorRows, listProjectNames, } from '../Data/conductors.js';
|
|
6
7
|
import { tailConductorLog } from '../Data/log-tail.js';
|
|
7
8
|
import { createConductorProcessHost } from '../Data/process.js';
|
|
8
|
-
const REFRESH_MS = 5_000;
|
|
9
9
|
/**
|
|
10
10
|
* The open row's log block: where it is, and its tail.
|
|
11
11
|
*
|
|
@@ -24,7 +24,14 @@ const logFor = (row) => row.isLocal
|
|
|
24
24
|
heading: `log — ${row.label} runs on another machine; no log here`,
|
|
25
25
|
lines: [],
|
|
26
26
|
};
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Conductors list — a normal {@link AsyncScreen}.
|
|
29
|
+
*
|
|
30
|
+
* Refresh is the shared mechanism (GAIA-376 `ui-refresh` file + `R`), not a
|
|
31
|
+
* screen-private timer. Enter loads; navigation away cancels in-flight reads
|
|
32
|
+
* via the async lifecycle.
|
|
33
|
+
*/
|
|
34
|
+
export class ConductorsScreen extends AsyncScreen {
|
|
28
35
|
static route = {
|
|
29
36
|
id: 'conductors',
|
|
30
37
|
path: '/conductors',
|
|
@@ -32,70 +39,41 @@ export class ConductorsScreen extends BaseScreen {
|
|
|
32
39
|
};
|
|
33
40
|
id = ConductorsScreen.route.id;
|
|
34
41
|
frameId = 'conductors';
|
|
35
|
-
rows = [];
|
|
36
|
-
projectNames = new Map();
|
|
37
42
|
selectedIndex = 0;
|
|
38
43
|
logFor = null;
|
|
39
|
-
status = '';
|
|
40
|
-
reading = false;
|
|
41
|
-
timer = null;
|
|
42
44
|
busyIds = new Set();
|
|
43
|
-
activation = 0;
|
|
44
45
|
topTab() {
|
|
45
46
|
return { level: 'root', tab: 'conductors' };
|
|
46
47
|
}
|
|
47
|
-
// GAIA-285 spec D5/D6 — the shared `R` lands here; the 5 s tick passes
|
|
48
|
-
// `quiet` so only an EXPLICIT reload states its loading in the footer.
|
|
49
|
-
quietRead = false;
|
|
50
|
-
get busy() {
|
|
51
|
-
return this.reading && !this.quietRead;
|
|
52
|
-
}
|
|
53
48
|
reload(context) {
|
|
54
|
-
|
|
49
|
+
this.retry(context);
|
|
55
50
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
get rows() {
|
|
52
|
+
return this.readyValue?.rows ?? [];
|
|
53
|
+
}
|
|
54
|
+
async load(context) {
|
|
55
|
+
if (!context.services)
|
|
56
|
+
throw new Error('Conductors requires services');
|
|
57
|
+
const client = context.services.client;
|
|
58
|
+
const [projectNames, rows] = await Promise.all([
|
|
59
|
+
listProjectNames(client),
|
|
60
|
+
listConductorRows(client, {
|
|
61
|
+
now: context.nowSeconds?.() ?? Math.floor(Date.now() / 1000),
|
|
62
|
+
exists: existsSync,
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
return { rows, projectNames };
|
|
62
66
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.stopTimer();
|
|
67
|
+
renderLoading() {
|
|
68
|
+
return ['Loading conductors…'];
|
|
66
69
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
* since GAIA-345 that tree is the SHARED table's (AC 1/2).
|
|
70
|
-
*
|
|
71
|
-
* There is no `conductor-list` component any more: the eight columns, the
|
|
72
|
-
* noun and the empty message are `CONDUCTOR_TABLE`, declared beside the row
|
|
73
|
-
* mapper that produces the rows. What this screen still owns is the CONTEXT
|
|
74
|
-
* the columns read — the frame's clock, which conductors have an action in
|
|
75
|
-
* flight, and the project names its own small read resolved — because none of
|
|
76
|
-
* those is a property of a `gaia_conductor` record.
|
|
77
|
-
*
|
|
78
|
-
* THE OPEN LOG IS NOT A ROW OF THE LIST, so it is not in the table: it is
|
|
79
|
-
* neither keyed on a conductor nor laid out in the columns, and a log line
|
|
80
|
-
* keyed under the list's ids could collide with a row's. It stays its own
|
|
81
|
-
* block below the table, whose lines are keyed by position — honest for a
|
|
82
|
-
* block replaced wholesale on every read that indexes nothing outside itself.
|
|
83
|
-
* The wrapper is stated on EVERY frame, log or no log, so the body's root
|
|
84
|
-
* node keeps one identity across the keypress that opens one.
|
|
85
|
-
*
|
|
86
|
-
* MOUNTED WHOLE, so the bound on its nodes is the data's own: one box of 8
|
|
87
|
-
* cells per conductor, over the conductors of one user on one control plane,
|
|
88
|
-
* plus at most 40 log lines. `@opentui/core@0.5.1` hands out 16 382 before it
|
|
89
|
-
* throws, so this screen windows nothing of its own — deliberately, not by
|
|
90
|
-
* omission.
|
|
91
|
-
*/
|
|
92
|
-
surface(context) {
|
|
93
|
-
const open = this.rows.find((row) => row.id === this.logFor);
|
|
70
|
+
renderReady(data, context) {
|
|
71
|
+
const open = data.rows.find((row) => row.id === this.logFor);
|
|
94
72
|
const log = open ? logFor(open) : undefined;
|
|
95
|
-
return columnNode('conductor-list', listTableTree(CONDUCTOR_TABLE,
|
|
73
|
+
return columnNode('conductor-list', listTableTree(CONDUCTOR_TABLE, data.rows, {
|
|
96
74
|
nowSeconds: context.nowSeconds?.() ?? Math.floor(Date.now() / 1000),
|
|
97
75
|
busyIds: this.busyIds,
|
|
98
|
-
projectNames:
|
|
76
|
+
projectNames: data.projectNames,
|
|
99
77
|
}, { selectedIndex: this.selectedIndex }), ...(log
|
|
100
78
|
? [
|
|
101
79
|
textNode({ id: 'conductor-log-gap', content: '' }),
|
|
@@ -104,19 +82,26 @@ export class ConductorsScreen extends BaseScreen {
|
|
|
104
82
|
]
|
|
105
83
|
: []));
|
|
106
84
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
85
|
+
didLoad(data) {
|
|
86
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, data.rows.length - 1));
|
|
87
|
+
}
|
|
88
|
+
chrome(_context) {
|
|
89
|
+
const rows = this.rows;
|
|
90
|
+
return {
|
|
91
|
+
selectable: rows.length,
|
|
110
92
|
selectedIndex: this.selectedIndex,
|
|
111
|
-
|
|
112
|
-
// SAME configuration that keys the rows — so the click target and the row
|
|
113
|
-
// node cannot be spelled two ways (GAIA-345 D2). The log's lines are not
|
|
114
|
-
// in here, which is the other half of "a log is not a list row".
|
|
115
|
-
rowIds: listRowIds(CONDUCTOR_TABLE, this.rows),
|
|
93
|
+
rowIds: listRowIds(CONDUCTOR_TABLE, rows),
|
|
116
94
|
title: 'GAIA conductors',
|
|
117
|
-
meta: `${
|
|
118
|
-
|
|
119
|
-
|
|
95
|
+
meta: `${rows.length} conductor(s)`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
actions() {
|
|
99
|
+
return [
|
|
100
|
+
actionItem('conductors-start', 's', 'start'),
|
|
101
|
+
actionItem('conductors-stop', 'x', 'stop'),
|
|
102
|
+
actionItem('conductors-enter', 'Enter', 'log'),
|
|
103
|
+
actionItem('conductors-tab', 'Tab', 'tabs'),
|
|
104
|
+
];
|
|
120
105
|
}
|
|
121
106
|
select(index, context) {
|
|
122
107
|
if (index < 0 || index >= this.rows.length)
|
|
@@ -134,6 +119,8 @@ export class ConductorsScreen extends BaseScreen {
|
|
|
134
119
|
return true;
|
|
135
120
|
}
|
|
136
121
|
handleInput(input, context) {
|
|
122
|
+
if (super.handleInput(input, context))
|
|
123
|
+
return true;
|
|
137
124
|
if (input === 's' || input === 'x') {
|
|
138
125
|
void this.act(input === 's' ? 'start' : 'stop', context);
|
|
139
126
|
return true;
|
|
@@ -147,87 +134,42 @@ export class ConductorsScreen extends BaseScreen {
|
|
|
147
134
|
return false;
|
|
148
135
|
}
|
|
149
136
|
reset() {
|
|
150
|
-
this.leave();
|
|
151
|
-
this.rows = [];
|
|
152
|
-
this.projectNames.clear();
|
|
153
137
|
this.selectedIndex = 0;
|
|
154
138
|
this.logFor = null;
|
|
155
|
-
this.status = '';
|
|
156
139
|
this.busyIds.clear();
|
|
157
|
-
|
|
158
|
-
async refresh(context, includeProjects = false, activation = this.activation, quiet = false) {
|
|
159
|
-
if (this.reading || !context.services)
|
|
160
|
-
return;
|
|
161
|
-
this.reading = true;
|
|
162
|
-
this.quietRead = quiet;
|
|
163
|
-
try {
|
|
164
|
-
if (includeProjects && this.projectNames.size === 0) {
|
|
165
|
-
const names = await listProjectNames(context.services.client);
|
|
166
|
-
if (activation !== this.activation)
|
|
167
|
-
return;
|
|
168
|
-
this.projectNames = names;
|
|
169
|
-
}
|
|
170
|
-
const rows = await listConductorRows(context.services.client, {
|
|
171
|
-
now: context.nowSeconds?.() ?? Math.floor(Date.now() / 1000),
|
|
172
|
-
exists: existsSync,
|
|
173
|
-
});
|
|
174
|
-
if (activation !== this.activation)
|
|
175
|
-
return;
|
|
176
|
-
this.rows = rows;
|
|
177
|
-
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.rows.length - 1));
|
|
178
|
-
}
|
|
179
|
-
finally {
|
|
180
|
-
this.reading = false;
|
|
181
|
-
this.quietRead = false;
|
|
182
|
-
}
|
|
183
|
-
// After the finally, so the frame this triggers already reads busy=false —
|
|
184
|
-
// drawn from inside the try it would restate `Loading…` one last time.
|
|
185
|
-
if (context.isActive())
|
|
186
|
-
context.requestDraw();
|
|
140
|
+
super.reset();
|
|
187
141
|
}
|
|
188
142
|
async act(action, context) {
|
|
189
143
|
const row = this.rows[this.selectedIndex];
|
|
190
144
|
if (!row || this.busyIds.has(row.id))
|
|
191
145
|
return;
|
|
192
|
-
if (!row.isLocal)
|
|
193
|
-
this.status = `${row.label} runs on another machine — no action from here`;
|
|
194
|
-
context.requestDraw();
|
|
146
|
+
if (!row.isLocal)
|
|
195
147
|
return;
|
|
196
|
-
|
|
197
|
-
if (action === 'start' && row.derivedStatus === 'running') {
|
|
198
|
-
this.status = `${row.label} is already running`;
|
|
199
|
-
context.requestDraw();
|
|
148
|
+
if (action === 'start' && row.derivedStatus === 'running')
|
|
200
149
|
return;
|
|
201
|
-
}
|
|
202
150
|
this.busyIds.add(row.id);
|
|
203
|
-
this.status = `${action}…`;
|
|
204
151
|
context.requestDraw();
|
|
205
152
|
const host = context.services?.conductorProcess ?? createConductorProcessHost();
|
|
206
153
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
154
|
+
if (action === 'start') {
|
|
155
|
+
await host.start({
|
|
209
156
|
workspaceRoot: row.workspaceRoot,
|
|
210
157
|
conductorName: row.conductorName,
|
|
211
|
-
})
|
|
212
|
-
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
await host.stop({
|
|
213
162
|
pid: row.lastPid,
|
|
214
163
|
workspaceRoot: row.workspaceRoot,
|
|
215
164
|
});
|
|
216
|
-
|
|
165
|
+
}
|
|
217
166
|
}
|
|
218
|
-
catch
|
|
219
|
-
|
|
167
|
+
catch {
|
|
168
|
+
// Outcome lives in the reloaded row state, not a footer notice.
|
|
220
169
|
}
|
|
221
170
|
finally {
|
|
222
171
|
this.busyIds.delete(row.id);
|
|
223
|
-
|
|
224
|
-
if (context.isActive())
|
|
225
|
-
context.requestDraw();
|
|
172
|
+
this.retry(context);
|
|
226
173
|
}
|
|
227
174
|
}
|
|
228
|
-
stopTimer() {
|
|
229
|
-
if (this.timer)
|
|
230
|
-
clearInterval(this.timer);
|
|
231
|
-
this.timer = null;
|
|
232
|
-
}
|
|
233
175
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { VNode } from '@opentui/core';
|
|
2
|
+
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
3
|
+
import type { FullScreenChrome } from '../../../Screen/full-screen.js';
|
|
4
|
+
import type { ScreenContext } from '../../../Screen/screen.js';
|
|
2
5
|
/**
|
|
3
6
|
* A project that claims the repo.
|
|
4
7
|
*
|
|
@@ -15,7 +18,11 @@ export interface ProjectPickerInput {
|
|
|
15
18
|
readonly repo: string;
|
|
16
19
|
}
|
|
17
20
|
export declare const projectPickerInputContext: import("../../../Routing/context.js").ContextKey<ProjectPickerInput>;
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Project picker — normal {@link AsyncScreen}. Candidates arrive via navigation
|
|
23
|
+
* context; refresh is the shared R / ui-refresh path.
|
|
24
|
+
*/
|
|
25
|
+
export declare class ProjectPickerScreen extends AsyncScreen<ProjectPickerInput> {
|
|
19
26
|
readonly frameId = "project-picker";
|
|
20
27
|
topTab(): {
|
|
21
28
|
readonly level: "root";
|
|
@@ -31,48 +38,30 @@ export declare class ProjectPickerScreen extends BaseScreen {
|
|
|
31
38
|
context: (_params: Readonly<Record<string, string>>, state: unknown, base: import("../../../Routing/context.js").ContextBag) => import("../../../Routing/context.js").ContextBag;
|
|
32
39
|
};
|
|
33
40
|
readonly id: string;
|
|
34
|
-
private candidates;
|
|
35
|
-
private repo;
|
|
36
41
|
selectedIndex: number;
|
|
37
|
-
|
|
42
|
+
reload(context: ScreenContext): void;
|
|
43
|
+
get candidates(): readonly PickerCandidate[];
|
|
44
|
+
get repo(): string;
|
|
38
45
|
get rows(): readonly PickerCandidate[];
|
|
46
|
+
protected load(context: ScreenContext): Promise<ProjectPickerInput>;
|
|
47
|
+
protected renderLoading(): string[];
|
|
39
48
|
/**
|
|
40
49
|
* The question this screen asks, and the one place it is written.
|
|
41
50
|
*
|
|
42
|
-
* Both surfaces open with it — the tree
|
|
43
|
-
*
|
|
44
|
-
* disagree about the repo they name.
|
|
51
|
+
* Both surfaces open with it — the tree via meta, the pipe as its first line —
|
|
52
|
+
* so composing it twice is how the two would come to disagree about the repo.
|
|
45
53
|
*/
|
|
46
54
|
private headingText;
|
|
47
|
-
/** The candidate the cursor stands on, clamped: an out-of-range index must
|
|
48
|
-
* move the marker, never lose it. */
|
|
49
55
|
private selected;
|
|
56
|
+
protected renderReady(data: ProjectPickerInput): VNode;
|
|
50
57
|
/**
|
|
51
58
|
* F5 — the pipe's own output, and it stays `string[]`.
|
|
52
59
|
*
|
|
53
|
-
* GAIA-345 D7/D8 — built from the SAME configuration the tree is
|
|
54
|
-
* `listTableRows` and `lineText`, plus the two heading lines and the trailing
|
|
55
|
-
* hint, which are the screen's and not rows of the list. The bytes are
|
|
56
|
-
* unchanged by construction rather than by a comparison somebody has to
|
|
57
|
-
* remember to make, and `tests/Entity/project/Screen/project-picker.test.ts`
|
|
58
|
-
* pins them line for line against the pre-conversion capture.
|
|
60
|
+
* GAIA-345 D7/D8 — built from the SAME configuration the tree is.
|
|
59
61
|
*/
|
|
60
62
|
renderNonInteractive(): string[];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
*
|
|
64
|
-
* The two heading rows are composed AROUND the table's node because they are
|
|
65
|
-
* not rows of it — they name no candidate, they are not selectable, and they
|
|
66
|
-
* carry no entry in `rowIds`. That is also what the old `selRow` offset of 2
|
|
67
|
-
* was compensating for before the kernel started finding the cursor by node.
|
|
68
|
-
*
|
|
69
|
-
* MOUNTED WHOLE — no window of its own, and the bound is named rather than
|
|
70
|
-
* assumed: a row is one `gaia_project` that claims the cwd's remote, which is
|
|
71
|
-
* the multisite topology GAIA-126/137 designs for. That is a handful of sites
|
|
72
|
-
* per repo, against `@opentui/core@0.5.1`'s ceiling of 16 382 renderables.
|
|
73
|
-
*/
|
|
74
|
-
private surface;
|
|
75
|
-
present(_context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
|
|
63
|
+
protected chrome(_context: ScreenContext): FullScreenChrome;
|
|
64
|
+
protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
|
|
76
65
|
handleInput(input: string, context: ScreenContext): boolean;
|
|
77
66
|
activate(index: number, context: ScreenContext): boolean;
|
|
78
67
|
select(index: number, context: ScreenContext): boolean;
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
// GAIA-345 D8 — `Component/project-picker/` is GONE and its two survivors live
|
|
12
12
|
// here, in their only consumer: the candidate type and the one-column table the
|
|
13
13
|
// two surfaces are both made of.
|
|
14
|
+
import { actionItem } from '../../../Component/footer-actions/index.js';
|
|
14
15
|
import { lineText, } from '../../../Component/section-list/index.js';
|
|
15
|
-
import { columnNode, textNode } from '../../../lib/vnode.js';
|
|
16
16
|
import { defineContext } from '../../../Routing/context.js';
|
|
17
|
-
import {
|
|
17
|
+
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
18
18
|
import { listRowIds, listTableRows, listTableTree, } from '../../collection.js';
|
|
19
19
|
/**
|
|
20
20
|
* The picker as a configuration (GAIA-345 D8) — one column, and NO HEADER.
|
|
@@ -54,7 +54,11 @@ const clamp = (selected, length) => {
|
|
|
54
54
|
return 0;
|
|
55
55
|
return Math.max(0, Math.min(Math.trunc(selected), length - 1));
|
|
56
56
|
};
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Project picker — normal {@link AsyncScreen}. Candidates arrive via navigation
|
|
59
|
+
* context; refresh is the shared R / ui-refresh path.
|
|
60
|
+
*/
|
|
61
|
+
export class ProjectPickerScreen extends AsyncScreen {
|
|
58
62
|
frameId = 'project-picker';
|
|
59
63
|
topTab() {
|
|
60
64
|
return { level: 'root', tab: 'projects' };
|
|
@@ -69,47 +73,53 @@ export class ProjectPickerScreen extends BaseScreen {
|
|
|
69
73
|
: base,
|
|
70
74
|
};
|
|
71
75
|
id = ProjectPickerScreen.route.id;
|
|
72
|
-
candidates = [];
|
|
73
|
-
repo = '';
|
|
74
76
|
selectedIndex = 0;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
context.requestDraw();
|
|
77
|
+
reload(context) {
|
|
78
|
+
this.retry(context);
|
|
79
|
+
}
|
|
80
|
+
get candidates() {
|
|
81
|
+
return this.readyValue?.candidates ?? [];
|
|
82
|
+
}
|
|
83
|
+
get repo() {
|
|
84
|
+
return this.readyValue?.repo ?? '';
|
|
85
85
|
}
|
|
86
86
|
get rows() {
|
|
87
87
|
return this.candidates;
|
|
88
88
|
}
|
|
89
|
+
load(context) {
|
|
90
|
+
if (context.contexts?.has(projectPickerInputContext)) {
|
|
91
|
+
const input = context.contexts.get(projectPickerInputContext);
|
|
92
|
+
return Promise.resolve({
|
|
93
|
+
candidates: [...input.candidates],
|
|
94
|
+
repo: input.repo,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return Promise.resolve({ candidates: [], repo: '' });
|
|
98
|
+
}
|
|
99
|
+
renderLoading() {
|
|
100
|
+
return ['Loading projects…'];
|
|
101
|
+
}
|
|
89
102
|
/**
|
|
90
103
|
* The question this screen asks, and the one place it is written.
|
|
91
104
|
*
|
|
92
|
-
* Both surfaces open with it — the tree
|
|
93
|
-
*
|
|
94
|
-
* disagree about the repo they name.
|
|
105
|
+
* Both surfaces open with it — the tree via meta, the pipe as its first line —
|
|
106
|
+
* so composing it twice is how the two would come to disagree about the repo.
|
|
95
107
|
*/
|
|
96
108
|
headingText() {
|
|
97
109
|
return `Several gaia projects claim ${this.repo} — which one?`;
|
|
98
110
|
}
|
|
99
|
-
/** The candidate the cursor stands on, clamped: an out-of-range index must
|
|
100
|
-
* move the marker, never lose it. */
|
|
101
111
|
selected() {
|
|
102
112
|
return this.candidates[clamp(this.selectedIndex, this.candidates.length)];
|
|
103
113
|
}
|
|
114
|
+
renderReady(data) {
|
|
115
|
+
return listTableTree(PICKER_TABLE, data.candidates, this.selected(), {
|
|
116
|
+
selectedIndex: clamp(this.selectedIndex, data.candidates.length),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
104
119
|
/**
|
|
105
120
|
* F5 — the pipe's own output, and it stays `string[]`.
|
|
106
121
|
*
|
|
107
|
-
* GAIA-345 D7/D8 — built from the SAME configuration the tree is
|
|
108
|
-
* `listTableRows` and `lineText`, plus the two heading lines and the trailing
|
|
109
|
-
* hint, which are the screen's and not rows of the list. The bytes are
|
|
110
|
-
* unchanged by construction rather than by a comparison somebody has to
|
|
111
|
-
* remember to make, and `tests/Entity/project/Screen/project-picker.test.ts`
|
|
112
|
-
* pins them line for line against the pre-conversion capture.
|
|
122
|
+
* GAIA-345 D7/D8 — built from the SAME configuration the tree is.
|
|
113
123
|
*/
|
|
114
124
|
renderNonInteractive() {
|
|
115
125
|
return [
|
|
@@ -120,46 +130,27 @@ export class ProjectPickerScreen extends BaseScreen {
|
|
|
120
130
|
'Run gaia ui project:<name> to choose directly.',
|
|
121
131
|
];
|
|
122
132
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
*
|
|
126
|
-
* The two heading rows are composed AROUND the table's node because they are
|
|
127
|
-
* not rows of it — they name no candidate, they are not selectable, and they
|
|
128
|
-
* carry no entry in `rowIds`. That is also what the old `selRow` offset of 2
|
|
129
|
-
* was compensating for before the kernel started finding the cursor by node.
|
|
130
|
-
*
|
|
131
|
-
* MOUNTED WHOLE — no window of its own, and the bound is named rather than
|
|
132
|
-
* assumed: a row is one `gaia_project` that claims the cwd's remote, which is
|
|
133
|
-
* the multisite topology GAIA-126/137 designs for. That is a handful of sites
|
|
134
|
-
* per repo, against `@opentui/core@0.5.1`'s ceiling of 16 382 renderables.
|
|
135
|
-
*/
|
|
136
|
-
surface() {
|
|
137
|
-
return columnNode('project-picker', textNode({
|
|
138
|
-
id: 'project-picker-heading',
|
|
139
|
-
content: this.headingText(),
|
|
140
|
-
}), textNode({ id: 'project-picker-heading-gap', content: '' }), listTableTree(PICKER_TABLE, this.candidates, this.selected(), {
|
|
141
|
-
selectedIndex: clamp(this.selectedIndex, this.candidates.length),
|
|
142
|
-
}));
|
|
143
|
-
}
|
|
144
|
-
present(_context) {
|
|
145
|
-
return presentation(this.surface(), {
|
|
133
|
+
chrome(_context) {
|
|
134
|
+
return {
|
|
146
135
|
selectable: this.candidates.length,
|
|
147
136
|
selectedIndex: this.selectedIndex,
|
|
148
|
-
// The node a click on each candidate lands on (GAIA-340 Task 11), read
|
|
149
|
-
// off the SAME configuration that keys the rows — which retires the
|
|
150
|
-
// hand-spelled `project-picker-candidate-<id>` the screen used to publish
|
|
151
|
-
// beside a row keyed the same way by a second expression (GAIA-345 D2).
|
|
152
137
|
rowIds: listRowIds(PICKER_TABLE, this.candidates),
|
|
153
138
|
title: 'Which project?',
|
|
154
|
-
meta: `${this.candidates.length} project(s) claim ${this.repo}
|
|
155
|
-
|
|
156
|
-
|
|
139
|
+
meta: `${this.candidates.length} project(s) claim ${this.repo} · ${this.headingText()}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
actions() {
|
|
143
|
+
return [actionItem('picker-enter', 'Enter', 'open')];
|
|
157
144
|
}
|
|
158
145
|
handleInput(input, context) {
|
|
146
|
+
if (super.handleInput(input, context))
|
|
147
|
+
return true;
|
|
159
148
|
if (input === 'j' || input === 'down')
|
|
160
149
|
this.selectedIndex = Math.min(this.selectedIndex + 1, this.candidates.length - 1);
|
|
161
150
|
else if (input === 'k' || input === 'up')
|
|
162
151
|
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
|
152
|
+
else if (input === 'enter')
|
|
153
|
+
return this.activate(this.selectedIndex, context);
|
|
163
154
|
else
|
|
164
155
|
return false;
|
|
165
156
|
context.requestDraw();
|
|
@@ -184,8 +175,7 @@ export class ProjectPickerScreen extends BaseScreen {
|
|
|
184
175
|
return true;
|
|
185
176
|
}
|
|
186
177
|
reset() {
|
|
187
|
-
this.candidates = [];
|
|
188
|
-
this.repo = '';
|
|
189
178
|
this.selectedIndex = 0;
|
|
179
|
+
super.reset();
|
|
190
180
|
}
|
|
191
181
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { VNode } from '@opentui/core';
|
|
2
2
|
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
3
|
-
import {
|
|
3
|
+
import type { FullScreenChrome } from '../../../Screen/full-screen.js';
|
|
4
|
+
import type { ScreenContext } from '../../../Screen/screen.js';
|
|
4
5
|
import { type ProjectRow } from '../Data/projects.js';
|
|
5
6
|
/**
|
|
6
7
|
* GAIA-340 D1 — the ready page is a renderable TREE; `loading` and `error` are
|
|
@@ -44,7 +45,8 @@ export declare class ProjectsScreen extends AsyncScreen<ProjectRow[]> {
|
|
|
44
45
|
protected load(context: ScreenContext): Promise<ProjectRow[]>;
|
|
45
46
|
protected renderLoading(): string[];
|
|
46
47
|
protected renderReady(rows: ProjectRow[]): VNode;
|
|
47
|
-
|
|
48
|
+
protected chrome(context: ScreenContext): FullScreenChrome;
|
|
49
|
+
protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
|
|
48
50
|
activate(index: number, context: ScreenContext): boolean;
|
|
49
51
|
select(index: number, context: ScreenContext): boolean;
|
|
50
52
|
handleInput(input: string, context: ScreenContext): boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { actionItem } from '../../../Component/footer-actions/index.js';
|
|
1
2
|
import { ListCursor } from '../../../lib/cursor.js';
|
|
2
3
|
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
3
|
-
import { presentation } from '../../../Screen/screen.js';
|
|
4
4
|
import { listRowIds, listTableTree } from '../../collection.js';
|
|
5
5
|
import { loadProjectRows, PROJECT_TABLE, } from '../Data/projects.js';
|
|
6
6
|
/**
|
|
@@ -69,19 +69,22 @@ export class ProjectsScreen extends AsyncScreen {
|
|
|
69
69
|
selectedIndex: this.selectedIndex,
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
chrome(context) {
|
|
73
73
|
const rows = this.rows;
|
|
74
|
-
return
|
|
74
|
+
return {
|
|
75
75
|
selectable: rows.length,
|
|
76
76
|
selectedIndex: this.selectedIndex,
|
|
77
|
-
//
|
|
78
|
-
// SAME configuration that keys the rows — so the click target and the row
|
|
79
|
-
// node cannot be spelled two ways (GAIA-345 D2).
|
|
77
|
+
// Click targets match listTableTree row ids (GAIA-345 D2).
|
|
80
78
|
rowIds: listRowIds(PROJECT_TABLE, rows),
|
|
81
79
|
title: 'GAIA projects',
|
|
82
80
|
meta: `${rows.length} registered project(s) · ${context.services?.baseUrl ?? ''}`,
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
actions() {
|
|
84
|
+
return [
|
|
85
|
+
actionItem('projects-new', 'n', 'new'),
|
|
86
|
+
actionItem('projects-tab', 'Tab', 'tabs'),
|
|
87
|
+
];
|
|
85
88
|
}
|
|
86
89
|
activate(index, context) {
|
|
87
90
|
const row = this.readyValue?.[index];
|
|
@@ -8,12 +8,20 @@
|
|
|
8
8
|
//
|
|
9
9
|
// GAIA-340 D1 — the FIRST block to hand its content over as renderables rather
|
|
10
10
|
// than as rows of text: the sections are built once, keyed, and turned into a
|
|
11
|
-
// VNode tree.
|
|
12
|
-
//
|
|
11
|
+
// VNode tree.
|
|
12
|
+
//
|
|
13
|
+
// GAIA-372 D9 — IT IS `markdownRow`'S ONLY CALLER NOW, and the ATX-only rule is
|
|
14
|
+
// THIS SCREEN'S rule rather than a rule shared with the ticket detail. A run log
|
|
15
|
+
// entry is MACHINE OUTPUT, not authored Markdown: an agent line full of `*`, `_`
|
|
16
|
+
// and backticks would be re-styled and have characters deleted by a renderer
|
|
17
|
+
// meant for prose. So the log keeps the narrow rule while the ticket detail's
|
|
18
|
+
// `gaia_rich` bodies go through `Component/markdown/`. Nothing about this screen's
|
|
19
|
+
// behaviour changed; routing the log through the full subset is a separate
|
|
20
|
+
// ticket, not a silent side effect of that one.
|
|
13
21
|
//
|
|
14
22
|
// Read-only by design (the ticket's own out-of-scope list): no write, no edit, no
|
|
15
23
|
// navigation out of it except Esc.
|
|
16
|
-
import { markdownRow } from '../../../../Component/
|
|
24
|
+
import { markdownRow } from '../../../../Component/markdown/index.js';
|
|
17
25
|
import { asString } from '../../../../lib/format.js';
|
|
18
26
|
import { columnNode, textNode } from '../../../../lib/vnode.js';
|
|
19
27
|
/** The sideloaded `gaia_run` with this id, or undefined. */
|
|
@@ -208,10 +216,10 @@ function flatRows(document, runId) {
|
|
|
208
216
|
{ key: `section-${section.key}`, text: section.heading, bold: true },
|
|
209
217
|
...section.rows.map((entry) => ({
|
|
210
218
|
key: entry.key,
|
|
211
|
-
// The
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
219
|
+
// The log's OWN minimal-markdown rule (GAIA-372 D9): an ATX heading inside
|
|
220
|
+
// a log entry renders bold with its markers stripped, and a log line
|
|
221
|
+
// starting with `#` must not render one way on screen and another way in a
|
|
222
|
+
// pipe. Nothing else in an entry is parsed, deliberately — see the header.
|
|
215
223
|
...markdownRow(entry.text),
|
|
216
224
|
})),
|
|
217
225
|
// The blank row between two sections, which the flattened surface pushed
|
|
@@ -20,3 +20,13 @@ export declare function buildRunQueryParams(projectId: string, criteria: RunCrit
|
|
|
20
20
|
size: number;
|
|
21
21
|
index: number;
|
|
22
22
|
}): DrupalJsonApiParams;
|
|
23
|
+
/**
|
|
24
|
+
* Build the JSON:API query for one assignee's runs (GAIA-377 D2).
|
|
25
|
+
*
|
|
26
|
+
* Runs carry no user of their own — attribution is
|
|
27
|
+
* `ticket_id.assignee_user_id.id`, matching StatisticsCounter::SOURCES.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildUserRunQueryParams(userId: string, criteria: RunCriteria, page?: {
|
|
30
|
+
size: number;
|
|
31
|
+
index: number;
|
|
32
|
+
}): DrupalJsonApiParams;
|
|
@@ -25,6 +25,22 @@ export function describeCriteria(criteria) {
|
|
|
25
25
|
export function buildRunQueryParams(projectId, criteria, page = { size: LIST_PAGE_SIZE, index: 0 }) {
|
|
26
26
|
const params = new DrupalJsonApiParams();
|
|
27
27
|
params.addFilter('ticket_id.project_id.id', projectId);
|
|
28
|
+
applyRunCriteria(params, criteria, page);
|
|
29
|
+
return params;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the JSON:API query for one assignee's runs (GAIA-377 D2).
|
|
33
|
+
*
|
|
34
|
+
* Runs carry no user of their own — attribution is
|
|
35
|
+
* `ticket_id.assignee_user_id.id`, matching StatisticsCounter::SOURCES.
|
|
36
|
+
*/
|
|
37
|
+
export function buildUserRunQueryParams(userId, criteria, page = { size: LIST_PAGE_SIZE, index: 0 }) {
|
|
38
|
+
const params = new DrupalJsonApiParams();
|
|
39
|
+
params.addFilter('ticket_id.assignee_user_id.id', userId);
|
|
40
|
+
applyRunCriteria(params, criteria, page);
|
|
41
|
+
return params;
|
|
42
|
+
}
|
|
43
|
+
function applyRunCriteria(params, criteria, page) {
|
|
28
44
|
if (criteria.closed !== null)
|
|
29
45
|
params.addFilter('closed', criteria.closed ? '1' : '0');
|
|
30
46
|
params.addInclude(['ticket_id']);
|
|
@@ -37,5 +53,4 @@ export function buildRunQueryParams(projectId, criteria, page = { size: LIST_PAG
|
|
|
37
53
|
// Page one carries no offset, so its query is the shortest it can be.
|
|
38
54
|
if (page.index > 0)
|
|
39
55
|
params.addPageOffset(page.index * page.size);
|
|
40
|
-
return params;
|
|
41
56
|
}
|