@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.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/src/Component/agent-list/index.d.ts +6 -0
- package/dist/src/Component/agent-list/index.js +14 -0
- package/dist/src/Component/comment-item/index.d.ts +21 -0
- package/dist/src/Component/comment-item/index.js +39 -0
- package/dist/src/Component/create-form/index.d.ts +3 -0
- package/dist/src/Component/create-form/index.js +26 -0
- package/dist/src/Component/dashboard-list/index.d.ts +8 -0
- package/dist/src/Component/dashboard-list/index.js +7 -0
- package/dist/src/Component/run-table/index.d.ts +10 -0
- package/dist/src/Component/run-table/index.js +20 -0
- package/dist/src/Component/section-list/index.d.ts +9 -0
- package/dist/src/Component/section-list/index.js +27 -0
- package/dist/src/Component/tab-bar/index.d.ts +9 -0
- package/dist/src/Component/tab-bar/index.js +22 -0
- package/dist/src/Component/ticket-detail/index.d.ts +30 -0
- package/dist/src/Component/ticket-detail/index.js +129 -0
- package/dist/src/Component/ticket-table/index.d.ts +5 -0
- package/dist/src/Component/ticket-table/index.js +14 -0
- package/dist/src/Component/top-tabs/index.d.ts +8 -0
- package/dist/src/Component/top-tabs/index.js +13 -0
- package/dist/src/Deriver/liveness.d.ts +12 -0
- package/dist/src/Deriver/liveness.js +34 -0
- package/dist/src/Deriver/run-status.d.ts +12 -0
- package/dist/src/Deriver/run-status.js +36 -0
- package/dist/src/Kernel/tui-kernel.d.ts +2 -0
- package/dist/src/Kernel/tui-kernel.js +983 -0
- package/dist/src/Screen/dashboard.d.ts +1 -0
- package/dist/src/Screen/dashboard.js +1 -0
- package/dist/src/Screen/project.d.ts +1 -0
- package/dist/src/Screen/project.js +1 -0
- package/dist/src/Screen/ticket.d.ts +1 -0
- package/dist/src/Screen/ticket.js +1 -0
- package/dist/src/Service/agent.d.ts +17 -0
- package/dist/src/Service/agent.js +28 -0
- package/dist/src/Service/create-form-data.d.ts +10 -0
- package/dist/src/Service/create-form-data.js +52 -0
- package/dist/src/Service/create-form.d.ts +52 -0
- package/dist/src/Service/create-form.js +98 -0
- package/dist/src/Service/project-data.d.ts +48 -0
- package/dist/src/Service/project-data.js +92 -0
- package/dist/src/Service/projects.d.ts +21 -0
- package/dist/src/Service/projects.js +80 -0
- package/dist/src/Service/run-data.d.ts +35 -0
- package/dist/src/Service/run-data.js +62 -0
- package/dist/src/Service/ticket-data.d.ts +6 -0
- package/dist/src/Service/ticket-data.js +7 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +10 -0
- package/dist/src/launcher.d.ts +58 -0
- package/dist/src/launcher.js +51 -0
- package/dist/src/lib/ansi.d.ts +19 -0
- package/dist/src/lib/ansi.js +45 -0
- package/dist/src/lib/format.d.ts +3 -0
- package/dist/src/lib/format.js +10 -0
- package/dist/src/lib/scroll.d.ts +1 -0
- package/dist/src/lib/scroll.js +16 -0
- package/dist/src/lib/ticket-query.d.ts +21 -0
- package/dist/src/lib/ticket-query.js +72 -0
- package/dist/src/plugin.d.ts +13 -0
- package/dist/src/plugin.js +17 -0
- package/dist/src/preset.d.ts +2 -0
- package/dist/src/preset.js +7 -0
- package/dist/src/types.d.ts +98 -0
- package/dist/src/types.js +5 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 keytec GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @gaia-ai/addon-gaia-ui
|
|
2
|
+
|
|
3
|
+
GAIA TUI: interactive terminal UI (project dashboard, ticket list, ticket detail) as a dropsh `tui` renderer plugin.
|
|
4
|
+
|
|
5
|
+
Part of the GAIA CLI. Install the meta package `@gaia-ai/gaia` to get the `gaia` CLI with all addons. Source: https://git.key-tec.de/keytec/gaia (gaia-cli/).
|
|
@@ -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,21 @@
|
|
|
1
|
+
/** One collapsible comment entry for the Comments tab. */
|
|
2
|
+
export interface CommentItem {
|
|
3
|
+
type: string;
|
|
4
|
+
author: string;
|
|
5
|
+
created: string | null;
|
|
6
|
+
bodyLines: string[];
|
|
7
|
+
preview: string;
|
|
8
|
+
}
|
|
9
|
+
/** The rendered Comments-tab view. */
|
|
10
|
+
export interface CommentsTabView {
|
|
11
|
+
lines: string[];
|
|
12
|
+
boldRows: Set<number>;
|
|
13
|
+
headerRows: number[];
|
|
14
|
+
selectable: number;
|
|
15
|
+
}
|
|
16
|
+
/** The relative age of a comment's ISO `created`, or '—' when unknown. */
|
|
17
|
+
export declare function relativeCreated(created: string | null | undefined, nowSeconds: number): string;
|
|
18
|
+
export declare function commentsTabView(items: CommentItem[], { expanded, nowSeconds, }?: {
|
|
19
|
+
expanded?: Set<number>;
|
|
20
|
+
nowSeconds?: number;
|
|
21
|
+
}): CommentsTabView;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { humanizeAge } from '../../Deriver/liveness.js';
|
|
2
|
+
/** The relative age of a comment's ISO `created`, or '—' when unknown. */
|
|
3
|
+
export function relativeCreated(created, nowSeconds) {
|
|
4
|
+
const ms = created ? Date.parse(created) : Number.NaN;
|
|
5
|
+
return Number.isNaN(ms)
|
|
6
|
+
? '—'
|
|
7
|
+
: humanizeAge(Math.floor(ms / 1000), nowSeconds);
|
|
8
|
+
}
|
|
9
|
+
// The Comments tab: one collapsible entry per comment. Collapsed = a single
|
|
10
|
+
// caret header row (`▸ type · author · age · preview`); expanded (`▾`) appends
|
|
11
|
+
// the full body, one physical row per body line. `headerRows[i]` is the row
|
|
12
|
+
// index of comment i's header (for selection highlight + scroll).
|
|
13
|
+
export function commentsTabView(items, { expanded = new Set(), nowSeconds = 0, } = {}) {
|
|
14
|
+
if (!items.length) {
|
|
15
|
+
return {
|
|
16
|
+
lines: ['No comments.'],
|
|
17
|
+
boldRows: new Set(),
|
|
18
|
+
headerRows: [],
|
|
19
|
+
selectable: 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const lines = [];
|
|
23
|
+
const boldRows = new Set();
|
|
24
|
+
const headerRows = [];
|
|
25
|
+
items.forEach((item, i) => {
|
|
26
|
+
headerRows.push(lines.length);
|
|
27
|
+
boldRows.add(lines.length);
|
|
28
|
+
const caret = expanded.has(i) ? '▾' : '▸';
|
|
29
|
+
const age = relativeCreated(item.created, nowSeconds);
|
|
30
|
+
const author = item.author || '—';
|
|
31
|
+
lines.push(`${caret} ${item.type} · ${author} · ${age} · ${item.preview}`);
|
|
32
|
+
if (expanded.has(i)) {
|
|
33
|
+
for (const bodyLine of item.bodyLines)
|
|
34
|
+
lines.push(` ${bodyLine}`);
|
|
35
|
+
lines.push('');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return { lines, boldRows, headerRows, selectable: items.length };
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const labelOf = (list, id) => list.find((o) => o.id === id)?.label ?? '—';
|
|
2
|
+
/** Render the "New ticket" popup form as plain rows (the kernel frames it). */
|
|
3
|
+
export function createFormLines(s, opts) {
|
|
4
|
+
const mark = (i) => (s.active === i ? '▸ ' : ' ');
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push(`${mark(0)}Description:`);
|
|
7
|
+
const body = s.description.length ? s.description.split('\n') : ['<empty>'];
|
|
8
|
+
for (const line of body)
|
|
9
|
+
lines.push(` ${line}`);
|
|
10
|
+
lines.push(`${mark(1)}Assignee: < ${labelOf(opts.users, s.assignee)} >`);
|
|
11
|
+
lines.push(`${mark(2)}Conductor: < ${labelOf(opts.conductors, s.conductor)} >`);
|
|
12
|
+
lines.push(`${mark(3)}Labels:`);
|
|
13
|
+
if (opts.labels.length === 0) {
|
|
14
|
+
lines.push(' (none available)');
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
opts.labels.forEach((o, i) => {
|
|
18
|
+
const box = s.labels.has(o.id) ? '[x]' : '[ ]';
|
|
19
|
+
const cursor = s.active === 3 && s.labelCursor === i ? '›' : ' ';
|
|
20
|
+
lines.push(` ${cursor}${box} ${o.label}`);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
lines.push(`${mark(4)}Workflow: < ${labelOf(opts.workflows, s.workflow)} >`);
|
|
24
|
+
lines.push(`${mark(5)}[ Create ticket ]`);
|
|
25
|
+
return lines;
|
|
26
|
+
}
|
|
@@ -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,10 @@
|
|
|
1
|
+
import type { RunRow } from '../../Service/run-data.js';
|
|
2
|
+
/**
|
|
3
|
+
* A fixed-column flat table of a project's runs: header + one line per run.
|
|
4
|
+
* Columns RUN · TICKET · STATE · PHASE · AGENT · STARTED, mirroring
|
|
5
|
+
* `ticketTableRows` (1 logical entry = 1 physical row). STATE is the derived
|
|
6
|
+
* liveness status and STARTED is the relative start age.
|
|
7
|
+
*/
|
|
8
|
+
export declare function runTableRows(runs: RunRow[], nowSeconds: number, { emptyMessage }?: {
|
|
9
|
+
emptyMessage?: string | undefined;
|
|
10
|
+
}): string[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { deriveRunStatus, runStartedAge } from '../../Deriver/run-status.js';
|
|
2
|
+
import { pad } from '../../lib/format.js';
|
|
3
|
+
/**
|
|
4
|
+
* A fixed-column flat table of a project's runs: header + one line per run.
|
|
5
|
+
* Columns RUN · TICKET · STATE · PHASE · AGENT · STARTED, mirroring
|
|
6
|
+
* `ticketTableRows` (1 logical entry = 1 physical row). STATE is the derived
|
|
7
|
+
* liveness status and STARTED is the relative start age.
|
|
8
|
+
*/
|
|
9
|
+
export function runTableRows(runs, nowSeconds, { emptyMessage } = {}) {
|
|
10
|
+
const header = `${pad('RUN', 8)} ${pad('TICKET', 10)} ${pad('STATE', 8)} ${pad('PHASE', 10)} ${pad('AGENT', 8)} STARTED`;
|
|
11
|
+
if (runs.length === 0)
|
|
12
|
+
return [header, emptyMessage ?? 'No runs for this project.'];
|
|
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
|
+
];
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Flatten `{heading, lines}` sections into plain rows plus the indices that are
|
|
2
|
+
// bold (section headings + markdown ATX headings). Headings carry NO ANSI here —
|
|
3
|
+
// the draw step applies bold after fit(), so sanitizeDisplay never sees (and
|
|
4
|
+
// never strips) the ESC.
|
|
5
|
+
export function flattenSections(sections) {
|
|
6
|
+
const lines = [];
|
|
7
|
+
const boldRows = new Set();
|
|
8
|
+
for (const section of sections) {
|
|
9
|
+
boldRows.add(lines.length);
|
|
10
|
+
lines.push(section.heading);
|
|
11
|
+
for (const line of section.lines) {
|
|
12
|
+
// Minimal markdown: an ATX heading (`#`…`######`) renders as bold with the
|
|
13
|
+
// markers stripped — same rule as a section heading, styled after fit().
|
|
14
|
+
// Bullets and code fences stay raw by design (explicit v1 decision).
|
|
15
|
+
const md = /^(#{1,6})\s+(.*)$/.exec(line);
|
|
16
|
+
if (md) {
|
|
17
|
+
boldRows.add(lines.length);
|
|
18
|
+
lines.push(md[2] ?? '');
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
lines.push(line);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
lines.push('');
|
|
25
|
+
}
|
|
26
|
+
return { lines, boldRows };
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** One ticket-detail tab: its id, label, and the section headings it groups. */
|
|
2
|
+
export interface DetailTab {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
headings: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const DETAIL_TABS: DetailTab[];
|
|
8
|
+
/** The tab bar as a single row; the active tab is bracketed. */
|
|
9
|
+
export declare function tabBar(activeIndex: number): string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The ticket-detail screen is split into tabs so only one tab's content draws at
|
|
2
|
+
// a time (keeps the fixed-height window small) — Sub-tickets + Referenced share a
|
|
3
|
+
// Related tab, Comments is its own tab with collapsible entries (owned by the
|
|
4
|
+
// plugin's draw loop, which needs the interactive expand state).
|
|
5
|
+
export const DETAIL_TABS = [
|
|
6
|
+
{
|
|
7
|
+
id: 'overview',
|
|
8
|
+
label: 'Overview',
|
|
9
|
+
headings: ['Description', 'Branch', 'Links', 'Environments'],
|
|
10
|
+
},
|
|
11
|
+
{ id: 'comments', label: 'Comments', headings: [] },
|
|
12
|
+
{
|
|
13
|
+
id: 'related',
|
|
14
|
+
label: 'Related',
|
|
15
|
+
headings: ['Sub-tickets', 'Referenced tickets'],
|
|
16
|
+
},
|
|
17
|
+
{ id: 'runs', label: 'Runs', headings: ['Runs'] },
|
|
18
|
+
];
|
|
19
|
+
/** The tab bar as a single row; the active tab is bracketed. */
|
|
20
|
+
export function tabBar(activeIndex) {
|
|
21
|
+
return DETAIL_TABS.map((t, i) => i === activeIndex ? `[${t.label}]` : ` ${t.label} `).join(' ');
|
|
22
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TicketDetailDocument } from '../../types.js';
|
|
2
|
+
import type { CommentItem } from '../comment-item/index.js';
|
|
3
|
+
import type { Section } from '../section-list/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* The ticket detail sections, each with an explicit empty state:
|
|
6
|
+
* Description, Comments, Sub-tickets, Runs, Referenced tickets, Links,
|
|
7
|
+
* Branch, Environments.
|
|
8
|
+
*/
|
|
9
|
+
export declare function detailSections(document: TicketDetailDocument): Section[];
|
|
10
|
+
/** The detailSections that belong to a given (non-comments) tab, in tab order. */
|
|
11
|
+
export declare function sectionsForTab(document: TicketDetailDocument, tabIndex: number): Section[];
|
|
12
|
+
/**
|
|
13
|
+
* One item per comment for the collapsible Comments tab: type, author, created
|
|
14
|
+
* (raw ISO — the plugin formats the relative age), the body split into physical
|
|
15
|
+
* lines, and a first-line preview for the collapsed header.
|
|
16
|
+
*/
|
|
17
|
+
export declare function commentItems(document: TicketDetailDocument): CommentItem[];
|
|
18
|
+
export declare function detailLines(detail?: TicketDetailDocument | null): {
|
|
19
|
+
lines: string[];
|
|
20
|
+
boldRows: Set<number>;
|
|
21
|
+
};
|
|
22
|
+
export declare function tabViewLines(detail: TicketDetailDocument | null | undefined, tabIndex: number, { expanded, nowSeconds, }?: {
|
|
23
|
+
expanded?: Set<number>;
|
|
24
|
+
nowSeconds?: number;
|
|
25
|
+
}): {
|
|
26
|
+
lines: string[];
|
|
27
|
+
boldRows: Set<number>;
|
|
28
|
+
headerRows: number[];
|
|
29
|
+
selectable: number;
|
|
30
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// The ticket-detail screen: composes section-list (flatten + markdown bold),
|
|
2
|
+
// tab-bar (DETAIL_TABS) and comment-item (the collapsible Comments tab) into
|
|
3
|
+
// the two shapes the plugin's draw loop consumes — the whole document
|
|
4
|
+
// flattened (`detailLines`, non-TTY/back-compat) and one tab at a time
|
|
5
|
+
// (`tabViewLines`, the interactive screen).
|
|
6
|
+
import { asString } from '../../lib/format.js';
|
|
7
|
+
import { commentsTabView } from '../comment-item/index.js';
|
|
8
|
+
import { flattenSections } from '../section-list/index.js';
|
|
9
|
+
import { DETAIL_TABS } from '../tab-bar/index.js';
|
|
10
|
+
function fieldValue(value) {
|
|
11
|
+
if (value && typeof value === 'object' && 'value' in value) {
|
|
12
|
+
const inner = value.value;
|
|
13
|
+
if (typeof inner === 'string')
|
|
14
|
+
return inner;
|
|
15
|
+
}
|
|
16
|
+
return typeof value === 'string' ? value : '';
|
|
17
|
+
}
|
|
18
|
+
function refCount(document, name) {
|
|
19
|
+
const data = document.data?.relationships?.[name]?.data;
|
|
20
|
+
return Array.isArray(data) ? data.length : data ? 1 : 0;
|
|
21
|
+
}
|
|
22
|
+
function commentResources(document) {
|
|
23
|
+
return (document.included ?? []).filter((r) => r.type === 'gaia_comment--gaia_comment');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The ticket detail sections, each with an explicit empty state:
|
|
27
|
+
* Description, Comments, Sub-tickets, Runs, Referenced tickets, Links,
|
|
28
|
+
* Branch, Environments.
|
|
29
|
+
*/
|
|
30
|
+
export function detailSections(document) {
|
|
31
|
+
const a = document.data?.attributes ?? {};
|
|
32
|
+
const sections = [];
|
|
33
|
+
const description = fieldValue(a.description);
|
|
34
|
+
sections.push({
|
|
35
|
+
heading: 'Description',
|
|
36
|
+
lines: description ? description.split('\n') : ['No description.'],
|
|
37
|
+
});
|
|
38
|
+
const comments = commentResources(document);
|
|
39
|
+
sections.push({
|
|
40
|
+
heading: 'Comments',
|
|
41
|
+
// One physical line per body line: a `— type ·` marker followed by the body
|
|
42
|
+
// split on '\n' (mirrors the Description section). Keeping a multi-line body
|
|
43
|
+
// in a single entry breaks draw()'s one-entry-one-row invariant (AC-4).
|
|
44
|
+
lines: comments.length
|
|
45
|
+
? comments.flatMap((c) => {
|
|
46
|
+
const type = asString(c.attributes?.gaia_comment_type) ?? 'comment';
|
|
47
|
+
const body = fieldValue(c.attributes?.body);
|
|
48
|
+
return [`— ${type} ·`, ...(body ? body.split('\n') : [''])];
|
|
49
|
+
})
|
|
50
|
+
: ['No comments.'],
|
|
51
|
+
});
|
|
52
|
+
const relSection = (heading, rel) => {
|
|
53
|
+
const n = refCount(document, rel);
|
|
54
|
+
sections.push({
|
|
55
|
+
heading,
|
|
56
|
+
lines: n
|
|
57
|
+
? [`${n} ${heading.toLowerCase()}`]
|
|
58
|
+
: [`No ${heading.toLowerCase()}.`],
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
relSection('Sub-tickets', 'sub_tickets');
|
|
62
|
+
relSection('Runs', 'runs');
|
|
63
|
+
relSection('Referenced tickets', 'referenced_tickets');
|
|
64
|
+
const links = Array.isArray(a.links) ? a.links : [];
|
|
65
|
+
sections.push({
|
|
66
|
+
heading: 'Links',
|
|
67
|
+
lines: links.length
|
|
68
|
+
? links.map((l) => {
|
|
69
|
+
const link = l;
|
|
70
|
+
return `${link.title ?? ''} ${link.uri ?? link.url ?? ''}`.trim();
|
|
71
|
+
})
|
|
72
|
+
: ['No links.'],
|
|
73
|
+
});
|
|
74
|
+
sections.push({
|
|
75
|
+
heading: 'Branch',
|
|
76
|
+
lines: [
|
|
77
|
+
`branch: ${asString(a.branch_name) ?? '—'}`,
|
|
78
|
+
`base: ${asString(a.base_branch) ?? '—'}`,
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
relSection('Environments', 'environments');
|
|
82
|
+
return sections;
|
|
83
|
+
}
|
|
84
|
+
/** The detailSections that belong to a given (non-comments) tab, in tab order. */
|
|
85
|
+
export function sectionsForTab(document, tabIndex) {
|
|
86
|
+
const tab = DETAIL_TABS[tabIndex];
|
|
87
|
+
const headings = tab?.headings ?? [];
|
|
88
|
+
if (!headings.length)
|
|
89
|
+
return [];
|
|
90
|
+
const byHeading = new Map(detailSections(document).map((s) => [s.heading, s]));
|
|
91
|
+
return headings
|
|
92
|
+
.map((h) => byHeading.get(h))
|
|
93
|
+
.filter((s) => s !== undefined);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* One item per comment for the collapsible Comments tab: type, author, created
|
|
97
|
+
* (raw ISO — the plugin formats the relative age), the body split into physical
|
|
98
|
+
* lines, and a first-line preview for the collapsed header.
|
|
99
|
+
*/
|
|
100
|
+
export function commentItems(document) {
|
|
101
|
+
const comments = commentResources(document);
|
|
102
|
+
return comments.map((c) => {
|
|
103
|
+
const type = asString(c.attributes?.gaia_comment_type) ?? 'comment';
|
|
104
|
+
const author = asString(c.attributes?.author_name) ?? '';
|
|
105
|
+
const created = asString(c.attributes?.created) ?? null;
|
|
106
|
+
const body = fieldValue(c.attributes?.body);
|
|
107
|
+
const bodyLines = body ? body.split('\n') : [''];
|
|
108
|
+
return { type, author, created, bodyLines, preview: bodyLines[0] ?? '' };
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// The whole ticket-detail document flattened (all sections). Reserved helper —
|
|
112
|
+
// currently unused by the interactive screen (which renders one tab at a time
|
|
113
|
+
// via tabViewLines); kept as a tested convenience for a full-document render.
|
|
114
|
+
export function detailLines(detail) {
|
|
115
|
+
return flattenSections(detailSections(detail ?? {}));
|
|
116
|
+
}
|
|
117
|
+
// The rows for one detail tab. The Comments tab is the collapsible list; every
|
|
118
|
+
// other tab flattens just its own sections. Returns `selectable`/`headerRows`
|
|
119
|
+
// so the draw loop can highlight + scroll the selected comment (0 elsewhere).
|
|
120
|
+
export function tabViewLines(detail, tabIndex, { expanded = new Set(), nowSeconds = 0, } = {}) {
|
|
121
|
+
if (DETAIL_TABS[tabIndex]?.id === 'comments') {
|
|
122
|
+
return commentsTabView(commentItems(detail ?? {}), {
|
|
123
|
+
expanded,
|
|
124
|
+
nowSeconds,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const { lines, boldRows } = flattenSections(sectionsForTab(detail ?? {}, tabIndex));
|
|
128
|
+
return { lines, boldRows, headerRows: [], selectable: 0 };
|
|
129
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { pad } from '../../lib/format.js';
|
|
2
|
+
/** A fixed-column flat table: header + one line per ticket. */
|
|
3
|
+
export function ticketTableRows(tickets, { emptyMessage } = {}) {
|
|
4
|
+
const header = `${pad('IDENTIFIER', 10)} ${pad('STATE', 8)} ${pad('PRIO', 5)} ${pad('WHO', 4)} ${pad('RUN', 8)} ${pad('PARENT', 10)} ${pad('COND', 14)} TITLE`;
|
|
5
|
+
if (tickets.length === 0)
|
|
6
|
+
return [header, emptyMessage ?? 'No tickets.'];
|
|
7
|
+
return [
|
|
8
|
+
header,
|
|
9
|
+
...tickets.map((t) => {
|
|
10
|
+
const labels = t.labels?.length ? ` [${t.labels.join(',')}]` : '';
|
|
11
|
+
return `${pad(t.identifier, 10)} ${pad(t.state, 8)} ${pad(t.priority ?? '—', 5)} ${pad(t.assigneeInitials || '—', 4)} ${pad(t.derivedStatus, 8)} ${pad(t.parent ?? '—', 10)} ${pad(t.conductor ?? '—', 14)} ${t.title}${labels}`;
|
|
12
|
+
}),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** One top-level TUI tab: its id and label. */
|
|
2
|
+
export interface TopTab {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const TOP_TABS: TopTab[];
|
|
7
|
+
/** The top tab bar as a single row; the active tab is bracketed. */
|
|
8
|
+
export declare function topTabBar(activeId: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The top-level navigation tabs. `tickets` is the project/ticket drill-down;
|
|
2
|
+
// `ticket-agents` is the managed create-agent list (GAIA-190); `runs` is the
|
|
3
|
+
// project-scoped run overview (GAIA-192). `conductors` is a planned follow-up
|
|
4
|
+
// (tracked separately) — not wired yet.
|
|
5
|
+
export const TOP_TABS = [
|
|
6
|
+
{ id: 'tickets', label: 'Tickets' },
|
|
7
|
+
{ id: 'ticket-agents', label: 'Ticket-Agents' },
|
|
8
|
+
{ id: 'runs', label: 'Runs' },
|
|
9
|
+
];
|
|
10
|
+
/** The top tab bar as a single row; the active tab is bracketed. */
|
|
11
|
+
export function topTabBar(activeId) {
|
|
12
|
+
return TOP_TABS.map((t) => t.id === activeId ? `[${t.label}]` : ` ${t.label} `).join(' ');
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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;
|
|
11
|
+
/** A short relative age label ("just now", "3m ago", …); '—' when unknown. */
|
|
12
|
+
export declare function humanizeAge(lastSeenSeconds: number | null, nowSeconds: number): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
/** A short relative age label ("just now", "3m ago", …); '—' when unknown. */
|
|
23
|
+
export function humanizeAge(lastSeenSeconds, nowSeconds) {
|
|
24
|
+
if (lastSeenSeconds == null)
|
|
25
|
+
return '—';
|
|
26
|
+
const delta = Math.max(0, nowSeconds - lastSeenSeconds);
|
|
27
|
+
if (delta < 45)
|
|
28
|
+
return 'just now';
|
|
29
|
+
if (delta < 3600)
|
|
30
|
+
return `${Math.round(delta / 60)}m ago`;
|
|
31
|
+
if (delta < 86_400)
|
|
32
|
+
return `${Math.round(delta / 3600)}h ago`;
|
|
33
|
+
return `${Math.round(delta / 86_400)}d ago`;
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RunRow } from '../Service/run-data.js';
|
|
2
|
+
/** The derived, display-facing status of a run. */
|
|
3
|
+
export type RunStatus = 'running' | 'stale' | 'claimed' | 'done' | 'expired' | 'failed' | string;
|
|
4
|
+
/**
|
|
5
|
+
* Derive the run's display status. Terminal states (`done`/`expired`/`failed`)
|
|
6
|
+
* pass through. An active run (`running`/`claimed`) is `stale` when its
|
|
7
|
+
* heartbeat is older than the liveness window OR its claim has expired; else it
|
|
8
|
+
* keeps its own state.
|
|
9
|
+
*/
|
|
10
|
+
export declare function deriveRunStatus(row: RunRow, nowSeconds: number): RunStatus;
|
|
11
|
+
/** A short relative age of the run's start ("just now", "1h ago", …), '—'. */
|
|
12
|
+
export declare function runStartedAge(row: RunRow, nowSeconds: number): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { humanizeAge } from './liveness.js';
|
|
2
|
+
const STALE_WINDOW_SECONDS = 600;
|
|
3
|
+
/** Parse an ISO timestamp (or a numeric unix value) to unix seconds, or null. */
|
|
4
|
+
function toSeconds(value) {
|
|
5
|
+
if (value == null)
|
|
6
|
+
return null;
|
|
7
|
+
if (typeof value === 'number')
|
|
8
|
+
return value;
|
|
9
|
+
const ms = Date.parse(value);
|
|
10
|
+
if (!Number.isNaN(ms))
|
|
11
|
+
return Math.floor(ms / 1000);
|
|
12
|
+
const n = Number(value);
|
|
13
|
+
return Number.isFinite(n) ? n : null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Derive the run's display status. Terminal states (`done`/`expired`/`failed`)
|
|
17
|
+
* pass through. An active run (`running`/`claimed`) is `stale` when its
|
|
18
|
+
* heartbeat is older than the liveness window OR its claim has expired; else it
|
|
19
|
+
* keeps its own state.
|
|
20
|
+
*/
|
|
21
|
+
export function deriveRunStatus(row, nowSeconds) {
|
|
22
|
+
const state = row.state;
|
|
23
|
+
if (state !== 'running' && state !== 'claimed')
|
|
24
|
+
return state;
|
|
25
|
+
const hb = toSeconds(row.heartbeat);
|
|
26
|
+
if (hb != null && nowSeconds - hb > STALE_WINDOW_SECONDS)
|
|
27
|
+
return 'stale';
|
|
28
|
+
const claim = toSeconds(row.claimExpiresAt);
|
|
29
|
+
if (claim != null && claim < nowSeconds)
|
|
30
|
+
return 'stale';
|
|
31
|
+
return state;
|
|
32
|
+
}
|
|
33
|
+
/** A short relative age of the run's start ("just now", "1h ago", …), '—'. */
|
|
34
|
+
export function runStartedAge(row, nowSeconds) {
|
|
35
|
+
return humanizeAge(toSeconds(row.startedAt), nowSeconds);
|
|
36
|
+
}
|