@gaia-ai/addon-gaia-ui 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/src/Component/agent-list/index.d.ts +6 -0
  4. package/dist/src/Component/agent-list/index.js +14 -0
  5. package/dist/src/Component/comment-item/index.d.ts +21 -0
  6. package/dist/src/Component/comment-item/index.js +39 -0
  7. package/dist/src/Component/create-form/index.d.ts +3 -0
  8. package/dist/src/Component/create-form/index.js +26 -0
  9. package/dist/src/Component/dashboard-list/index.d.ts +8 -0
  10. package/dist/src/Component/dashboard-list/index.js +7 -0
  11. package/dist/src/Component/run-table/index.d.ts +10 -0
  12. package/dist/src/Component/run-table/index.js +20 -0
  13. package/dist/src/Component/section-list/index.d.ts +9 -0
  14. package/dist/src/Component/section-list/index.js +27 -0
  15. package/dist/src/Component/tab-bar/index.d.ts +9 -0
  16. package/dist/src/Component/tab-bar/index.js +22 -0
  17. package/dist/src/Component/ticket-detail/index.d.ts +30 -0
  18. package/dist/src/Component/ticket-detail/index.js +129 -0
  19. package/dist/src/Component/ticket-table/index.d.ts +5 -0
  20. package/dist/src/Component/ticket-table/index.js +14 -0
  21. package/dist/src/Component/top-tabs/index.d.ts +8 -0
  22. package/dist/src/Component/top-tabs/index.js +13 -0
  23. package/dist/src/Deriver/liveness.d.ts +12 -0
  24. package/dist/src/Deriver/liveness.js +34 -0
  25. package/dist/src/Deriver/run-status.d.ts +12 -0
  26. package/dist/src/Deriver/run-status.js +36 -0
  27. package/dist/src/Kernel/tui-kernel.d.ts +2 -0
  28. package/dist/src/Kernel/tui-kernel.js +983 -0
  29. package/dist/src/Screen/dashboard.d.ts +1 -0
  30. package/dist/src/Screen/dashboard.js +1 -0
  31. package/dist/src/Screen/project.d.ts +1 -0
  32. package/dist/src/Screen/project.js +1 -0
  33. package/dist/src/Screen/ticket.d.ts +1 -0
  34. package/dist/src/Screen/ticket.js +1 -0
  35. package/dist/src/Service/agent.d.ts +17 -0
  36. package/dist/src/Service/agent.js +28 -0
  37. package/dist/src/Service/create-form-data.d.ts +10 -0
  38. package/dist/src/Service/create-form-data.js +52 -0
  39. package/dist/src/Service/create-form.d.ts +52 -0
  40. package/dist/src/Service/create-form.js +98 -0
  41. package/dist/src/Service/project-data.d.ts +48 -0
  42. package/dist/src/Service/project-data.js +92 -0
  43. package/dist/src/Service/projects.d.ts +21 -0
  44. package/dist/src/Service/projects.js +80 -0
  45. package/dist/src/Service/run-data.d.ts +35 -0
  46. package/dist/src/Service/run-data.js +62 -0
  47. package/dist/src/Service/ticket-data.d.ts +6 -0
  48. package/dist/src/Service/ticket-data.js +7 -0
  49. package/dist/src/index.d.ts +6 -0
  50. package/dist/src/index.js +10 -0
  51. package/dist/src/launcher.d.ts +58 -0
  52. package/dist/src/launcher.js +51 -0
  53. package/dist/src/lib/ansi.d.ts +19 -0
  54. package/dist/src/lib/ansi.js +45 -0
  55. package/dist/src/lib/format.d.ts +3 -0
  56. package/dist/src/lib/format.js +10 -0
  57. package/dist/src/lib/scroll.d.ts +1 -0
  58. package/dist/src/lib/scroll.js +16 -0
  59. package/dist/src/lib/ticket-query.d.ts +21 -0
  60. package/dist/src/lib/ticket-query.js +72 -0
  61. package/dist/src/plugin.d.ts +13 -0
  62. package/dist/src/plugin.js +17 -0
  63. package/dist/src/preset.d.ts +2 -0
  64. package/dist/src/preset.js +7 -0
  65. package/dist/src/types.d.ts +98 -0
  66. package/dist/src/types.js +5 -0
  67. package/package.json +28 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import type { TuiAgentService } from '../types.js';
