@gaia-ai/addon-gaia-ui 0.7.0 → 0.8.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/stats-panel/index.d.ts +13 -2
- package/dist/src/Component/stats-panel/index.js +28 -18
- package/dist/src/Component/top-tabs/index.d.ts +6 -0
- package/dist/src/Component/top-tabs/index.js +15 -6
- package/dist/src/Entity/collection.d.ts +98 -0
- package/dist/src/Entity/collection.js +46 -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 +57 -0
- package/dist/src/Entity/conductor/Data/conductors.js +67 -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 +42 -0
- package/dist/src/Entity/conductor/Screen/conductors.js +200 -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 +3 -0
- package/dist/src/Entity/project/Data/projects.js +3 -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 +36 -0
- package/dist/src/Entity/project/Screen/project-picker.js +90 -0
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +40 -0
- package/dist/src/Entity/project/Screen/projects-screen.js +103 -0
- 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 +85 -4
- package/dist/src/Entity/run/Data/runs.js +143 -27
- package/dist/src/Entity/run/Screen/run-detail.d.ts +25 -0
- package/dist/src/Entity/run/Screen/run-detail.js +51 -0
- package/dist/src/Entity/run/Screen/runs.d.ts +82 -0
- package/dist/src/Entity/run/Screen/runs.js +169 -0
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +42 -0
- package/dist/src/Entity/statistics/Screen/dashboard.js +177 -0
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +10 -0
- package/dist/src/Entity/ticket/Component/create-form/index.js +4 -0
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +3 -2
- package/dist/src/Entity/ticket/Component/edit-form/index.js +30 -8
- package/dist/src/Entity/ticket/Component/tab-bar/index.js +6 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +1 -1
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +33 -2
- 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/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 +105 -4
- package/dist/src/Entity/ticket/Data/tickets.js +164 -12
- 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 +68 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +336 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +81 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +200 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +177 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +45 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +135 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +61 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +300 -0
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +129 -0
- package/dist/src/Entity/ticket/Screen/tickets.js +507 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +27 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +154 -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 +5 -1
- package/dist/src/Form/keys.js +38 -5
- package/dist/src/Kernel/tui-kernel.d.ts +0 -10
- package/dist/src/Kernel/tui-kernel.js +250 -2384
- 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 +39 -0
- package/dist/src/Routing/route.js +1 -0
- package/dist/src/Routing/router.d.ts +33 -0
- package/dist/src/Routing/router.js +102 -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 +50 -0
- package/dist/src/Screen/async-screen.js +164 -0
- package/dist/src/Screen/list-screen.d.ts +158 -0
- package/dist/src/Screen/list-screen.js +237 -0
- package/dist/src/Screen/screen.d.ts +106 -0
- package/dist/src/Screen/screen.js +18 -0
- package/dist/src/Tui/controller.d.ts +13 -0
- package/dist/src/Tui/controller.js +1794 -0
- package/dist/src/Widget/text.js +49 -4
- package/dist/src/Widget/types.d.ts +2 -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/cursor.d.ts +31 -0
- package/dist/src/lib/cursor.js +45 -0
- package/dist/src/lib/format.d.ts +21 -0
- package/dist/src/lib/format.js +59 -0
- package/dist/src/lib/table.d.ts +37 -0
- package/dist/src/lib/table.js +20 -0
- package/dist/src/lib/width.d.ts +33 -1
- package/dist/src/lib/width.js +107 -12
- package/dist/src/plugin.js +19 -0
- package/dist/src/types.d.ts +26 -0
- package/dist/src/types.js +0 -4
- package/package.json +2 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Next value in a cyclic list; `null` (== "all") wraps at both ends. */
|
|
2
|
+
export declare function cycleValue<T>(values: readonly T[], current: T | null): T | null;
|
|
3
|
+
/** The closed cycle order; null ("all") wraps at both ends via cycleValue. */
|
|
4
|
+
export declare const CLOSED_FILTER_VALUES: readonly [false, true];
|
|
5
|
+
/**
|
|
6
|
+
* Rows per page in the project ticket and run lists (GAIA-292 D1).
|
|
7
|
+
*
|
|
8
|
+
* Lives here, beside the shared closed cycle, because BOTH slices need it and
|
|
9
|
+
* `Entity/cycle.ts` is the slice-less module they may each import without
|
|
10
|
+
* closing the G1 cycle.
|
|
11
|
+
*
|
|
12
|
+
* 20 since the operator's live review of GAIA-292; the spec's measurements and
|
|
13
|
+
* D6 were taken at 15, and the spec records the change rather than hiding it.
|
|
14
|
+
*
|
|
15
|
+
* The parent picker deliberately does NOT use this: it reads one un-paged bag of
|
|
16
|
+
* `PARENT_PAGE_SIZE` candidates and discloses truncation at that number.
|
|
17
|
+
*/
|
|
18
|
+
export declare const LIST_PAGE_SIZE = 20;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The shared list-cycling primitive. Slice-less like `transport.ts` — BOTH the
|
|
2
|
+
// ticket and the run slice import it, and the entity slices must stay acyclic
|
|
3
|
+
// (G1): a run → ticket import would close the project → ticket → run → ticket
|
|
4
|
+
// cycle (GAIA-282; the ticket query module re-exports both symbols so its
|
|
5
|
+
// established import sites keep working).
|
|
6
|
+
/** Next value in a cyclic list; `null` (== "all") wraps at both ends. */
|
|
7
|
+
export function cycleValue(values, current) {
|
|
8
|
+
if (current == null)
|
|
9
|
+
return values[0] ?? null;
|
|
10
|
+
const i = values.indexOf(current);
|
|
11
|
+
if (i < 0 || i === values.length - 1)
|
|
12
|
+
return null;
|
|
13
|
+
return values[i + 1] ?? null;
|
|
14
|
+
}
|
|
15
|
+
/** The closed cycle order; null ("all") wraps at both ends via cycleValue. */
|
|
16
|
+
export const CLOSED_FILTER_VALUES = [false, true];
|
|
17
|
+
/**
|
|
18
|
+
* Rows per page in the project ticket and run lists (GAIA-292 D1).
|
|
19
|
+
*
|
|
20
|
+
* Lives here, beside the shared closed cycle, because BOTH slices need it and
|
|
21
|
+
* `Entity/cycle.ts` is the slice-less module they may each import without
|
|
22
|
+
* closing the G1 cycle.
|
|
23
|
+
*
|
|
24
|
+
* 20 since the operator's live review of GAIA-292; the spec's measurements and
|
|
25
|
+
* D6 were taken at 15, and the spec records the change rather than hiding it.
|
|
26
|
+
*
|
|
27
|
+
* The parent picker deliberately does NOT use this: it reads one un-paged bag of
|
|
28
|
+
* `PARENT_PAGE_SIZE` candidates and discloses truncation at that number.
|
|
29
|
+
*/
|
|
30
|
+
export const LIST_PAGE_SIZE = 20;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DropshClient } from '../../../types.js';
|
|
2
|
+
export interface ResolvedProject {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
}
|
|
7
|
+
/** Resolve project identity without reading any project-owned collections. */
|
|
8
|
+
export declare function resolveProject(client: DropshClient, slug: string): Promise<ResolvedProject>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Resolve project identity without reading any project-owned collections. */
|
|
2
|
+
export async function resolveProject(client, slug) {
|
|
3
|
+
const document = await client
|
|
4
|
+
.collection('gaia_project')
|
|
5
|
+
.where('name', '=', slug)
|
|
6
|
+
.first();
|
|
7
|
+
return {
|
|
8
|
+
id: document?.id ?? slug,
|
|
9
|
+
name: document?.attr('name') ?? slug,
|
|
10
|
+
slug,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -3,6 +3,9 @@ import { type ConductorStatus } from '../../conductor/Data/liveness.js';
|
|
|
3
3
|
/** A derived project row: one distinct project + its liveliest conductor. */
|
|
4
4
|
export interface ProjectRow {
|
|
5
5
|
project: string;
|
|
6
|
+
projectId: string;
|
|
7
|
+
projectName: string;
|
|
8
|
+
projectSlug: string;
|
|
6
9
|
derivedStatus: ConductorStatus;
|
|
7
10
|
lastSeen: number | null;
|
|
8
11
|
ageLabel: string;
|
|
@@ -74,6 +74,9 @@ export async function loadProjectRows({ client, nowSeconds, }) {
|
|
|
74
74
|
}
|
|
75
75
|
return [...best.entries()].map(([project, b]) => ({
|
|
76
76
|
project,
|
|
77
|
+
projectId: [...projectNameById.entries()].find(([, name]) => name === project)?.[0] ?? project,
|
|
78
|
+
projectName: project,
|
|
79
|
+
projectSlug: project,
|
|
77
80
|
derivedStatus: b.derivedStatus,
|
|
78
81
|
lastSeen: b.lastSeen,
|
|
79
82
|
ageLabel: humanizeAge(b.lastSeen, now),
|
|
@@ -16,5 +16,10 @@ export interface ProjectWithTickets {
|
|
|
16
16
|
* The ticket listing delegates to {@link loadFilteredTickets} with empty
|
|
17
17
|
* criteria — one include-aware `client.get` path shared with the search/filter
|
|
18
18
|
* load, so both resolve parent/conductor identically (no dual-mapper drift).
|
|
19
|
+
*
|
|
20
|
+
* It takes the loader's FIRST page and drops the `hasNext` beside it (GAIA-292):
|
|
21
|
+
* this read has no paging affordance of its own — it is the overview header's
|
|
22
|
+
* ticket bag, not the paged Tickets tab — so the flag has no consumer here and
|
|
23
|
+
* carrying it would imply a control that does not exist.
|
|
19
24
|
*/
|
|
20
25
|
export declare function loadProjectWithTickets(client: DropshClient, projectSlug: string): Promise<ProjectWithTickets>;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// `loadFilteredTickets` so the search/filter path and this one resolve
|
|
7
7
|
// parent/conductor through the same mapper, with no dual-mapper drift.
|
|
8
8
|
import { loadFilteredTickets, } from '../../ticket/Data/tickets.js';
|
|
9
|
+
import { resolveProject } from './project.js';
|
|
9
10
|
/**
|
|
10
11
|
* Resolve a `gaia_project` by its slug (the registry `project`, which matches
|
|
11
12
|
* the project's `name`) and list its tickets, most-recently-changed first.
|
|
@@ -13,24 +14,25 @@ import { loadFilteredTickets, } from '../../ticket/Data/tickets.js';
|
|
|
13
14
|
* The ticket listing delegates to {@link loadFilteredTickets} with empty
|
|
14
15
|
* criteria — one include-aware `client.get` path shared with the search/filter
|
|
15
16
|
* load, so both resolve parent/conductor identically (no dual-mapper drift).
|
|
17
|
+
*
|
|
18
|
+
* It takes the loader's FIRST page and drops the `hasNext` beside it (GAIA-292):
|
|
19
|
+
* this read has no paging affordance of its own — it is the overview header's
|
|
20
|
+
* ticket bag, not the paged Tickets tab — so the flag has no consumer here and
|
|
21
|
+
* carrying it would imply a control that does not exist.
|
|
16
22
|
*/
|
|
17
23
|
export async function loadProjectWithTickets(client, projectSlug) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
.where('name', '=', projectSlug)
|
|
21
|
-
.first();
|
|
22
|
-
const projectId = projectRes?.id ?? projectSlug;
|
|
23
|
-
const tickets = await loadFilteredTickets(client, projectId, {
|
|
24
|
+
const project = await resolveProject(client, projectSlug);
|
|
25
|
+
const page = await loadFilteredTickets(client, project.id, {
|
|
24
26
|
query: '',
|
|
25
27
|
state: null,
|
|
26
|
-
|
|
28
|
+
// closed: null — this is the project overview read, not the ticket list;
|
|
29
|
+
// it keeps "show everything" (spec D6).
|
|
30
|
+
closed: null,
|
|
31
|
+
parent: null,
|
|
32
|
+
who: null,
|
|
27
33
|
});
|
|
28
34
|
return {
|
|
29
|
-
project
|
|
30
|
-
|
|
31
|
-
name: projectRes?.attr('name') ?? projectSlug,
|
|
32
|
-
slug: projectSlug,
|
|
33
|
-
},
|
|
34
|
-
tickets,
|
|
35
|
+
project,
|
|
36
|
+
tickets: page.rows,
|
|
35
37
|
};
|
|
36
38
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BaseScreen, type ScreenContext } from '../../../Screen/screen.js';
|
|
2
|
+
import { type PickerCandidate } from '../Component/project-picker/index.js';
|
|
3
|
+
export interface ProjectPickerInput {
|
|
4
|
+
readonly candidates: readonly PickerCandidate[];
|
|
5
|
+
readonly repo: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const projectPickerInputContext: import("../../../Routing/context.js").ContextKey<ProjectPickerInput>;
|
|
8
|
+
export declare class ProjectPickerScreen extends BaseScreen {
|
|
9
|
+
readonly frameId = "project-picker";
|
|
10
|
+
topTab(): {
|
|
11
|
+
readonly level: "root";
|
|
12
|
+
readonly tab: "projects";
|
|
13
|
+
};
|
|
14
|
+
static readonly route: {
|
|
15
|
+
id: string;
|
|
16
|
+
path: string;
|
|
17
|
+
parent: string;
|
|
18
|
+
fallback: {
|
|
19
|
+
routeId: string;
|
|
20
|
+
};
|
|
21
|
+
context: (_params: Readonly<Record<string, string>>, state: unknown, base: import("../../../Routing/context.js").ContextBag) => import("../../../Routing/context.js").ContextBag;
|
|
22
|
+
};
|
|
23
|
+
readonly id: string;
|
|
24
|
+
private candidates;
|
|
25
|
+
private repo;
|
|
26
|
+
selectedIndex: number;
|
|
27
|
+
enter(context: ScreenContext): void;
|
|
28
|
+
get rows(): readonly PickerCandidate[];
|
|
29
|
+
render(): string[];
|
|
30
|
+
renderNonInteractive(): string[];
|
|
31
|
+
present(_context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
|
|
32
|
+
handleInput(input: string, context: ScreenContext): boolean;
|
|
33
|
+
activate(index: number, context: ScreenContext): boolean;
|
|
34
|
+
select(index: number, context: ScreenContext): boolean;
|
|
35
|
+
reset(): void;
|
|
36
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { defineContext } from '../../../Routing/context.js';
|
|
2
|
+
import { BaseScreen, presentation, } from '../../../Screen/screen.js';
|
|
3
|
+
import { projectPickerRows, } from '../Component/project-picker/index.js';
|
|
4
|
+
export const projectPickerInputContext = defineContext('project-picker-input', 'input');
|
|
5
|
+
export class ProjectPickerScreen extends BaseScreen {
|
|
6
|
+
frameId = 'project-picker';
|
|
7
|
+
topTab() {
|
|
8
|
+
return { level: 'root', tab: 'projects' };
|
|
9
|
+
}
|
|
10
|
+
static route = {
|
|
11
|
+
id: 'project.picker',
|
|
12
|
+
path: '/projects/pick',
|
|
13
|
+
parent: 'projects',
|
|
14
|
+
fallback: { routeId: 'project.dashboard' },
|
|
15
|
+
context: (_params, state, base) => state
|
|
16
|
+
? base.set(projectPickerInputContext, state)
|
|
17
|
+
: base,
|
|
18
|
+
};
|
|
19
|
+
id = ProjectPickerScreen.route.id;
|
|
20
|
+
candidates = [];
|
|
21
|
+
repo = '';
|
|
22
|
+
selectedIndex = 0;
|
|
23
|
+
enter(context) {
|
|
24
|
+
const input = context.contexts?.has(projectPickerInputContext)
|
|
25
|
+
? context.contexts.get(projectPickerInputContext)
|
|
26
|
+
: undefined;
|
|
27
|
+
if (input) {
|
|
28
|
+
this.candidates = [...input.candidates];
|
|
29
|
+
this.repo = input.repo;
|
|
30
|
+
this.selectedIndex = 0;
|
|
31
|
+
}
|
|
32
|
+
context.requestDraw();
|
|
33
|
+
}
|
|
34
|
+
get rows() {
|
|
35
|
+
return this.candidates;
|
|
36
|
+
}
|
|
37
|
+
render() {
|
|
38
|
+
return projectPickerRows(this.candidates, this.repo, this.selectedIndex);
|
|
39
|
+
}
|
|
40
|
+
renderNonInteractive() {
|
|
41
|
+
return [
|
|
42
|
+
...this.render(),
|
|
43
|
+
'',
|
|
44
|
+
'Run gaia ui project:<name> to choose directly.',
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
present(_context) {
|
|
48
|
+
return presentation(this.render(), {
|
|
49
|
+
selectable: this.candidates.length,
|
|
50
|
+
selectedIndex: this.selectedIndex,
|
|
51
|
+
selectedRow: this.candidates.length ? 2 + this.selectedIndex : -1,
|
|
52
|
+
title: 'Which project?',
|
|
53
|
+
meta: `${this.candidates.length} project(s) claim ${this.repo} · Enter open · Esc dashboard`,
|
|
54
|
+
status: ' ↑↓ select · Enter open · Esc dashboard · q quit ',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
handleInput(input, context) {
|
|
58
|
+
if (input === 'j' || input === '\x1b[B')
|
|
59
|
+
this.selectedIndex = Math.min(this.selectedIndex + 1, this.candidates.length - 1);
|
|
60
|
+
else if (input === 'k' || input === '\x1b[A')
|
|
61
|
+
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
|
|
62
|
+
else
|
|
63
|
+
return false;
|
|
64
|
+
context.requestDraw();
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
activate(index, context) {
|
|
68
|
+
const picked = this.candidates[index];
|
|
69
|
+
if (!picked || !context.navigate)
|
|
70
|
+
return false;
|
|
71
|
+
context.navigate('project.dashboard', {
|
|
72
|
+
projectId: picked.id,
|
|
73
|
+
projectName: picked.name,
|
|
74
|
+
projectSlug: picked.name,
|
|
75
|
+
});
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
select(index, context) {
|
|
79
|
+
if (index < 0 || index >= this.candidates.length)
|
|
80
|
+
return false;
|
|
81
|
+
this.selectedIndex = index;
|
|
82
|
+
context.requestDraw();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
reset() {
|
|
86
|
+
this.candidates = [];
|
|
87
|
+
this.repo = '';
|
|
88
|
+
this.selectedIndex = 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
2
|
+
import { type ScreenContext } from '../../../Screen/screen.js';
|
|
3
|
+
import { type ProjectRow } from '../Data/projects.js';
|
|
4
|
+
export declare class ProjectsScreen extends AsyncScreen<ProjectRow[]> {
|
|
5
|
+
readonly frameId = "projects";
|
|
6
|
+
topTab(): {
|
|
7
|
+
readonly level: "root";
|
|
8
|
+
readonly tab: "projects";
|
|
9
|
+
};
|
|
10
|
+
reload(context: ScreenContext): void;
|
|
11
|
+
static readonly route: {
|
|
12
|
+
id: string;
|
|
13
|
+
path: string;
|
|
14
|
+
tab: {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
scope: string;
|
|
18
|
+
order: number;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
readonly id: string;
|
|
22
|
+
private readonly cursor;
|
|
23
|
+
/** Kept as a settable PUBLIC member: the kernel reads it and
|
|
24
|
+
* `tests/Screen/project-screens.test.ts` writes it (GAIA-294 D2). */
|
|
25
|
+
get selectedIndex(): number;
|
|
26
|
+
set selectedIndex(value: number);
|
|
27
|
+
saveState(): {
|
|
28
|
+
selectedIndex: number;
|
|
29
|
+
};
|
|
30
|
+
restoreState(token: unknown): void;
|
|
31
|
+
get rows(): readonly ProjectRow[];
|
|
32
|
+
protected load(context: ScreenContext): Promise<ProjectRow[]>;
|
|
33
|
+
protected renderLoading(): string[];
|
|
34
|
+
protected renderReady(rows: ProjectRow[]): string[];
|
|
35
|
+
present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
|
|
36
|
+
activate(index: number, context: ScreenContext): boolean;
|
|
37
|
+
select(index: number, context: ScreenContext): boolean;
|
|
38
|
+
handleInput(input: string, context: ScreenContext): boolean;
|
|
39
|
+
reset(): void;
|
|
40
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ListCursor } from '../../../lib/cursor.js';
|
|
2
|
+
import { AsyncScreen } from '../../../Screen/async-screen.js';
|
|
3
|
+
import { presentation } from '../../../Screen/screen.js';
|
|
4
|
+
import { projectRows } from '../Component/project-list/index.js';
|
|
5
|
+
import { loadProjectRows } from '../Data/projects.js';
|
|
6
|
+
export class ProjectsScreen extends AsyncScreen {
|
|
7
|
+
frameId = 'projects';
|
|
8
|
+
topTab() {
|
|
9
|
+
return { level: 'root', tab: 'projects' };
|
|
10
|
+
}
|
|
11
|
+
reload(context) {
|
|
12
|
+
this.retry(context);
|
|
13
|
+
}
|
|
14
|
+
static route = {
|
|
15
|
+
id: 'projects',
|
|
16
|
+
path: '/projects',
|
|
17
|
+
tab: { id: 'projects', label: 'Projects', scope: 'root', order: 20 },
|
|
18
|
+
};
|
|
19
|
+
id = ProjectsScreen.route.id;
|
|
20
|
+
cursor = new ListCursor();
|
|
21
|
+
/** Kept as a settable PUBLIC member: the kernel reads it and
|
|
22
|
+
* `tests/Screen/project-screens.test.ts` writes it (GAIA-294 D2). */
|
|
23
|
+
get selectedIndex() {
|
|
24
|
+
return this.cursor.index;
|
|
25
|
+
}
|
|
26
|
+
set selectedIndex(value) {
|
|
27
|
+
this.cursor.index = value;
|
|
28
|
+
}
|
|
29
|
+
saveState() {
|
|
30
|
+
return { selectedIndex: this.selectedIndex };
|
|
31
|
+
}
|
|
32
|
+
restoreState(token) {
|
|
33
|
+
if (token &&
|
|
34
|
+
typeof token === 'object' &&
|
|
35
|
+
typeof token.selectedIndex === 'number') {
|
|
36
|
+
this.selectedIndex = token.selectedIndex;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
get rows() {
|
|
40
|
+
return this.readyValue ?? [];
|
|
41
|
+
}
|
|
42
|
+
load(context) {
|
|
43
|
+
if (!context.services)
|
|
44
|
+
throw new Error('Projects requires services');
|
|
45
|
+
return loadProjectRows({
|
|
46
|
+
client: context.services.client,
|
|
47
|
+
nowSeconds: context.nowSeconds ?? (() => Math.floor(Date.now() / 1000)),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
renderLoading() {
|
|
51
|
+
return ['Loading projects…'];
|
|
52
|
+
}
|
|
53
|
+
renderReady(rows) {
|
|
54
|
+
return projectRows(rows);
|
|
55
|
+
}
|
|
56
|
+
present(context) {
|
|
57
|
+
const rows = this.rows;
|
|
58
|
+
return presentation(this.render(context), {
|
|
59
|
+
selectable: rows.length,
|
|
60
|
+
selectedIndex: this.selectedIndex,
|
|
61
|
+
selectedRow: rows.length ? this.selectedIndex : -1,
|
|
62
|
+
title: 'GAIA projects',
|
|
63
|
+
meta: `${rows.length} registered project(s) · ${context.services?.baseUrl ?? ''}`,
|
|
64
|
+
status: ' ↑↓ select · Enter open · n new · Tab tabs · q quit ',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
activate(index, context) {
|
|
68
|
+
const row = this.readyValue?.[index];
|
|
69
|
+
if (!row || !context.navigate)
|
|
70
|
+
return false;
|
|
71
|
+
context.navigate('project.dashboard', {
|
|
72
|
+
projectId: row.projectId,
|
|
73
|
+
projectName: row.projectName,
|
|
74
|
+
projectSlug: row.projectSlug,
|
|
75
|
+
});
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
select(index, context) {
|
|
79
|
+
if (index < 0 || index >= this.rows.length)
|
|
80
|
+
return false;
|
|
81
|
+
this.selectedIndex = index;
|
|
82
|
+
context.requestDraw();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
handleInput(input, context) {
|
|
86
|
+
if (super.handleInput(input, context))
|
|
87
|
+
return true;
|
|
88
|
+
const rows = this.readyValue;
|
|
89
|
+
if (!rows)
|
|
90
|
+
return false;
|
|
91
|
+
if (this.cursor.handle(input, rows.length)) {
|
|
92
|
+
context.requestDraw();
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (input === '\r' || input === '\n')
|
|
96
|
+
return this.activate(this.selectedIndex, context);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
reset() {
|
|
100
|
+
this.cursor.reset();
|
|
101
|
+
super.reset();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import type
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* liveness status and STARTED is the relative start age.
|
|
7
|
-
*/
|
|
1
|
+
import { type RunRow } from '../../Data/runs.js';
|
|
2
|
+
/** A fixed-column flat table of a project's runs: header + one line per run.
|
|
3
|
+
* Columns RUN · TICKET · STATE · PHASE · AGENT · STARTED, declared beside the row
|
|
4
|
+
* mapper in `Data/runs.ts` (GAIA-294). STATE is the derived liveness status and
|
|
5
|
+
* STARTED the relative start age, which is what `nowSeconds` is for. */
|
|
8
6
|
export declare function runTableRows(runs: RunRow[], nowSeconds: number, { emptyMessage }?: {
|
|
9
7
|
emptyMessage?: string | undefined;
|
|
10
8
|
}): string[];
|
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* liveness status and STARTED is the relative start age.
|
|
8
|
-
*/
|
|
1
|
+
import { renderTable } from '../../../../lib/table.js';
|
|
2
|
+
import { RUN_COLUMNS } from '../../Data/runs.js';
|
|
3
|
+
/** A fixed-column flat table of a project's runs: header + one line per run.
|
|
4
|
+
* Columns RUN · TICKET · STATE · PHASE · AGENT · STARTED, declared beside the row
|
|
5
|
+
* mapper in `Data/runs.ts` (GAIA-294). STATE is the derived liveness status and
|
|
6
|
+
* STARTED the relative start age, which is what `nowSeconds` is for. */
|
|
9
7
|
export function runTableRows(runs, nowSeconds, { emptyMessage } = {}) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return [
|
|
14
|
-
header,
|
|
15
|
-
...runs.map((r) => {
|
|
16
|
-
const run = `#${r.drupalId ?? r.id.slice(0, 6)}`;
|
|
17
|
-
return `${pad(run, 8)} ${pad(r.ticketIdentifier, 10)} ${pad(deriveRunStatus(r, nowSeconds), 8)} ${pad(r.phase, 10)} ${pad(r.agent, 8)} ${runStartedAge(r, nowSeconds)}`;
|
|
18
|
-
}),
|
|
19
|
-
];
|
|
8
|
+
return renderTable(RUN_COLUMNS, runs, nowSeconds, {
|
|
9
|
+
emptyMessage: emptyMessage ?? 'No runs for this project.',
|
|
10
|
+
});
|
|
20
11
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
2
|
+
/** The active filter criteria for the project run list. */
|
|
3
|
+
export interface RunCriteria {
|
|
4
|
+
/** false: open only (the default) · true: closed only · null: all. */
|
|
5
|
+
closed: boolean | null;
|
|
6
|
+
}
|
|
7
|
+
/** The shipped default: closed runs hidden (GAIA-282 D1). */
|
|
8
|
+
export declare const DEFAULT_RUN_CRITERIA: RunCriteria;
|
|
9
|
+
/** "Active" means the view is narrowed relative to showing every row —
|
|
10
|
+
* true at the shipped default, by the same decision as the ticket list. */
|
|
11
|
+
export declare function hasActiveCriteria(criteria: RunCriteria): boolean;
|
|
12
|
+
/** Human-readable active-criteria summary, or '' when none. */
|
|
13
|
+
export declare function describeCriteria(criteria: RunCriteria): string;
|
|
14
|
+
/**
|
|
15
|
+
* Build the JSON:API query for a project's runs under the given criteria.
|
|
16
|
+
* Scope is the ticket→project relationship (a project may span conductors).
|
|
17
|
+
* '1'/'0', never the boolean — spec fact 7's measured backend behavior.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildRunQueryParams(projectId: string, criteria: RunCriteria, page?: {
|
|
20
|
+
size: number;
|
|
21
|
+
index: number;
|
|
22
|
+
}): DrupalJsonApiParams;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The run list's criteria — the same shape the ticket list established
|
|
2
|
+
// (GAIA-282 D4, AC-6): criteria type, hasActive, describe, param builder.
|
|
3
|
+
// Runs offer only the closed cycle; search/state are not part of GAIA-282.
|
|
4
|
+
import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
|
|
5
|
+
import { LIST_PAGE_SIZE } from '../../cycle.js';
|
|
6
|
+
import { RUN_LIST_FIELDS, RUN_TICKET_INCLUDE_FIELDS } from './runs.js';
|
|
7
|
+
/** The shipped default: closed runs hidden (GAIA-282 D1). */
|
|
8
|
+
export const DEFAULT_RUN_CRITERIA = { closed: false };
|
|
9
|
+
/** "Active" means the view is narrowed relative to showing every row —
|
|
10
|
+
* true at the shipped default, by the same decision as the ticket list. */
|
|
11
|
+
export function hasActiveCriteria(criteria) {
|
|
12
|
+
return criteria.closed !== null;
|
|
13
|
+
}
|
|
14
|
+
/** Human-readable active-criteria summary, or '' when none. */
|
|
15
|
+
export function describeCriteria(criteria) {
|
|
16
|
+
if (criteria.closed === null)
|
|
17
|
+
return '';
|
|
18
|
+
return criteria.closed ? 'closed:only' : 'closed:hidden';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the JSON:API query for a project's runs under the given criteria.
|
|
22
|
+
* Scope is the ticket→project relationship (a project may span conductors).
|
|
23
|
+
* '1'/'0', never the boolean — spec fact 7's measured backend behavior.
|
|
24
|
+
*/
|
|
25
|
+
export function buildRunQueryParams(projectId, criteria, page = { size: LIST_PAGE_SIZE, index: 0 }) {
|
|
26
|
+
const params = new DrupalJsonApiParams();
|
|
27
|
+
params.addFilter('ticket_id.project_id.id', projectId);
|
|
28
|
+
if (criteria.closed !== null)
|
|
29
|
+
params.addFilter('closed', criteria.closed ? '1' : '0');
|
|
30
|
+
params.addInclude(['ticket_id']);
|
|
31
|
+
// Unlike the ticket list, this fieldset governs the INCLUDES only — the
|
|
32
|
+
// primary type here is `gaia_run` (GAIA-292 D3).
|
|
33
|
+
params.addFields('gaia_ticket--gaia_ticket', RUN_TICKET_INCLUDE_FIELDS);
|
|
34
|
+
params.addFields('gaia_run--gaia_run', RUN_LIST_FIELDS);
|
|
35
|
+
params.addSort('started_at', 'DESC');
|
|
36
|
+
params.addPageLimit(page.size);
|
|
37
|
+
// Page one carries no offset, so its query is the shortest it can be.
|
|
38
|
+
if (page.index > 0)
|
|
39
|
+
params.addPageOffset(page.index * page.size);
|
|
40
|
+
return params;
|
|
41
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { DropshClient, JsonApiResource } from '../../../types.js';
|
|
2
|
+
import { type Page } from '../../collection.js';
|
|
3
|
+
import { type RunCriteria } from './query.js';
|
|
2
4
|
/** One flat-table row derived from a `gaia_run` dropsh resource. */
|
|
3
5
|
export interface RunRow {
|
|
4
6
|
id: string;
|
|
@@ -18,19 +20,98 @@ export interface RunRow {
|
|
|
18
20
|
startedAt: string | null;
|
|
19
21
|
heartbeat: string | null;
|
|
20
22
|
claimExpiresAt: string | null;
|
|
23
|
+
/** Boolean closed flag; an absent attribute reads as open (spec D5). */
|
|
24
|
+
closed: boolean;
|
|
21
25
|
}
|
|
26
|
+
/** In the fieldset, or the sideloaded ticket stops resolving (spec fact 5). */
|
|
27
|
+
export declare const RUN_LIST_RELATIONSHIPS: readonly ["ticket_id"];
|
|
28
|
+
/**
|
|
29
|
+
* The run list's columns — RUN · TICKET · STATE · PHASE · AGENT · STARTED, each
|
|
30
|
+
* carrying its own fieldset entry (GAIA-294 AC 4).
|
|
31
|
+
*
|
|
32
|
+
* `Ctx` is `nowSeconds`: STATE is the derived liveness status and STARTED the
|
|
33
|
+
* relative start age, and both need a clock the row does not carry.
|
|
34
|
+
*/
|
|
35
|
+
export declare const RUN_COLUMNS: readonly [{
|
|
36
|
+
readonly key: "run";
|
|
37
|
+
readonly label: "RUN";
|
|
38
|
+
readonly width: 8;
|
|
39
|
+
readonly fields: readonly ["drupal_internal__id"];
|
|
40
|
+
readonly get: (r: RunRow) => string;
|
|
41
|
+
}, {
|
|
42
|
+
readonly key: "ticket";
|
|
43
|
+
readonly label: "TICKET";
|
|
44
|
+
readonly width: 10;
|
|
45
|
+
readonly fields: readonly ["ticket_id"];
|
|
46
|
+
readonly get: (r: RunRow) => string;
|
|
47
|
+
}, {
|
|
48
|
+
readonly key: "state";
|
|
49
|
+
readonly label: "STATE";
|
|
50
|
+
readonly width: 8;
|
|
51
|
+
readonly fields: readonly ["state", "heartbeat", "claim_expires_at"];
|
|
52
|
+
readonly get: (r: RunRow, nowSeconds: number) => string;
|
|
53
|
+
}, {
|
|
54
|
+
readonly key: "phase";
|
|
55
|
+
readonly label: "PHASE";
|
|
56
|
+
readonly width: 10;
|
|
57
|
+
readonly fields: readonly ["state_at_start"];
|
|
58
|
+
readonly get: (r: RunRow) => string;
|
|
59
|
+
}, {
|
|
60
|
+
readonly key: "agent";
|
|
61
|
+
readonly label: "AGENT";
|
|
62
|
+
readonly width: 8;
|
|
63
|
+
readonly fields: readonly ["agent"];
|
|
64
|
+
readonly get: (r: RunRow) => string;
|
|
65
|
+
}, {
|
|
66
|
+
readonly key: "started";
|
|
67
|
+
readonly label: "STARTED";
|
|
68
|
+
readonly width: 0;
|
|
69
|
+
readonly fields: readonly ["started_at", "created"];
|
|
70
|
+
readonly get: (r: RunRow, nowSeconds: number) => string;
|
|
71
|
+
}];
|
|
72
|
+
/**
|
|
73
|
+
* The attributes a run row needs (GAIA-292 D2), in the order the query string
|
|
74
|
+
* states them. `created` is a column field as `started_at`'s documented fallback
|
|
75
|
+
* for a run that never started, and `heartbeat` + `claim_expires_at` ride the STATE
|
|
76
|
+
* column because `deriveRunStatus` reads them.
|
|
77
|
+
*
|
|
78
|
+
* Membership is owned by the columns and proved exhaustive below; only the ORDER
|
|
79
|
+
* is declared, because `tests/Entity/run/Data/query.test.ts` pins the emitted
|
|
80
|
+
* `fields[gaia_run--gaia_run]=…` sequence and column order would hoist
|
|
81
|
+
* `heartbeat`/`claim_expires_at` to third and fourth (GAIA-294, behaviour-neutral
|
|
82
|
+
* to the bytes on the wire).
|
|
83
|
+
*/
|
|
84
|
+
export declare const RUN_LIST_ATTRIBUTES: readonly ["drupal_internal__id", "state", "state_at_start", "agent", "started_at", "created", "heartbeat", "claim_expires_at", "closed"];
|
|
85
|
+
/** What `addFields('gaia_run--gaia_run', …)` takes. */
|
|
86
|
+
export declare const RUN_LIST_FIELDS: string[];
|
|
87
|
+
/**
|
|
88
|
+
* The sideloaded ticket renders ONE column: its identifier.
|
|
89
|
+
*
|
|
90
|
+
* Measured (GAIA-292 spec fact 4): without this, a 37-row run page shipped
|
|
91
|
+
* 191.4 KB of `included` — 12 whole ticket resources with their descriptions —
|
|
92
|
+
* out of 254.7 KB total, and took 4732 ms.
|
|
93
|
+
*/
|
|
94
|
+
export declare const RUN_TICKET_INCLUDE_FIELDS: string[];
|
|
22
95
|
/**
|
|
23
96
|
* Map a RAW JSON:API `data[]` entry (from `client.get`) to a run row, resolving
|
|
24
97
|
* the ticket identifier from the sideloaded includes indexed by id. Mirrors
|
|
25
98
|
* `mapTicketData` — the only I/O is upstream in {@link loadProjectRuns}.
|
|
99
|
+
*
|
|
100
|
+
* Every attribute and relationship is read through an accessor keyed on the
|
|
101
|
+
* fieldset, so reading one the list does not request is a compile error (AC-5).
|
|
26
102
|
*/
|
|
27
103
|
export declare function mapRunData(item: JsonApiResource, ticketsById: Map<string, JsonApiResource>): RunRow;
|
|
104
|
+
/** One page of run rows plus whether another exists — the shared {@link Page},
|
|
105
|
+
* whose four members and their meaning are documented there (GAIA-294). */
|
|
106
|
+
export type RunPage = Page<RunRow>;
|
|
28
107
|
/**
|
|
29
|
-
* List the `gaia_run` entities of a project
|
|
30
|
-
*
|
|
31
|
-
*
|
|
108
|
+
* List the `gaia_run` entities of a project under the given criteria,
|
|
109
|
+
* most-recently-started first — ONE page of them. The query (scope, include,
|
|
110
|
+
* fieldsets, sort, closed filter) is built by {@link buildRunQueryParams}; the
|
|
111
|
+
* page (size + index) is the caller's, and `hasNext` comes from the document's
|
|
112
|
+
* `links.next`.
|
|
32
113
|
*/
|
|
33
|
-
export declare function loadProjectRuns(client: DropshClient, projectId: string): Promise<
|
|
114
|
+
export declare function loadProjectRuns(client: DropshClient, projectId: string, criteria: RunCriteria, page?: number): Promise<RunPage>;
|
|
34
115
|
/**
|
|
35
116
|
* Newest-STARTED first, and TOTAL (GAIA-255 D5).
|
|
36
117
|
*
|