@gaia-ai/addon-gaia-ui 0.6.4 → 0.6.5
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/Component/agent-list/index.d.ts +6 -0
- package/dist/src/Agent/Component/agent-list/index.js +14 -0
- package/dist/src/Agent/launch.d.ts +25 -0
- package/dist/src/Agent/launch.js +49 -0
- package/dist/src/Component/tab-bar/index.d.ts +11 -0
- package/dist/src/Component/tab-bar/index.js +21 -1
- package/dist/src/Component/ticket-detail/index.d.ts +8 -0
- package/dist/src/Component/ticket-detail/index.js +68 -4
- package/dist/src/Component/top-tabs/index.d.ts +117 -3
- package/dist/src/Component/top-tabs/index.js +124 -8
- package/dist/src/Entity/conductor/Data/conductors.d.ts +22 -0
- package/dist/src/Entity/conductor/Data/conductors.js +17 -0
- package/dist/src/Entity/conductor/Data/liveness.d.ts +10 -0
- package/dist/src/Entity/conductor/Data/liveness.js +25 -0
- package/dist/src/Entity/project/Component/dashboard-list/index.d.ts +8 -0
- package/dist/src/Entity/project/Component/dashboard-list/index.js +7 -0
- package/dist/src/Entity/project/Component/project-picker/index.d.ts +15 -0
- package/dist/src/Entity/project/Component/project-picker/index.js +42 -0
- package/dist/src/Entity/project/Data/projects.d.ts +21 -0
- package/dist/src/Entity/project/Data/projects.js +81 -0
- package/dist/src/Entity/project/Data/with-tickets.d.ts +20 -0
- package/dist/src/Entity/project/Data/with-tickets.js +36 -0
- package/dist/src/Entity/run/Component/run-table/index.d.ts +10 -0
- package/dist/src/Entity/run/Component/run-table/index.js +20 -0
- package/dist/src/Entity/run/Data/active-run.d.ts +15 -0
- package/dist/src/Entity/run/Data/active-run.js +23 -0
- package/dist/src/Entity/run/Data/runs.d.ts +35 -0
- package/dist/src/Entity/run/Data/runs.js +62 -0
- package/dist/src/Entity/run/Data/status.d.ts +12 -0
- package/dist/src/Entity/run/Data/status.js +40 -0
- package/dist/src/Entity/term/Data/terms.d.ts +9 -0
- package/dist/src/Entity/term/Data/terms.js +8 -0
- package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +21 -0
- package/dist/src/Entity/ticket/Component/comment-item/index.js +39 -0
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +3 -0
- package/dist/src/Entity/ticket/Component/create-form/index.js +26 -0
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +28 -0
- package/dist/src/Entity/ticket/Component/edit-form/index.js +78 -0
- package/dist/src/Entity/ticket/Component/tab-bar/index.d.ts +20 -0
- package/dist/src/Entity/ticket/Component/tab-bar/index.js +44 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +65 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +240 -0
- package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +5 -0
- package/dist/src/Entity/ticket/Component/ticket-table/index.js +14 -0
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +7 -0
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -0
- package/dist/src/Entity/ticket/Data/detail.d.ts +12 -0
- package/dist/src/Entity/ticket/Data/detail.js +13 -0
- package/dist/src/Entity/ticket/Data/query.d.ts +20 -0
- package/dist/src/Entity/ticket/Data/query.js +66 -0
- package/dist/src/Entity/ticket/Data/tickets.d.ts +30 -0
- package/dist/src/Entity/ticket/Data/tickets.js +66 -0
- package/dist/src/Entity/ticket/Data/workflow.d.ts +13 -0
- package/dist/src/Entity/ticket/Data/workflow.js +26 -0
- package/dist/src/Entity/ticket/Data/write.d.ts +12 -0
- package/dist/src/Entity/ticket/Data/write.js +18 -0
- package/dist/src/Entity/ticket/Form/create-data.d.ts +9 -0
- package/dist/src/Entity/ticket/Form/create-data.js +49 -0
- package/dist/src/Entity/ticket/Form/create.d.ts +64 -0
- package/dist/src/Entity/ticket/Form/create.js +71 -0
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +9 -0
- package/dist/src/Entity/ticket/Form/edit-data.js +38 -0
- package/dist/src/Entity/ticket/Form/edit.d.ts +65 -0
- package/dist/src/Entity/ticket/Form/edit.js +61 -0
- package/dist/src/Entity/transport.d.ts +20 -0
- package/dist/src/Entity/transport.js +27 -0
- package/dist/src/Entity/user/Data/users.d.ts +20 -0
- package/dist/src/Entity/user/Data/users.js +19 -0
- package/dist/src/Form/field.d.ts +39 -0
- package/dist/src/Form/field.js +16 -0
- package/dist/src/Form/form.d.ts +40 -0
- package/dist/src/Form/form.js +85 -0
- package/dist/src/Form/index.d.ts +3 -0
- package/dist/src/Form/index.js +5 -0
- package/dist/src/Form/keys.d.ts +3 -0
- package/dist/src/Form/keys.js +40 -0
- package/dist/src/Kernel/tui-kernel.js +591 -199
- package/dist/src/launcher.d.ts +29 -5
- package/dist/src/launcher.js +8 -0
- package/dist/src/lib/ansi.d.ts +15 -0
- package/dist/src/lib/ansi.js +53 -9
- package/dist/src/lib/format.d.ts +21 -0
- package/dist/src/lib/format.js +46 -4
- package/dist/src/lib/hit-test.d.ts +119 -0
- package/dist/src/lib/hit-test.js +129 -0
- package/dist/src/lib/jsonapi-error.d.ts +7 -0
- package/dist/src/lib/jsonapi-error.js +43 -0
- package/dist/src/lib/mouse.d.ts +35 -0
- package/dist/src/lib/mouse.js +89 -0
- package/dist/src/lib/width-table.d.ts +4 -0
- package/dist/src/lib/width-table.js +147 -0
- package/dist/src/lib/width.d.ts +25 -0
- package/dist/src/lib/width.js +130 -0
- package/dist/src/plugin.js +1 -1
- package/dist/src/route.d.ts +11 -0
- package/dist/src/route.js +215 -0
- package/dist/src/types.d.ts +38 -1
- package/package.json +2 -2
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HostedAgent } from '../../../types.js';
|
|
2
|
+
/** A fixed-column flat table for the Ticket-Agents screen: header + one line per
|
|
3
|
+
* live agent (its label · the project/group it runs for). One physical row per
|
|
4
|
+
* entry, so the Kernel's `1 + index` selection math holds. The agents are the
|
|
5
|
+
* host's live set (from `list(group)`); a finished agent drops out on refresh. */
|
|
6
|
+
export declare function agentListRows(agents: HostedAgent[]): string[];
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { pad } from '../../../lib/format.js';
|
|
2
|
+
/** A fixed-column flat table for the Ticket-Agents screen: header + one line per
|
|
3
|
+
* live agent (its label · the project/group it runs for). One physical row per
|
|
4
|
+
* entry, so the Kernel's `1 + index` selection math holds. The agents are the
|
|
5
|
+
* host's live set (from `list(group)`); a finished agent drops out on refresh. */
|
|
6
|
+
export function agentListRows(agents) {
|
|
7
|
+
const header = `${pad('AGENT', 32)} PROJECT`;
|
|
8
|
+
if (agents.length === 0)
|
|
9
|
+
return [header, 'No ticket agents.'];
|
|
10
|
+
return [
|
|
11
|
+
header,
|
|
12
|
+
...agents.map((a) => `${pad(a.label || a.id, 32)} ${a.group}`),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CreateFormPayload } from '../Entity/ticket/Form/create.js';
|
|
2
|
+
import type { TuiAgentService } from '../types.js';
|
|
3
|
+
/** The launch intent passed to {@link TuiAgentService.launch}. */
|
|
4
|
+
export type CreateLaunch = Parameters<TuiAgentService['launch']>[0];
|
|
5
|
+
/**
|
|
6
|
+
* Build the `ticket:create` launch intent for a project. The prompt is a pure
|
|
7
|
+
* pointer to WORKFLOW.md (the pre-filled form is passed via
|
|
8
|
+
* `$GAIA_NEW_TICKET_INPUT`); `GAIA_ID` is never inherited (a create must not be
|
|
9
|
+
* bound to a claimed ticket).
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildCreateLaunch(opts: {
|
|
12
|
+
project: string;
|
|
13
|
+
cwd: string;
|
|
14
|
+
specPath?: string | undefined;
|
|
15
|
+
workflow?: string | undefined;
|
|
16
|
+
createPrompt?: string | undefined;
|
|
17
|
+
label?: string | undefined;
|
|
18
|
+
}): CreateLaunch;
|
|
19
|
+
/**
|
|
20
|
+
* A short human label for the agent list: the description's first line (capped to 40
|
|
21
|
+
* incl. the ellipsis), else "New <workflow>". GAIA-236 AC-8: it builds the `label`
|
|
22
|
+
* `buildCreateLaunch` takes, so it belongs to the module that builds the launch
|
|
23
|
+
* intent — moved from `Service/create-form.ts` unchanged.
|
|
24
|
+
*/
|
|
25
|
+
export declare function shortName(payload: CreateFormPayload): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Ticket-create launch request (AC-3). Pressing `n` starts an embedded agent
|
|
2
|
+
// that runs the GAIA `ticket:create` workflow. This module is pure: it builds
|
|
3
|
+
// the launch intent (prompt + env) the TUI hands to the injected agent host
|
|
4
|
+
// (`services.agents.launch`); the host (herdr) owns transport + layout. No
|
|
5
|
+
// node-pty / @xterm, no session object.
|
|
6
|
+
const CREATE_PROMPT = 'Read the repository\'s WORKFLOW.md and follow its "Create ticket" section to ' +
|
|
7
|
+
'create a new GAIA ticket. Do not start work on the new ticket.';
|
|
8
|
+
/**
|
|
9
|
+
* Build the `ticket:create` launch intent for a project. The prompt is a pure
|
|
10
|
+
* pointer to WORKFLOW.md (the pre-filled form is passed via
|
|
11
|
+
* `$GAIA_NEW_TICKET_INPUT`); `GAIA_ID` is never inherited (a create must not be
|
|
12
|
+
* bound to a claimed ticket).
|
|
13
|
+
*/
|
|
14
|
+
export function buildCreateLaunch(opts) {
|
|
15
|
+
const env = {
|
|
16
|
+
...process.env,
|
|
17
|
+
TERM: 'xterm-256color',
|
|
18
|
+
GAIA_PROJECT: opts.project,
|
|
19
|
+
};
|
|
20
|
+
delete env.GAIA_ID;
|
|
21
|
+
if (opts.specPath)
|
|
22
|
+
env.GAIA_NEW_TICKET_INPUT = opts.specPath;
|
|
23
|
+
if (opts.workflow)
|
|
24
|
+
env.GAIA_WORKFLOW = opts.workflow;
|
|
25
|
+
return {
|
|
26
|
+
group: opts.project,
|
|
27
|
+
cwd: opts.cwd,
|
|
28
|
+
prompt: opts.createPrompt?.trim() || CREATE_PROMPT,
|
|
29
|
+
env,
|
|
30
|
+
isNew: true,
|
|
31
|
+
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A short human label for the agent list: the description's first line (capped to 40
|
|
36
|
+
* incl. the ellipsis), else "New <workflow>". GAIA-236 AC-8: it builds the `label`
|
|
37
|
+
* `buildCreateLaunch` takes, so it belongs to the module that builds the launch
|
|
38
|
+
* intent — moved from `Service/create-form.ts` unchanged.
|
|
39
|
+
*/
|
|
40
|
+
export function shortName(payload) {
|
|
41
|
+
const firstLine = payload.description
|
|
42
|
+
.split('\n')
|
|
43
|
+
.map((l) => l.trim())
|
|
44
|
+
.find((l) => l.length > 0);
|
|
45
|
+
if (firstLine) {
|
|
46
|
+
return firstLine.length > 40 ? `${firstLine.slice(0, 39)}…` : firstLine;
|
|
47
|
+
}
|
|
48
|
+
return `New ${payload.workflow?.label ?? 'ticket'}`;
|
|
49
|
+
}
|
|
@@ -7,3 +7,14 @@ export interface DetailTab {
|
|
|
7
7
|
export declare const DETAIL_TABS: DetailTab[];
|
|
8
8
|
/** The tab bar as a single row; the active tab is bracketed. */
|
|
9
9
|
export declare function tabBar(activeIndex: number): string;
|
|
10
|
+
/** Where one detail tab sits inside the bar: `[start, end)` in display columns
|
|
11
|
+
* from the bar's own first column. */
|
|
12
|
+
export interface DetailTabSpan {
|
|
13
|
+
index: number;
|
|
14
|
+
start: number;
|
|
15
|
+
end: number;
|
|
16
|
+
}
|
|
17
|
+
/** The bar's own hit map, built from the SAME `segment`/`SEPARATOR` the rendered
|
|
18
|
+
* bar is joined from — see `Component/top-tabs/index.ts`'s `topTabSpans` for the
|
|
19
|
+
* full argument. */
|
|
20
|
+
export declare function detailTabSpans(activeIndex: number): DetailTabSpan[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { displayWidth } from '../../lib/width.js';
|
|
1
2
|
// The ticket-detail screen is split into tabs so only one tab's content draws at
|
|
2
3
|
// a time (keeps the fixed-height window small) — Sub-tickets + Referenced share a
|
|
3
4
|
// Related tab, Comments is its own tab with collapsible entries (owned by the
|
|
@@ -16,7 +17,26 @@ export const DETAIL_TABS = [
|
|
|
16
17
|
},
|
|
17
18
|
{ id: 'runs', label: 'Runs', headings: ['Runs'] },
|
|
18
19
|
];
|
|
20
|
+
/** What joins two tabs in the rendered bar — part of neither tab's span. */
|
|
21
|
+
const SEPARATOR = ' ';
|
|
22
|
+
/** One tab's rendered segment: bracketed when active, space-padded otherwise. */
|
|
23
|
+
const segment = (tab, index, activeIndex) => index === activeIndex ? `[${tab.label}]` : ` ${tab.label} `;
|
|
19
24
|
/** The tab bar as a single row; the active tab is bracketed. */
|
|
20
25
|
export function tabBar(activeIndex) {
|
|
21
|
-
return DETAIL_TABS.map((t, i) => i
|
|
26
|
+
return DETAIL_TABS.map((t, i) => segment(t, i, activeIndex)).join(SEPARATOR);
|
|
27
|
+
}
|
|
28
|
+
/** The bar's own hit map, built from the SAME `segment`/`SEPARATOR` the rendered
|
|
29
|
+
* bar is joined from — see `Component/top-tabs/index.ts`'s `topTabSpans` for the
|
|
30
|
+
* full argument. */
|
|
31
|
+
export function detailTabSpans(activeIndex) {
|
|
32
|
+
const spans = [];
|
|
33
|
+
let start = 0;
|
|
34
|
+
DETAIL_TABS.forEach((tab, index) => {
|
|
35
|
+
if (index > 0)
|
|
36
|
+
start += displayWidth(SEPARATOR);
|
|
37
|
+
const width = displayWidth(segment(tab, index, activeIndex));
|
|
38
|
+
spans.push({ index, start, end: start + width });
|
|
39
|
+
start += width;
|
|
40
|
+
});
|
|
41
|
+
return spans;
|
|
22
42
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { TicketDetailDocument } from '../../types.js';
|
|
2
2
|
import type { CommentItem } from '../comment-item/index.js';
|
|
3
3
|
import type { Section } from '../section-list/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* The `expandedComments` set a freshly opened ticket detail starts with: the
|
|
6
|
+
* newest comment expanded, every older one collapsed.
|
|
7
|
+
*
|
|
8
|
+
* It lives beside the sort on purpose — "index 0 is the newest" is one module's
|
|
9
|
+
* invariant, so the kernel never needs to know which index that is.
|
|
10
|
+
*/
|
|
11
|
+
export declare function initialExpandedComments(document: TicketDetailDocument): Set<number>;
|
|
4
12
|
/**
|
|
5
13
|
* The ticket detail sections, each with an explicit empty state:
|
|
6
14
|
* Description, Comments, Sub-tickets, Runs, Referenced tickets, Links,
|
|
@@ -19,8 +19,71 @@ function refCount(document, name) {
|
|
|
19
19
|
const data = document.data?.relationships?.[name]?.data;
|
|
20
20
|
return Array.isArray(data) ? data.length : data ? 1 : 0;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Newest-first — the exact reversal of the server's total order
|
|
24
|
+
* (`CommentsItemList`: `created ASC, id ASC`), three keys applied in turn:
|
|
25
|
+
* `created` DESC, then `drupal_internal__id` DESC (the server's own monotonic
|
|
26
|
+
* key, so two comments written in the same second keep the server's relative
|
|
27
|
+
* order, reversed), then the resource `id` DESC.
|
|
28
|
+
*
|
|
29
|
+
* The third key exists to make the comparator TOTAL rather than leaning on
|
|
30
|
+
* `Array.prototype.sort` being stable in modern V8: the order must be
|
|
31
|
+
* *deterministic* — identical for the same comments in any input order, and
|
|
32
|
+
* defined even for a fixture or partial include that carries no
|
|
33
|
+
* `drupal_internal__id`. Stability would only preserve whatever order the
|
|
34
|
+
* include happened to arrive in.
|
|
35
|
+
*
|
|
36
|
+
* A missing or unparseable `created` sorts LAST (treated as the oldest), so a
|
|
37
|
+
* malformed row can never take the top slot and steal the default expansion.
|
|
38
|
+
*/
|
|
39
|
+
function compareCommentsNewestFirst(a, b) {
|
|
40
|
+
const at = Date.parse(asString(a.attributes?.created) ?? '');
|
|
41
|
+
const bt = Date.parse(asString(b.attributes?.created) ?? '');
|
|
42
|
+
const aUnknown = Number.isNaN(at);
|
|
43
|
+
const bUnknown = Number.isNaN(bt);
|
|
44
|
+
if (aUnknown !== bUnknown)
|
|
45
|
+
return aUnknown ? 1 : -1;
|
|
46
|
+
if (!aUnknown && at !== bt)
|
|
47
|
+
return bt - at;
|
|
48
|
+
const ai = Number(a.attributes?.drupal_internal__id);
|
|
49
|
+
const bi = Number(b.attributes?.drupal_internal__id);
|
|
50
|
+
if (!Number.isNaN(ai) && !Number.isNaN(bi) && ai !== bi)
|
|
51
|
+
return bi - ai;
|
|
52
|
+
// Code-unit comparison, not localeCompare: a locale-sensitive collation would
|
|
53
|
+
// put the "deterministic" back in the hands of the runtime's locale data.
|
|
54
|
+
const aid = a.id ?? '';
|
|
55
|
+
const bid = b.id ?? '';
|
|
56
|
+
return aid === bid ? 0 : aid < bid ? 1 : -1;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The document's `gaia_comment` includes, NEWEST FIRST.
|
|
60
|
+
*
|
|
61
|
+
* This is the single function both renderings read — `detailSections`' Comments
|
|
62
|
+
* section (→ `detailLines`, the non-TTY render) and `commentItems` (→ the
|
|
63
|
+
* interactive Comments tab) — so newest-first is one line of code and the two
|
|
64
|
+
* orders structurally cannot drift.
|
|
65
|
+
*
|
|
66
|
+
* It sorts the copy `filter` already allocated and NEVER `document.included`
|
|
67
|
+
* itself: that array is mixed (comments *plus* sub-tickets, runs and referenced
|
|
68
|
+
* tickets) and shared with those consumers, so an in-place sort would be an
|
|
69
|
+
* invisible side effect on someone else's input.
|
|
70
|
+
*/
|
|
22
71
|
function commentResources(document) {
|
|
23
|
-
return (document.included ?? [])
|
|
72
|
+
return (document.included ?? [])
|
|
73
|
+
.filter((r) => r.type === 'gaia_comment--gaia_comment')
|
|
74
|
+
.sort(compareCommentsNewestFirst);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The `expandedComments` set a freshly opened ticket detail starts with: the
|
|
78
|
+
* newest comment expanded, every older one collapsed.
|
|
79
|
+
*
|
|
80
|
+
* It lives beside the sort on purpose — "index 0 is the newest" is one module's
|
|
81
|
+
* invariant, so the kernel never needs to know which index that is.
|
|
82
|
+
*/
|
|
83
|
+
export function initialExpandedComments(document) {
|
|
84
|
+
return commentResources(document).length
|
|
85
|
+
? new Set([0])
|
|
86
|
+
: new Set();
|
|
24
87
|
}
|
|
25
88
|
/**
|
|
26
89
|
* The ticket detail sections, each with an explicit empty state:
|
|
@@ -108,9 +171,10 @@ export function commentItems(document) {
|
|
|
108
171
|
return { type, author, created, bodyLines, preview: bodyLines[0] ?? '' };
|
|
109
172
|
});
|
|
110
173
|
}
|
|
111
|
-
// The whole ticket-detail document flattened (all sections)
|
|
112
|
-
//
|
|
113
|
-
//
|
|
174
|
+
// The whole ticket-detail document flattened (all sections) — the NON-TTY ticket
|
|
175
|
+
// render, printed verbatim by the kernel when stdout is not a terminal
|
|
176
|
+
// (`Kernel/tui-kernel.ts:147`), i.e. what `gaia ui GAIA-nnn | cat` shows. The
|
|
177
|
+
// interactive screen renders one tab at a time via tabViewLines instead.
|
|
114
178
|
export function detailLines(detail) {
|
|
115
179
|
return flattenSections(detailSections(detail ?? {}));
|
|
116
180
|
}
|
|
@@ -3,6 +3,120 @@ export interface TopTab {
|
|
|
3
3
|
id: string;
|
|
4
4
|
label: string;
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/** The root level: what there is before a project is chosen. */
|
|
7
|
+
export declare const ROOT_TABS: readonly [{
|
|
8
|
+
readonly id: "projects";
|
|
9
|
+
readonly label: "Projects";
|
|
10
|
+
}];
|
|
11
|
+
/** The project level: the tabs of an opened project. */
|
|
12
|
+
export declare const PROJECT_TABS: readonly [{
|
|
13
|
+
readonly id: "tickets";
|
|
14
|
+
readonly label: "Tickets";
|
|
15
|
+
}, {
|
|
16
|
+
readonly id: "ticket-agents";
|
|
17
|
+
readonly label: "Ticket-Agents";
|
|
18
|
+
}, {
|
|
19
|
+
readonly id: "runs";
|
|
20
|
+
readonly label: "Runs";
|
|
21
|
+
}];
|
|
22
|
+
/** The tab bars, keyed by level — the one place a level is named. */
|
|
23
|
+
export declare const TAB_LEVELS: {
|
|
24
|
+
readonly root: readonly [{
|
|
25
|
+
readonly id: "projects";
|
|
26
|
+
readonly label: "Projects";
|
|
27
|
+
}];
|
|
28
|
+
readonly project: readonly [{
|
|
29
|
+
readonly id: "tickets";
|
|
30
|
+
readonly label: "Tickets";
|
|
31
|
+
}, {
|
|
32
|
+
readonly id: "ticket-agents";
|
|
33
|
+
readonly label: "Ticket-Agents";
|
|
34
|
+
}, {
|
|
35
|
+
readonly id: "runs";
|
|
36
|
+
readonly label: "Runs";
|
|
37
|
+
}];
|
|
38
|
+
};
|
|
39
|
+
/** The name of a level that carries a top-level tab bar. */
|
|
40
|
+
export type TabLevel = keyof typeof TAB_LEVELS;
|
|
41
|
+
/** The ids of one level's tabs, derived from {@link TAB_LEVELS}. */
|
|
42
|
+
export type TabIdOf<L extends TabLevel> = (typeof TAB_LEVELS)[L][number]['id'];
|
|
43
|
+
/**
|
|
44
|
+
* The id of any top-level tab, derived — never re-listed. Before GAIA-239 this
|
|
45
|
+
* union was hand-written in `tui-kernel.ts` alongside a third hardcoded copy of
|
|
46
|
+
* the same ids for the Tab cycle order; the three could drift silently.
|
|
47
|
+
*/
|
|
48
|
+
export type TopTabId = {
|
|
49
|
+
[L in TabLevel]: TabIdOf<L>;
|
|
50
|
+
}[TabLevel];
|
|
51
|
+
/**
|
|
52
|
+
* Where a screen sits in the navigation: which bar it draws, and which tab of
|
|
53
|
+
* that bar is its own. A discriminated union over the levels, so a location can
|
|
54
|
+
* never pair one level's name with another level's tab id.
|
|
55
|
+
*/
|
|
56
|
+
export type TabLocation = {
|
|
57
|
+
[L in TabLevel]: {
|
|
58
|
+
level: L;
|
|
59
|
+
tab: TabIdOf<L>;
|
|
60
|
+
};
|
|
61
|
+
}[TabLevel];
|
|
62
|
+
/**
|
|
63
|
+
* One level's tab bar as a single row; the active tab is bracketed.
|
|
64
|
+
*
|
|
65
|
+
* The level is part of the signature on purpose: it is what keeps the id checked
|
|
66
|
+
* at compile time now that there is more than one bar. `topTabBar('root', 'runs')`
|
|
67
|
+
* is a TS2345 naming the root level's valid ids — not a bar that silently
|
|
68
|
+
* brackets nothing.
|
|
69
|
+
*/
|
|
70
|
+
export declare function topTabBar<L extends TabLevel>(level: L, activeId: TabIdOf<L>): string;
|
|
71
|
+
/**
|
|
72
|
+
* The same bar, for a caller holding a whole {@link TabLocation} rather than a
|
|
73
|
+
* literal level. The pairing was already checked when the location was built, so
|
|
74
|
+
* this needs no second check — and unlike `topTabBar(loc.level, loc.tab)` it does
|
|
75
|
+
* not ask the compiler to correlate two independently narrowed properties.
|
|
76
|
+
*/
|
|
77
|
+
export declare function tabBarFor(location: TabLocation): string;
|
|
78
|
+
/**
|
|
79
|
+
* Which level a tab id belongs to, as a whole {@link TabLocation} — or `null`
|
|
80
|
+
* for an id no level declares.
|
|
81
|
+
*
|
|
82
|
+
* The cast is the one place the level→id pairing is asserted rather than
|
|
83
|
+
* checked, and it is sound by construction: the id was just found **in that
|
|
84
|
+
* level's own registry**, which is exactly what `TabLocation` requires.
|
|
85
|
+
* TypeScript cannot see that through the `Object.keys` loop, so it is stated
|
|
86
|
+
* here once, behind a checked lookup, instead of at every call site.
|
|
87
|
+
*/
|
|
88
|
+
export declare function locateTab(tab: TopTabId): TabLocation | null;
|
|
89
|
+
/** Where one tab sits inside the bar: `[start, end)` in display columns from the
|
|
90
|
+
* bar's own first column. */
|
|
91
|
+
export interface TopTabSpan<Id extends string = TopTabId> {
|
|
92
|
+
id: Id;
|
|
93
|
+
start: number;
|
|
94
|
+
end: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* One level's own hit map, built from the SAME `segment`/`SEPARATOR` that level's
|
|
98
|
+
* rendered bar is joined from — so the layout and the click mapping are two
|
|
99
|
+
* readings of one expression and cannot drift apart. That is the argument
|
|
100
|
+
* `lib/hit-test.ts` makes for the body rows, applied to a column axis.
|
|
101
|
+
*
|
|
102
|
+
* Offsets are display columns, which is the unit a mouse report carries. For the
|
|
103
|
+
* current ASCII labels that coincides with string offsets, which is what lets
|
|
104
|
+
* the test slice `topTabBar()` with a span and compare.
|
|
105
|
+
*
|
|
106
|
+
* GAIA-239: the LEVEL is part of the signature, exactly as it is for
|
|
107
|
+
* {@link topTabBar}, and for the same reason twice over. GAIA-241 shipped one
|
|
108
|
+
* `topTabSpans(activeId)` over the single flat bar; read level-blind it would
|
|
109
|
+
* publish the project bar's three spans on the root screen too, so a click at
|
|
110
|
+
* the column where `Ticket-Agents` used to sit would open a tab that screen does
|
|
111
|
+
* not draw — the very defect this ticket removes, arriving through the mouse.
|
|
112
|
+
* A map and a bar of two different levels is now a TS2345, not a silent
|
|
113
|
+
* mis-click.
|
|
114
|
+
*/
|
|
115
|
+
export declare function topTabSpans<L extends TabLevel>(level: L, activeId: TabIdOf<L>): TopTabSpan<TabIdOf<L>>[];
|
|
116
|
+
/**
|
|
117
|
+
* The same hit map, for a caller holding a whole {@link TabLocation} — the
|
|
118
|
+
* companion of {@link tabBarFor}, and the pairing it needs was already checked
|
|
119
|
+
* when the location was built. Keeping the two in step is what makes "the bar you
|
|
120
|
+
* see is the bar you click" true by construction: both read `location`.
|
|
121
|
+
*/
|
|
122
|
+
export declare function tabSpansFor(location: TabLocation): TopTabSpan[];
|
|
@@ -1,13 +1,129 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
1
|
+
import { displayWidth } from '../../lib/width.js';
|
|
2
|
+
// GAIA-239: THE registry — the single place the set, the order and the labels of
|
|
3
|
+
// the top-level tabs are written. Navigation has THREE levels, and every screen
|
|
4
|
+
// shows exactly ONE tab bar: the one of its own level.
|
|
5
|
+
//
|
|
6
|
+
// root dashboard, project-picker → Projects
|
|
7
|
+
// project project, ticket-agents, project-runs → Tickets · Ticket-Agents · Runs
|
|
8
|
+
// ticket ticket → the DETAIL tabs, drawn as the
|
|
9
|
+
// header box's meta row, so no
|
|
10
|
+
// top-level bar at all
|
|
11
|
+
//
|
|
12
|
+
// `tickets` is the project/ticket drill-down; `ticket-agents` is the managed
|
|
13
|
+
// create-agent list (GAIA-190); `runs` is the project-scoped run overview
|
|
14
|
+
// (GAIA-192) — all three project-scoped, which is why they sit on the project
|
|
15
|
+
// level and not on the root one.
|
|
16
|
+
//
|
|
17
|
+
// Only tabs that are actually navigable are listed. A planned screen (`Users` on
|
|
18
|
+
// the root level, `Conductors` on the project level) arrives with its own ticket
|
|
19
|
+
// and adds its entry here then — a tab drawn before its screen exists would
|
|
20
|
+
// advertise navigation the operator cannot take, which is the very defect
|
|
21
|
+
// GAIA-239 removes.
|
|
22
|
+
//
|
|
23
|
+
// `as const satisfies` is load-bearing on both halves: `satisfies` still
|
|
24
|
+
// shape-checks every entry against `TopTab`, while `as const` keeps the literal
|
|
25
|
+
// ids so the id unions below can be DERIVED from them. A plain `: TopTab[]`
|
|
26
|
+
// annotation would widen `id` to `string` and the derivation would collapse to
|
|
27
|
+
// `string`, taking the compile-time check with it.
|
|
28
|
+
/** The root level: what there is before a project is chosen. */
|
|
29
|
+
export const ROOT_TABS = [
|
|
30
|
+
{ id: 'projects', label: 'Projects' },
|
|
31
|
+
];
|
|
32
|
+
/** The project level: the tabs of an opened project. */
|
|
33
|
+
export const PROJECT_TABS = [
|
|
6
34
|
{ id: 'tickets', label: 'Tickets' },
|
|
7
35
|
{ id: 'ticket-agents', label: 'Ticket-Agents' },
|
|
8
36
|
{ id: 'runs', label: 'Runs' },
|
|
9
37
|
];
|
|
10
|
-
/** The
|
|
11
|
-
export
|
|
12
|
-
|
|
38
|
+
/** The tab bars, keyed by level — the one place a level is named. */
|
|
39
|
+
export const TAB_LEVELS = {
|
|
40
|
+
root: ROOT_TABS,
|
|
41
|
+
project: PROJECT_TABS,
|
|
42
|
+
};
|
|
43
|
+
/** What joins two tabs in the rendered bar — part of neither tab's span. */
|
|
44
|
+
const SEPARATOR = ' ';
|
|
45
|
+
/** One tab's rendered segment: bracketed when active, space-padded otherwise. */
|
|
46
|
+
const segment = (tab, activeId) => tab.id === activeId ? `[${tab.label}]` : ` ${tab.label} `;
|
|
47
|
+
const renderBar = (tabs, activeId) => tabs.map((t) => segment(t, activeId)).join(SEPARATOR);
|
|
48
|
+
/**
|
|
49
|
+
* One level's tab bar as a single row; the active tab is bracketed.
|
|
50
|
+
*
|
|
51
|
+
* The level is part of the signature on purpose: it is what keeps the id checked
|
|
52
|
+
* at compile time now that there is more than one bar. `topTabBar('root', 'runs')`
|
|
53
|
+
* is a TS2345 naming the root level's valid ids — not a bar that silently
|
|
54
|
+
* brackets nothing.
|
|
55
|
+
*/
|
|
56
|
+
export function topTabBar(level, activeId) {
|
|
57
|
+
return renderBar(TAB_LEVELS[level], activeId);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The same bar, for a caller holding a whole {@link TabLocation} rather than a
|
|
61
|
+
* literal level. The pairing was already checked when the location was built, so
|
|
62
|
+
* this needs no second check — and unlike `topTabBar(loc.level, loc.tab)` it does
|
|
63
|
+
* not ask the compiler to correlate two independently narrowed properties.
|
|
64
|
+
*/
|
|
65
|
+
export function tabBarFor(location) {
|
|
66
|
+
return renderBar(TAB_LEVELS[location.level], location.tab);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Which level a tab id belongs to, as a whole {@link TabLocation} — or `null`
|
|
70
|
+
* for an id no level declares.
|
|
71
|
+
*
|
|
72
|
+
* The cast is the one place the level→id pairing is asserted rather than
|
|
73
|
+
* checked, and it is sound by construction: the id was just found **in that
|
|
74
|
+
* level's own registry**, which is exactly what `TabLocation` requires.
|
|
75
|
+
* TypeScript cannot see that through the `Object.keys` loop, so it is stated
|
|
76
|
+
* here once, behind a checked lookup, instead of at every call site.
|
|
77
|
+
*/
|
|
78
|
+
export function locateTab(tab) {
|
|
79
|
+
for (const level of Object.keys(TAB_LEVELS)) {
|
|
80
|
+
const tabs = TAB_LEVELS[level];
|
|
81
|
+
if (tabs.some((t) => t.id === tab)) {
|
|
82
|
+
return { level, tab };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const renderSpans = (tabs, activeId) => {
|
|
88
|
+
const spans = [];
|
|
89
|
+
let start = 0;
|
|
90
|
+
tabs.forEach((tab, index) => {
|
|
91
|
+
if (index > 0)
|
|
92
|
+
start += displayWidth(SEPARATOR);
|
|
93
|
+
const width = displayWidth(segment(tab, activeId));
|
|
94
|
+
spans.push({ id: tab.id, start, end: start + width });
|
|
95
|
+
start += width;
|
|
96
|
+
});
|
|
97
|
+
return spans;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* One level's own hit map, built from the SAME `segment`/`SEPARATOR` that level's
|
|
101
|
+
* rendered bar is joined from — so the layout and the click mapping are two
|
|
102
|
+
* readings of one expression and cannot drift apart. That is the argument
|
|
103
|
+
* `lib/hit-test.ts` makes for the body rows, applied to a column axis.
|
|
104
|
+
*
|
|
105
|
+
* Offsets are display columns, which is the unit a mouse report carries. For the
|
|
106
|
+
* current ASCII labels that coincides with string offsets, which is what lets
|
|
107
|
+
* the test slice `topTabBar()` with a span and compare.
|
|
108
|
+
*
|
|
109
|
+
* GAIA-239: the LEVEL is part of the signature, exactly as it is for
|
|
110
|
+
* {@link topTabBar}, and for the same reason twice over. GAIA-241 shipped one
|
|
111
|
+
* `topTabSpans(activeId)` over the single flat bar; read level-blind it would
|
|
112
|
+
* publish the project bar's three spans on the root screen too, so a click at
|
|
113
|
+
* the column where `Ticket-Agents` used to sit would open a tab that screen does
|
|
114
|
+
* not draw — the very defect this ticket removes, arriving through the mouse.
|
|
115
|
+
* A map and a bar of two different levels is now a TS2345, not a silent
|
|
116
|
+
* mis-click.
|
|
117
|
+
*/
|
|
118
|
+
export function topTabSpans(level, activeId) {
|
|
119
|
+
return renderSpans(TAB_LEVELS[level], activeId);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The same hit map, for a caller holding a whole {@link TabLocation} — the
|
|
123
|
+
* companion of {@link tabBarFor}, and the pairing it needs was already checked
|
|
124
|
+
* when the location was built. Keeping the two in step is what makes "the bar you
|
|
125
|
+
* see is the bar you click" true by construction: both read `location`.
|
|
126
|
+
*/
|
|
127
|
+
export function tabSpansFor(location) {
|
|
128
|
+
return renderSpans(TAB_LEVELS[location.level], location.tab);
|
|
13
129
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DropshClient } from '../../../types.js';
|
|
2
|
+
import { type OptionClients } from '../../transport.js';
|
|
3
|
+
/** One conductor of the control plane, as this renderer needs it. */
|
|
4
|
+
export interface Conductor {
|
|
5
|
+
id: string;
|
|
6
|
+
/** Never empty — the uuid is the last fallback, so nothing renders as blank. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** The machine the conductor runs on. */
|
|
9
|
+
machineId: string | undefined;
|
|
10
|
+
/** Liveness, as the control plane reports it. */
|
|
11
|
+
status: string | undefined;
|
|
12
|
+
/** The conductor's project, `null` when unlinked. */
|
|
13
|
+
projectId: string | null;
|
|
14
|
+
}
|
|
15
|
+
/** The conductor collection through one client, in server order. */
|
|
16
|
+
export declare const listConductors: (client: DropshClient) => Promise<Conductor[]>;
|
|
17
|
+
/**
|
|
18
|
+
* The same collection read through the WRITE client where there is one — still just
|
|
19
|
+
* reading, but the write scope is the one that decides what a write may reference,
|
|
20
|
+
* and `pm` sees the FULL conductor set where `session` sees a subset.
|
|
21
|
+
*/
|
|
22
|
+
export declare const listConductorsPreferringWrite: (clients: OptionClients) => Promise<Conductor[]>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { list, listPreferringWrite, } from '../../transport.js';
|
|
2
|
+
const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
3
|
+
const toConductor = (r) => ({
|
|
4
|
+
id: r.id,
|
|
5
|
+
label: str(r.attr('label')) ?? str(r.attr('machine_id')) ?? r.id,
|
|
6
|
+
machineId: str(r.attr('machine_id')),
|
|
7
|
+
status: str(r.attr('status')),
|
|
8
|
+
projectId: r.rel('project_id'),
|
|
9
|
+
});
|
|
10
|
+
/** The conductor collection through one client, in server order. */
|
|
11
|
+
export const listConductors = (client) => list(client, 'gaia_conductor', toConductor);
|
|
12
|
+
/**
|
|
13
|
+
* The same collection read through the WRITE client where there is one — still just
|
|
14
|
+
* reading, but the write scope is the one that decides what a write may reference,
|
|
15
|
+
* and `pm` sees the FULL conductor set where `session` sees a subset.
|
|
16
|
+
*/
|
|
17
|
+
export const listConductorsPreferringWrite = (clients) => listPreferringWrite(clients, 'gaia_conductor', toConductor);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The derived liveness state of a conductor. */
|
|
2
|
+
export type ConductorStatus = 'running' | 'stale' | 'offline' | 'registry-only';
|
|
3
|
+
/** The raw liveness inputs derived from a `gaia_conductor` record. */
|
|
4
|
+
export interface ConductorInput {
|
|
5
|
+
present?: boolean;
|
|
6
|
+
status?: string | null;
|
|
7
|
+
lastSeen?: number | null;
|
|
8
|
+
leaseExpiresAt?: number | null;
|
|
9
|
+
}
|
|
10
|
+
export declare function deriveConductorStatus(conductor: ConductorInput, nowSeconds: number): ConductorStatus;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Conductor liveness derivation. Mirrors the server-side ConductorDeriver
|
|
2
|
+
// (web/modules/custom/gaia_core/modules/gaia_dashboard/src/Dashboard/ConductorDeriver.php)
|
|
3
|
+
// and the CLI `classify()` in gaia-cli/conductor/src/commands/conductor.ts:
|
|
4
|
+
// no gaia_conductor record → registry-only; status 'offline' → offline; else
|
|
5
|
+
// stale when last_seen older than 600s OR the lease has expired; else running.
|
|
6
|
+
const STALE_WINDOW_SECONDS = 600;
|
|
7
|
+
export function deriveConductorStatus(conductor, nowSeconds) {
|
|
8
|
+
if (!conductor?.present)
|
|
9
|
+
return 'registry-only';
|
|
10
|
+
if (conductor.status === 'offline')
|
|
11
|
+
return 'offline';
|
|
12
|
+
if (conductor.lastSeen == null)
|
|
13
|
+
return 'stale';
|
|
14
|
+
if (nowSeconds - conductor.lastSeen > STALE_WINDOW_SECONDS)
|
|
15
|
+
return 'stale';
|
|
16
|
+
if (conductor.leaseExpiresAt != null &&
|
|
17
|
+
conductor.leaseExpiresAt < nowSeconds) {
|
|
18
|
+
return 'stale';
|
|
19
|
+
}
|
|
20
|
+
return 'running';
|
|
21
|
+
}
|
|
22
|
+
// `humanizeAge` moved to `lib/format.ts` (GAIA-236, entity-slice Phase 0): it is a
|
|
23
|
+
// generic relative-time formatter with no conductor vocabulary, and keeping it here
|
|
24
|
+
// is what made `Component/comment-item` and `Deriver/run-status` import a CONDUCTOR
|
|
25
|
+
// module. Its test moved with it, to `tests/lib/format.test.ts`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The minimum shape a dashboard row needs to render. */
|
|
2
|
+
export interface DashboardRowInput {
|
|
3
|
+
project: string;
|
|
4
|
+
derivedStatus: string;
|
|
5
|
+
ageLabel: string;
|
|
6
|
+
}
|
|
7
|
+
/** One line per project: name, derived status, relative age. */
|
|
8
|
+
export declare function dashboardRows(projects: DashboardRowInput[]): string[];
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { pad } from '../../../../lib/format.js';
|
|
2
|
+
/** One line per project: name, derived status, relative age. */
|
|
3
|
+
export function dashboardRows(projects) {
|
|
4
|
+
if (projects.length === 0)
|
|
5
|
+
return ['No projects.'];
|
|
6
|
+
return projects.map((p) => `${pad(p.project, 24)} ${pad(p.derivedStatus, 14)} ${p.ageLabel}`);
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A project that claims the repo. */
|
|
2
|
+
export interface PickerCandidate {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The picker screen's rows: a heading naming the repo, then one row per
|
|
8
|
+
* candidate with `▸` on the selected one.
|
|
9
|
+
*/
|
|
10
|
+
export declare function projectPickerRows(candidates: PickerCandidate[], repo: string, selected: number): string[];
|
|
11
|
+
/**
|
|
12
|
+
* The non-TTY form (spec Decision 11): the candidate list plus the direct
|
|
13
|
+
* command, printed once so `gaia ui --here | cat` stays scriptable — no prompt.
|
|
14
|
+
*/
|
|
15
|
+
export declare function projectPickerHint(candidates: PickerCandidate[], repo: string): string;
|