2
+ /** The launch intent passed to {@link TuiAgentService.launch}. */
3
+ export type CreateLaunch = Parameters<TuiAgentService['launch']>[0];
4
+ /**
5
+ * Build the `ticket:create` launch intent for a project. The prompt is a pure
6
+ * pointer to WORKFLOW.md (the pre-filled form is passed via
7
+ * `$GAIA_NEW_TICKET_INPUT`); `GAIA_ID` is never inherited (a create must not be
8
+ * bound to a claimed ticket).
9
+ */
10
+ export declare function buildCreateLaunch(opts: {
11
+ project: string;
12
+ cwd: string;
13
+ specPath?: string | undefined;
14
+ workflow?: string | undefined;
15
+ createPrompt?: string | undefined;
16
+ label?: string | undefined;
17
+ }): CreateLaunch;
@@ -0,0 +1,28 @@
1
+ const CREATE_PROMPT = 'Read the repository\'s WORKFLOW.md and follow its "Create ticket" section to ' +
2
+ 'create a new GAIA ticket. Do not start work on the new ticket.';
3
+ /**
4
+ * Build the `ticket:create` launch intent for a project. The prompt is a pure
5
+ * pointer to WORKFLOW.md (the pre-filled form is passed via
6
+ * `$GAIA_NEW_TICKET_INPUT`); `GAIA_ID` is never inherited (a create must not be
7
+ * bound to a claimed ticket).
8
+ */
9
+ export function buildCreateLaunch(opts) {
10
+ const env = {
11
+ ...process.env,
12
+ TERM: 'xterm-256color',
13
+ GAIA_PROJECT: opts.project,
14
+ };
15
+ delete env.GAIA_ID;
16
+ if (opts.specPath)
17
+ env.GAIA_NEW_TICKET_INPUT = opts.specPath;
18
+ if (opts.workflow)
19
+ env.GAIA_WORKFLOW = opts.workflow;
20
+ return {
21
+ group: opts.project,
22
+ cwd: opts.cwd,
23
+ prompt: opts.createPrompt?.trim() || CREATE_PROMPT,
24
+ env,
25
+ isNew: true,
26
+ ...(opts.label !== undefined ? { label: opts.label } : {}),
27
+ };
28
+ }
@@ -0,0 +1,10 @@
1
+ import type { DropshClient } from '../types.js';
2
+ import type { CreateFormOptions, Option } from './create-form.js';
3
+ /** The fixed GAIA workflow classification (triage → workflow + destination). */
4
+ export declare const WORKFLOW_OPTIONS: Option[];
5
+ /** Hints from the launcher used to preselect the machine's own user + conductor. */
6
+ export interface CreateFormHints {
7
+ currentUser?: string | undefined;
8
+ ownConductorMachineId?: string | undefined;
9
+ }
10
+ export declare function loadCreateFormOptions(client: DropshClient, hints?: CreateFormHints): Promise<CreateFormOptions>;
@@ -0,0 +1,52 @@
1
+ /** The fixed GAIA workflow classification (triage → workflow + destination). */
2
+ export const WORKFLOW_OPTIONS = [
3
+ { id: 'gaia_feature', label: 'feature' },
4
+ { id: 'gaia_bug', label: 'bug' },
5
+ { id: 'gaia_chore', label: 'chore' },
6
+ ];
7
+ const str = (v) => typeof v === 'string' && v.length > 0 ? v : undefined;
8
+ async function list(client, type, map) {
9
+ try {
10
+ return (await client.collection(type).list()).map(map);
11
+ }
12
+ catch {
13
+ return [];
14
+ }
15
+ }
16
+ /** Stable-sort the items matching `isOwn` to the front (so index 0 = default). */
17
+ function ownFirst(items, isOwn) {
18
+ const own = items.filter(isOwn);
19
+ const rest = items.filter((i) => !isOwn(i));
20
+ return [...own, ...rest];
21
+ }
22
+ export async function loadCreateFormOptions(client, hints = {}) {
23
+ const [users, conductorRows, labels] = await Promise.all([
24
+ list(client, 'user', (r) => ({
25
+ id: r.id,
26
+ label: str(r.attr('display_name')) ?? str(r.attr('name')) ?? r.id,
27
+ name: str(r.attr('name')),
28
+ })),
29
+ list(client, 'gaia_conductor', (r) => ({
30
+ id: r.id,
31
+ label: str(r.attr('label')) ?? str(r.attr('machine_id')) ?? r.id,
32
+ machineId: str(r.attr('machine_id')),
33
+ })),
34
+ list(client, 'gaia_term', (r) => ({
35
+ id: r.id,
36
+ label: str(r.attr('name')) ?? str(r.attr('label')) ?? r.id,
37
+ })),
38
+ ]);
39
+ // Preselect the machine's own user (by username) + conductor (by machine_id).
40
+ const orderedUsers = hints.currentUser
41
+ ? ownFirst(users, (u) => u.name === hints.currentUser || u.id === hints.currentUser)
42
+ : users;
43
+ const orderedConductors = hints.ownConductorMachineId
44
+ ? ownFirst(conductorRows, (c) => c.machineId === hints.ownConductorMachineId)
45
+ : conductorRows;
46
+ return {
47
+ users: orderedUsers.map((u) => ({ id: u.id, label: u.label })),
48
+ conductors: orderedConductors.map((c) => ({ id: c.id, label: c.label })),
49
+ labels,
50
+ workflows: WORKFLOW_OPTIONS,
51
+ };
52
+ }
@@ -0,0 +1,52 @@
1
+ export interface Option {
2
+ id: string;
3
+ label: string;
4
+ }
5
+ export interface CreateFormOptions {
6
+ users: Option[];
7
+ conductors: Option[];
8
+ labels: Option[];
9
+ workflows: Option[];
10
+ }
11
+ export type FieldKind = 'text' | 'select' | 'multiselect' | 'action';
12
+ export interface FieldDef {
13
+ id: 'description' | 'assignee' | 'conductor' | 'labels' | 'workflow' | 'submit';
14
+ label: string;
15
+ kind: FieldKind;
16
+ }
17
+ /** Field order in the popup (top to bottom). */
18
+ export declare const FIELDS: FieldDef[];
19
+ export interface CreateFormState {
20
+ description: string;
21
+ assignee: string | null;
22
+ conductor: string | null;
23
+ workflow: string | null;
24
+ labels: Set<string>;
25
+ /** Active field index into {@link FIELDS}. */
26
+ active: number;
27
+ /** Highlighted label index (when the labels field is active). */
28
+ labelCursor: number;
29
+ }
30
+ export declare function initialForm(opts: CreateFormOptions): CreateFormState;
31
+ export declare const activeField: (s: CreateFormState) => FieldDef;
32
+ export declare function moveField(s: CreateFormState, delta: number): CreateFormState;
33
+ /** Append typed text to the description (only meaningful on the text field). */
34
+ export declare function typeChar(s: CreateFormState, text: string): CreateFormState;
35
+ export declare function backspace(s: CreateFormState): CreateFormState;
36
+ export declare function newline(s: CreateFormState): CreateFormState;
37
+ /** Cycle the active single-select field's value by `dir` (+1 / -1). */
38
+ export declare function cycle(s: CreateFormState, opts: CreateFormOptions, dir: 1 | -1): CreateFormState;
39
+ export declare function moveLabelCursor(s: CreateFormState, opts: CreateFormOptions, dir: 1 | -1): CreateFormState;
40
+ export declare function toggleLabel(s: CreateFormState, opts: CreateFormOptions): CreateFormState;
41
+ /** The submitted payload (resolved options), handed to the create agent. */
42
+ export interface CreateFormPayload {
43
+ description: string;
44
+ workflow: Option | null;
45
+ assignee: Option | null;
46
+ conductor: Option | null;
47
+ labels: Option[];
48
+ }
49
+ /** A short human label for the agent list: the description's first line (capped),
50
+ * else "New <workflow>". */
51
+ export declare function shortName(payload: CreateFormPayload): string;
52
+ export declare function serialize(s: CreateFormState, opts: CreateFormOptions): CreateFormPayload;
@@ -0,0 +1,98 @@
1
+ // Pure state model for the "New ticket" popup form (GAIA-190). The kernel drives
2
+ // it from raw key input; `Component/create-form` renders it. On submit the
3
+ // serialized payload is handed to the create agent (via a temp spec file), which
4
+ // creates the ticket through the canonical path. No I/O here.
5
+ /** Field order in the popup (top to bottom). */
6
+ export const FIELDS = [
7
+ { id: 'description', label: 'Description', kind: 'text' },
8
+ { id: 'assignee', label: 'Assignee', kind: 'select' },
9
+ { id: 'conductor', label: 'Conductor', kind: 'select' },
10
+ { id: 'labels', label: 'Labels', kind: 'multiselect' },
11
+ { id: 'workflow', label: 'Workflow', kind: 'select' },
12
+ { id: 'submit', label: 'Create ticket', kind: 'action' },
13
+ ];
14
+ const clamp = (v, min, max) => Math.max(min, Math.min(v, max));
15
+ export function initialForm(opts) {
16
+ return {
17
+ description: '',
18
+ assignee: opts.users[0]?.id ?? null,
19
+ conductor: opts.conductors[0]?.id ?? null,
20
+ workflow: opts.workflows[0]?.id ?? null,
21
+ labels: new Set(),
22
+ active: 0,
23
+ labelCursor: 0,
24
+ };
25
+ }
26
+ export const activeField = (s) => (FIELDS[s.active] ?? FIELDS[0]);
27
+ export function moveField(s, delta) {
28
+ return { ...s, active: clamp(s.active + delta, 0, FIELDS.length - 1) };
29
+ }
30
+ /** Append typed text to the description (only meaningful on the text field). */
31
+ export function typeChar(s, text) {
32
+ return { ...s, description: s.description + text };
33
+ }
34
+ export function backspace(s) {
35
+ return { ...s, description: s.description.slice(0, -1) };
36
+ }
37
+ export function newline(s) {
38
+ return { ...s, description: `${s.description}\n` };
39
+ }
40
+ const cycleValue = (list, current, dir) => {
41
+ if (list.length === 0)
42
+ return current;
43
+ const idx = list.findIndex((o) => o.id === current);
44
+ const next = (idx < 0 ? 0 : idx + dir + list.length) % list.length;
45
+ return list[next]?.id ?? current;
46
+ };
47
+ /** Cycle the active single-select field's value by `dir` (+1 / -1). */
48
+ export function cycle(s, opts, dir) {
49
+ switch (activeField(s).id) {
50
+ case 'assignee':
51
+ return { ...s, assignee: cycleValue(opts.users, s.assignee, dir) };
52
+ case 'conductor':
53
+ return { ...s, conductor: cycleValue(opts.conductors, s.conductor, dir) };
54
+ case 'workflow':
55
+ return { ...s, workflow: cycleValue(opts.workflows, s.workflow, dir) };
56
+ default:
57
+ return s;
58
+ }
59
+ }
60
+ export function moveLabelCursor(s, opts, dir) {
61
+ return {
62
+ ...s,
63
+ labelCursor: clamp(s.labelCursor + dir, 0, Math.max(0, opts.labels.length - 1)),
64
+ };
65
+ }
66
+ export function toggleLabel(s, opts) {
67
+ const option = opts.labels[s.labelCursor];
68
+ if (!option)
69
+ return s;
70
+ const labels = new Set(s.labels);
71
+ if (labels.has(option.id))
72
+ labels.delete(option.id);
73
+ else
74
+ labels.add(option.id);
75
+ return { ...s, labels };
76
+ }
77
+ const optionFor = (list, id) => list.find((o) => o.id === id) ?? null;
78
+ /** A short human label for the agent list: the description's first line (capped),
79
+ * else "New <workflow>". */
80
+ export function shortName(payload) {
81
+ const firstLine = payload.description
82
+ .split('\n')
83
+ .map((l) => l.trim())
84
+ .find((l) => l.length > 0);
85
+ if (firstLine) {
86
+ return firstLine.length > 40 ? `${firstLine.slice(0, 39)}…` : firstLine;
87
+ }
88
+ return `New ${payload.workflow?.label ?? 'ticket'}`;
89
+ }
90
+ export function serialize(s, opts) {
91
+ return {
92
+ description: s.description,
93
+ workflow: optionFor(opts.workflows, s.workflow),
94
+ assignee: optionFor(opts.users, s.assignee),
95
+ conductor: optionFor(opts.conductors, s.conductor),
96
+ labels: opts.labels.filter((o) => s.labels.has(o.id)),
97
+ };
98
+ }
@@ -0,0 +1,48 @@
1
+ import { type TicketCriteria } from '../lib/ticket-query.js';
2
+ import type { DropshClient, JsonApiResource } from '../types.js';
3
+ /** One flat-table row derived from a `gaia_ticket` dropsh resource. */
4
+ export interface TicketRow {
5
+ id: string;
6
+ identifier: string;
7
+ title: string;
8
+ state: string;
9
+ priority: unknown;
10
+ assigneeInitials: string;
11
+ derivedStatus: string;
12
+ labels?: string[];
13
+ type: string | null;
14
+ /** Parent ticket identifier (`GAIA-nnn`) from `parent_id`, else null (AC-1/2). */
15
+ parent: string | null;
16
+ /** Assigned conductor label from `conductor_id`, else null (AC-3/4). */
17
+ conductor: string | null;
18
+ }
19
+ /** A `(id) → resource` lookup over a document's sideloaded `included` entries.
20
+ * JSON:API ids are UUIDs (unique across bundles), so id alone keys uniquely. */
21
+ type IncludedIndex = Map<string, JsonApiResource>;
22
+ /** Map a RAW JSON:API `data[]` entry (from client.get) to a table row.
23
+ * `included` resolves the parent identifier + conductor label from the
24
+ * sideloaded relationships (AC-5); absent/unresolved → null (AC-2/4). */
25
+ export declare function mapTicketData(item: JsonApiResource, included?: IncludedIndex): TicketRow;
26
+ /** Server-side filtered/searched ticket list for a project. The query sideloads
27
+ * `parent_id` + `conductor_id`; each row's parent/conductor is resolved from
28
+ * the document's `included` (AC-5). */
29
+ export declare function loadFilteredTickets(client: DropshClient, projectId: string, criteria: TicketCriteria): Promise<TicketRow[]>;
30
+ /** The resolved project header + its ticket rows. */
31
+ export interface ProjectWithTickets {
32
+ project: {
33
+ id: string;
34
+ name: string;
35
+ slug: string;
36
+ };
37
+ tickets: TicketRow[];
38
+ }
39
+ /**
40
+ * Resolve a `gaia_project` by its slug (the registry `project`, which matches
41
+ * the project's `name`) and list its tickets, most-recently-changed first.
42
+ *
43
+ * The ticket listing delegates to {@link loadFilteredTickets} with empty
44
+ * criteria — one include-aware `client.get` path shared with the search/filter
45
+ * load, so both resolve parent/conductor identically (no dual-mapper drift).
46
+ */
47
+ export declare function loadProjectWithTickets(client: DropshClient, projectSlug: string): Promise<ProjectWithTickets>;
48
+ export {};
@@ -0,0 +1,92 @@
1
+ // Project / ticket list reads — through the dropsh JSON:API client
2
+ // (`services.client`). Every rendered object is a Drupal entity (AC-5).
3
+ import { asString } from '../lib/format.js';
4
+ import { buildTicketQueryParams, } from '../lib/ticket-query.js';
5
+ /** Build the {@link IncludedIndex} from a document's `included` array. */
6
+ function indexIncluded(included) {
7
+ const index = new Map();
8
+ for (const r of included ?? [])
9
+ if (r.id)
10
+ index.set(r.id, r);
11
+ return index;
12
+ }
13
+ /** Read the id of a single-valued relationship linkage (`data` object), or
14
+ * null when the linkage is absent, null, or an (unexpected) array. */
15
+ function singleRelId(rel) {
16
+ const data = rel?.data;
17
+ if (!data || Array.isArray(data))
18
+ return null;
19
+ return data.id ?? null;
20
+ }
21
+ /** Map a RAW JSON:API `data[]` entry (from client.get) to a table row.
22
+ * `included` resolves the parent identifier + conductor label from the
23
+ * sideloaded relationships (AC-5); absent/unresolved → null (AC-2/4). */
24
+ export function mapTicketData(item, included) {
25
+ const a = item.attributes ?? {};
26
+ const str = (key) => typeof a[key] === 'string' ? a[key] : undefined;
27
+ const labelData = item.relationships?.labels?.data;
28
+ const labels = Array.isArray(labelData)
29
+ ? labelData.map((l) => l.id).filter(Boolean)
30
+ : labelData
31
+ ? [labelData.id]
32
+ : [];
33
+ const index = included ?? new Map();
34
+ const parentId = singleRelId(item.relationships?.parent_id);
35
+ const parent = parentId
36
+ ? (asString(index.get(parentId)?.attributes?.identifier) ?? null)
37
+ : null;
38
+ const conductorId = singleRelId(item.relationships?.conductor_id);
39
+ const conductor = conductorId
40
+ ? (asString(index.get(conductorId)?.attributes?.label) ?? null)
41
+ : null;
42
+ return {
43
+ id: item.id ?? '',
44
+ identifier: str('identifier') ?? item.id ?? '',
45
+ title: str('title') ?? 'Untitled',
46
+ state: str('state') ?? 'unknown',
47
+ priority: a.priority ?? null,
48
+ assigneeInitials: str('assignee_initials') ?? '',
49
+ derivedStatus: str('derived_status') ?? 'idle',
50
+ labels,
51
+ type: str('workflow') ?? null,
52
+ parent,
53
+ conductor,
54
+ };
55
+ }
56
+ /** Server-side filtered/searched ticket list for a project. The query sideloads
57
+ * `parent_id` + `conductor_id`; each row's parent/conductor is resolved from
58
+ * the document's `included` (AC-5). */
59
+ export async function loadFilteredTickets(client, projectId, criteria) {
60
+ const params = buildTicketQueryParams(projectId, criteria);
61
+ const doc = (await client.get('gaia_ticket/gaia_ticket', params));
62
+ const index = indexIncluded(doc?.included);
63
+ return (doc?.data ?? []).map((item) => mapTicketData(item, index));
64
+ }
65
+ /**
66
+ * Resolve a `gaia_project` by its slug (the registry `project`, which matches
67
+ * the project's `name`) and list its tickets, most-recently-changed first.
68
+ *
69
+ * The ticket listing delegates to {@link loadFilteredTickets} with empty
70
+ * criteria — one include-aware `client.get` path shared with the search/filter
71
+ * load, so both resolve parent/conductor identically (no dual-mapper drift).
72
+ */
73
+ export async function loadProjectWithTickets(client, projectSlug) {
74
+ const projectRes = await client
75
+ .collection('gaia_project')
76
+ .where('name', '=', projectSlug)
77
+ .first();
78
+ const projectId = projectRes?.id ?? projectSlug;
79
+ const tickets = await loadFilteredTickets(client, projectId, {
80
+ query: '',
81
+ state: null,
82
+ type: null,
83
+ });
84
+ return {
85
+ project: {
86
+ id: projectId,
87
+ name: projectRes?.attr('name') ?? projectSlug,
88
+ slug: projectSlug,
89
+ },
90
+ tickets,
91
+ };
92
+ }
@@ -0,0 +1,21 @@
1
+ import { type ConductorStatus } from '../Deriver/liveness.js';
2
+ import type { DropshClient } from '../types.js';
3
+ /** A derived dashboard row: one distinct project + its liveliest conductor. */
4
+ export interface DashboardProject {
5
+ project: string;
6
+ derivedStatus: ConductorStatus;
7
+ lastSeen: number | null;
8
+ ageLabel: string;
9
+ }
10
+ /**
11
+ * Resolve the dashboard's project rows straight from the control plane: list the
12
+ * `gaia_conductor` entities (read is owner-gated, so this is already scoped to
13
+ * the current user's conductors — no local registry needed), sideload their
14
+ * `project_id`, group by project name, and report each project's liveliest
15
+ * conductor (running > stale > offline). Everything is a Drupal entity read
16
+ * through `services.client` (AC-5); the dashboard itself stays a screen/route.
17
+ */
18
+ export declare function loadDashboardProjects({ client, nowSeconds, }: {
19
+ client: DropshClient;
20
+ nowSeconds: () => number;
21
+ }): Promise<DashboardProject[]>;
@@ -0,0 +1,80 @@
1
+ import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
2
+ import { deriveConductorStatus, humanizeAge, } from '../Deriver/liveness.js';
3
+ /** JSON:API timestamps arrive as ISO-8601 strings; liveness works in seconds. */
4
+ function toEpochSeconds(value) {
5
+ if (value == null)
6
+ return null;
7
+ if (typeof value === 'number')
8
+ return value;
9
+ if (typeof value !== 'string')
10
+ return null;
11
+ const ms = Date.parse(value);
12
+ return Number.isNaN(ms) ? null : Math.floor(ms / 1000);
13
+ }
14
+ /** Id of a single-valued relationship linkage (`data` object), or null. */
15
+ function singleRelId(rel) {
16
+ const data = rel?.data;
17
+ if (!data || Array.isArray(data))
18
+ return null;
19
+ return data.id ?? null;
20
+ }
21
+ // Most-alive-first ranking so a project with several conductors reports its
22
+ // liveliest one, not whichever the collection happened to return first.
23
+ const STATUS_RANK = {
24
+ running: 0,
25
+ stale: 1,
26
+ offline: 2,
27
+ 'registry-only': 3,
28
+ };
29
+ /**
30
+ * Resolve the dashboard's project rows straight from the control plane: list the
31
+ * `gaia_conductor` entities (read is owner-gated, so this is already scoped to
32
+ * the current user's conductors — no local registry needed), sideload their
33
+ * `project_id`, group by project name, and report each project's liveliest
34
+ * conductor (running > stale > offline). Everything is a Drupal entity read
35
+ * through `services.client` (AC-5); the dashboard itself stays a screen/route.
36
+ */
37
+ export async function loadDashboardProjects({ client, nowSeconds, }) {
38
+ const params = new DrupalJsonApiParams().addInclude(['project_id']);
39
+ let doc;
40
+ try {
41
+ doc = (await client.get('gaia_conductor/gaia_conductor', params));
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ // Resolve each conductor's project name from the sideloaded gaia_project.
47
+ const projectNameById = new Map();
48
+ for (const inc of doc.included ?? []) {
49
+ if (inc.id && typeof inc.attributes?.name === 'string') {
50
+ projectNameById.set(inc.id, inc.attributes.name);
51
+ }
52
+ }
53
+ const now = nowSeconds();
54
+ const best = new Map();
55
+ for (const c of doc.data ?? []) {
56
+ const a = c.attributes ?? {};
57
+ const projectId = singleRelId(c.relationships?.project_id);
58
+ const project = projectId ? projectNameById.get(projectId) : undefined;
59
+ // A conductor whose project cannot be resolved is not a dashboard row.
60
+ if (!project)
61
+ continue;
62
+ const lastSeen = toEpochSeconds(a.last_seen ?? null);
63
+ const derivedStatus = deriveConductorStatus({
64
+ present: true,
65
+ status: typeof a.status === 'string' ? a.status : null,
66
+ lastSeen,
67
+ leaseExpiresAt: toEpochSeconds(a.lease_expires_at ?? null),
68
+ }, now);
69
+ const prev = best.get(project);
70
+ if (!prev || STATUS_RANK[derivedStatus] < STATUS_RANK[prev.derivedStatus]) {
71
+ best.set(project, { derivedStatus, lastSeen });
72
+ }
73
+ }
74
+ return [...best.entries()].map(([project, b]) => ({
75
+ project,
76
+ derivedStatus: b.derivedStatus,
77
+ lastSeen: b.lastSeen,
78
+ ageLabel: humanizeAge(b.lastSeen, now),
79
+ }));
80
+ }
@@ -0,0 +1,35 @@
1
+ import type { DropshClient, JsonApiResource } from '../types.js';
2
+ /** One flat-table row derived from a `gaia_run` dropsh resource. */
3
+ export interface RunRow {
4
+ id: string;
5
+ /** The `#844`-style internal id (human handle in the RUN column). */
6
+ drupalId: number | string | null;
7
+ /** The run's ticket relationship id, or null when absent. */
8
+ ticketId: string | null;
9
+ /** The ticket's `identifier` from the sideloaded include, or '—'. */
10
+ ticketIdentifier: string;
11
+ /** Authoritative run state (`claimed|running|done|expired|failed`). */
12
+ state: string;
13
+ /** The ticket state the run was dispatched for (`state_at_start`). */
14
+ phase: string;
15
+ /** The executing agent (`claude`, `codex`, …). */
16
+ agent: string;
17
+ /** ISO start time; falls back to `created` when the run never started. */
18
+ startedAt: string | null;
19
+ heartbeat: string | null;
20
+ claimExpiresAt: string | null;
21
+ }
22
+ /**
23
+ * Map a RAW JSON:API `data[]` entry (from `client.get`) to a run row, resolving
24
+ * the ticket identifier from the sideloaded includes indexed by id. Mirrors
25
+ * `mapTicketData` — the only I/O is upstream in {@link loadProjectRuns}.
26
+ */
27
+ export declare function mapRunData(item: JsonApiResource, ticketsById: Map<string, JsonApiResource>): RunRow;
28
+ /**
29
+ * List the `gaia_run` entities of a project, most-recently-started first.
30
+ * Filtered by `ticket_id.project_id.id`, includes `ticket_id`, sorts
31
+ * `started_at` DESC, pages 50 (deeper history via offset is a later follow-up).
32
+ */
33
+ export declare function loadProjectRuns(client: DropshClient, projectId: string): Promise<RunRow[]>;
34
+ /** The selected run's ticket id, or null when out of range / ticket-less. */
35
+ export declare function selectedRunTicketId(rows: Pick<RunRow, 'ticketId'>[], index: number): string | null;
@@ -0,0 +1,62 @@
1
+ // Project-scoped run list reads — through the dropsh JSON:API client
2
+ // (`services.client`). Every rendered object is a `gaia_run` Drupal entity
3
+ // (AC-5). Runs are scoped to the selected project via the nested filter
4
+ // `ticket_id.project_id.id` (a project may span several conductors, so the
5
+ // ticket→project relationship is the canonical scope, not `conductor_id`).
6
+ import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
7
+ /** Read a value as a string, or `undefined` when it is not one. */
8
+ function str(value) {
9
+ return typeof value === 'string' ? value : undefined;
10
+ }
11
+ /**
12
+ * Map a RAW JSON:API `data[]` entry (from `client.get`) to a run row, resolving
13
+ * the ticket identifier from the sideloaded includes indexed by id. Mirrors
14
+ * `mapTicketData` — the only I/O is upstream in {@link loadProjectRuns}.
15
+ */
16
+ export function mapRunData(item, ticketsById) {
17
+ const a = item.attributes ?? {};
18
+ const ticketRel = item.relationships?.ticket_id?.data;
19
+ const ticketId = Array.isArray(ticketRel)
20
+ ? (ticketRel[0]?.id ?? null)
21
+ : (ticketRel?.id ?? null);
22
+ const ticket = ticketId ? ticketsById.get(ticketId) : undefined;
23
+ const drupalId = typeof a.drupal_internal__id === 'number' ||
24
+ typeof a.drupal_internal__id === 'string'
25
+ ? a.drupal_internal__id
26
+ : null;
27
+ return {
28
+ id: item.id ?? '',
29
+ drupalId,
30
+ ticketId,
31
+ ticketIdentifier: str(ticket?.attributes?.identifier) ?? (ticketId ? ticketId : '—'),
32
+ state: str(a.state) ?? 'unknown',
33
+ phase: str(a.state_at_start) ?? '—',
34
+ agent: str(a.agent) ?? '—',
35
+ startedAt: str(a.started_at) ?? str(a.created) ?? null,
36
+ heartbeat: str(a.heartbeat) ?? null,
37
+ claimExpiresAt: str(a.claim_expires_at) ?? null,
38
+ };
39
+ }
40
+ /**
41
+ * List the `gaia_run` entities of a project, most-recently-started first.
42
+ * Filtered by `ticket_id.project_id.id`, includes `ticket_id`, sorts
43
+ * `started_at` DESC, pages 50 (deeper history via offset is a later follow-up).
44
+ */
45
+ export async function loadProjectRuns(client, projectId) {
46
+ const params = new DrupalJsonApiParams();
47
+ params.addFilter('ticket_id.project_id.id', projectId);
48
+ params.addInclude(['ticket_id']);
49
+ params.addSort('started_at', 'DESC');
50
+ params.addPageLimit(50);
51
+ const doc = (await client.get('gaia_run/gaia_run', params));
52
+ const ticketsById = new Map();
53
+ for (const inc of doc?.included ?? []) {
54
+ if (inc.id)
55
+ ticketsById.set(inc.id, inc);
56
+ }
57
+ return (doc?.data ?? []).map((item) => mapRunData(item, ticketsById));
58
+ }
59
+ /** The selected run's ticket id, or null when out of range / ticket-less. */
60
+ export function selectedRunTicketId(rows, index) {
61
+ return rows[index]?.ticketId ?? null;
62
+ }
@@ -0,0 +1,6 @@
1
+ import type { DropshClient, TicketDetailDocument } from '../types.js';
2
+ /**
3
+ * Load one ticket's full detail document (the entity + its sideloaded
4
+ * comments, sub-tickets, runs and referenced tickets).
5
+ */
6
+ export declare function loadTicketDetail(client: DropshClient, ticketId: string): Promise<TicketDetailDocument>;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Load one ticket's full detail document (the entity + its sideloaded
3
+ * comments, sub-tickets, runs and referenced tickets).
4
+ */
5
+ export function loadTicketDetail(client, ticketId) {
6
+ return client.get(`gaia_ticket/gaia_ticket/${ticketId}?include=comments,sub_tickets,runs,referenced_tickets`);
7
+ }
@@ -0,0 +1,6 @@
1
+ import gaiaUiPlugin from './plugin.js';
2
+ export default gaiaUiPlugin;
3
+ export type { GaiaUiOptions } from './plugin.js';
4
+ export declare const ticketRendererPlugin: typeof gaiaUiPlugin;
5
+ export { type DropshBuildProgram, type DropshProgram, type GaiaUiLauncherOptions, type HomeConnection, runGaiaUi, } from './launcher.js';
6
+ export type { HostedAgent, TuiAgentService } from './types.js';
@@ -0,0 +1,10 @@
1
+ import gaiaUiPlugin from './plugin.js';
2
+ // The dropsh `tui` renderer plugin factory (default export) — the conductor's
3
+ // plugin resolver auto-picks the default.
4
+ export default gaiaUiPlugin;
5
+ // Back-compat alias for the former package's named export.
6
+ export const ticketRendererPlugin = gaiaUiPlugin;
7
+ // GAIA-194 AC-4/AC-5: the home-rooted launcher (dropsh-tui invocation moved out
8
+ // of the conductor `gaia ui` command). herdr-agnostic (agent impl injected) and
9
+ // dropsh-decoupled (buildProgram injected).
10
+ export { runGaiaUi, } from './launcher.js';