@gaia-ai/addon-gaia-ui 0.9.2 → 0.11.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.
Files changed (136) hide show
  1. package/dist/src/Component/footer-actions/index.d.ts +58 -0
  2. package/dist/src/Component/footer-actions/index.js +111 -0
  3. package/dist/src/Component/list-chrome/index.d.ts +19 -0
  4. package/dist/src/Component/list-chrome/index.js +61 -0
  5. package/dist/src/Component/markdown/index.d.ts +68 -0
  6. package/dist/src/Component/markdown/index.js +133 -0
  7. package/dist/src/Component/section-list/index.d.ts +115 -23
  8. package/dist/src/Component/section-list/index.js +120 -34
  9. package/dist/src/Component/top-tabs/index.d.ts +110 -7
  10. package/dist/src/Component/top-tabs/index.js +88 -9
  11. package/dist/src/Entity/collection.js +1 -1
  12. package/dist/src/Entity/conductor/Data/conductors.d.ts +4 -3
  13. package/dist/src/Entity/conductor/Data/conductors.js +76 -10
  14. package/dist/src/Entity/conductor/Data/own-conductor.js +7 -1
  15. package/dist/src/Entity/conductor/Screen/conductors.d.ts +26 -41
  16. package/dist/src/Entity/conductor/Screen/conductors.js +65 -123
  17. package/dist/src/Entity/project/Screen/project-picker.d.ts +20 -31
  18. package/dist/src/Entity/project/Screen/project-picker.js +48 -58
  19. package/dist/src/Entity/project/Screen/projects-screen.d.ts +4 -2
  20. package/dist/src/Entity/project/Screen/projects-screen.js +11 -8
  21. package/dist/src/Entity/run/Component/run-detail/index.js +15 -7
  22. package/dist/src/Entity/run/Data/query.d.ts +10 -0
  23. package/dist/src/Entity/run/Data/query.js +16 -1
  24. package/dist/src/Entity/run/Data/runs.d.ts +2 -0
  25. package/dist/src/Entity/run/Data/runs.js +19 -1
  26. package/dist/src/Entity/run/Screen/run-detail.d.ts +16 -23
  27. package/dist/src/Entity/run/Screen/run-detail.js +27 -34
  28. package/dist/src/Entity/run/Screen/runs.d.ts +7 -3
  29. package/dist/src/Entity/run/Screen/runs.js +31 -48
  30. package/dist/src/Entity/run/Screen/user-runs.d.ts +79 -0
  31. package/dist/src/Entity/run/Screen/user-runs.js +98 -0
  32. package/dist/src/Entity/statistics/Data/statistics.d.ts +111 -9
  33. package/dist/src/Entity/statistics/Data/statistics.js +121 -9
  34. package/dist/src/Entity/statistics/Screen/dashboard.d.ts +10 -2
  35. package/dist/src/Entity/statistics/Screen/dashboard.js +127 -36
  36. package/dist/src/Entity/team/Data/members.d.ts +30 -0
  37. package/dist/src/Entity/team/Data/members.js +83 -0
  38. package/dist/src/Entity/team/Data/team.d.ts +22 -0
  39. package/dist/src/Entity/team/Data/team.js +38 -0
  40. package/dist/src/Entity/team/Data/teams.d.ts +223 -0
  41. package/dist/src/Entity/team/Data/teams.js +267 -0
  42. package/dist/src/Entity/team/Screen/team-users.d.ts +76 -0
  43. package/dist/src/Entity/team/Screen/team-users.js +215 -0
  44. package/dist/src/Entity/team/Screen/teams-screen.d.ts +116 -0
  45. package/dist/src/Entity/team/Screen/teams-screen.js +349 -0
  46. package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +14 -1
  47. package/dist/src/Entity/ticket/Component/comment-item/index.js +15 -1
  48. package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +8 -3
  49. package/dist/src/Entity/ticket/Component/edit-form/index.js +4 -6
  50. package/dist/src/Entity/ticket/Component/sections/index.js +16 -1
  51. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +102 -13
  52. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +440 -72
  53. package/dist/src/Entity/ticket/Data/detail.d.ts +19 -3
  54. package/dist/src/Entity/ticket/Data/detail.js +20 -4
  55. package/dist/src/Entity/ticket/Data/query.d.ts +7 -0
  56. package/dist/src/Entity/ticket/Data/query.js +18 -2
  57. package/dist/src/Entity/ticket/Data/tickets.d.ts +24 -0
  58. package/dist/src/Entity/ticket/Data/tickets.js +41 -1
  59. package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +10 -4
  60. package/dist/src/Entity/ticket/Screen/create-ticket-form.js +37 -16
  61. package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +6 -4
  62. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +16 -16
  63. package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +6 -4
  64. package/dist/src/Entity/ticket/Screen/parent-filter-form.js +14 -11
  65. package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +5 -2
  66. package/dist/src/Entity/ticket/Screen/ticket-agents.js +14 -14
  67. package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +89 -4
  68. package/dist/src/Entity/ticket/Screen/ticket-detail.js +295 -74
  69. package/dist/src/Entity/ticket/Screen/tickets.d.ts +66 -3
  70. package/dist/src/Entity/ticket/Screen/tickets.js +140 -76
  71. package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +91 -0
  72. package/dist/src/Entity/ticket/Screen/user-tickets.js +94 -0
  73. package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +6 -4
  74. package/dist/src/Entity/ticket/Screen/who-filter-form.js +14 -11
  75. package/dist/src/Entity/transport.d.ts +14 -4
  76. package/dist/src/Entity/transport.js +2 -1
  77. package/dist/src/Entity/user/Data/users.d.ts +87 -0
  78. package/dist/src/Entity/user/Data/users.js +60 -0
  79. package/dist/src/Entity/user/Screen/user-overview.d.ts +33 -0
  80. package/dist/src/Entity/user/Screen/user-overview.js +159 -0
  81. package/dist/src/Entity/user/Screen/users.d.ts +39 -0
  82. package/dist/src/Entity/user/Screen/users.js +136 -0
  83. package/dist/src/Extension/contract.d.ts +58 -0
  84. package/dist/src/Extension/contract.js +1 -0
  85. package/dist/src/Extension/load.d.ts +10 -0
  86. package/dist/src/Extension/load.js +21 -0
  87. package/dist/src/Extension/store.d.ts +11 -0
  88. package/dist/src/Extension/store.js +44 -0
  89. package/dist/src/Kernel/chrome.d.ts +49 -12
  90. package/dist/src/Kernel/chrome.js +150 -57
  91. package/dist/src/Kernel/key-input.js +1 -1
  92. package/dist/src/Kernel/mount.d.ts +1 -1
  93. package/dist/src/Kernel/mount.js +1 -1
  94. package/dist/src/Kernel/regions.d.ts +64 -0
  95. package/dist/src/Kernel/regions.js +328 -0
  96. package/dist/src/Kernel/renderer.d.ts +25 -0
  97. package/dist/src/Kernel/renderer.js +142 -0
  98. package/dist/src/Kernel/splash.d.ts +27 -0
  99. package/dist/src/Kernel/splash.js +72 -0
  100. package/dist/src/Kernel/tui-kernel.js +139 -435
  101. package/dist/src/Routing/context.d.ts +5 -0
  102. package/dist/src/Routing/context.js +6 -0
  103. package/dist/src/Routing/registry.js +21 -2
  104. package/dist/src/Routing/router.js +5 -1
  105. package/dist/src/Routing/routes.js +19 -0
  106. package/dist/src/Screen/async-modal-screen.d.ts +26 -0
  107. package/dist/src/Screen/async-modal-screen.js +44 -0
  108. package/dist/src/Screen/async-screen.d.ts +32 -40
  109. package/dist/src/Screen/async-screen.js +45 -41
  110. package/dist/src/Screen/full-screen.d.ts +1 -0
  111. package/dist/src/Screen/full-screen.js +3 -0
  112. package/dist/src/Screen/list-screen.d.ts +7 -1
  113. package/dist/src/Screen/list-screen.js +17 -6
  114. package/dist/src/Screen/modal-screen.d.ts +6 -0
  115. package/dist/src/Screen/modal-screen.js +2 -0
  116. package/dist/src/Screen/screen.d.ts +42 -6
  117. package/dist/src/Screen/screen.js +2 -2
  118. package/dist/src/Widget/textarea.js +10 -14
  119. package/dist/src/Widget/types.d.ts +1 -1
  120. package/dist/src/launcher.d.ts +24 -26
  121. package/dist/src/launcher.js +11 -11
  122. package/dist/src/lib/format.js +1 -1
  123. package/dist/src/lib/markdown.d.ts +53 -0
  124. package/dist/src/lib/markdown.js +522 -0
  125. package/dist/src/lib/refresh-signal.d.ts +56 -0
  126. package/dist/src/lib/refresh-signal.js +85 -0
  127. package/dist/src/lib/theme.d.ts +36 -0
  128. package/dist/src/lib/theme.js +40 -0
  129. package/dist/src/lib/version.d.ts +5 -0
  130. package/dist/src/lib/version.js +46 -0
  131. package/dist/src/lib/vnode.d.ts +86 -63
  132. package/dist/src/lib/vnode.js +112 -85
  133. package/dist/src/types.d.ts +47 -12
  134. package/dist/src/ui.d.ts +2 -0
  135. package/dist/src/ui.js +1 -0
  136. package/package.json +4 -3
@@ -1,6 +1,6 @@
1
+ import { actionItem } from '../../../Component/footer-actions/index.js';
1
2
  import { ListCursor } from '../../../lib/cursor.js';
2
3
  import { AsyncScreen } from '../../../Screen/async-screen.js';
3
- import { presentation } from '../../../Screen/screen.js';
4
4
  import { listRowIds, listTableTree } from '../../collection.js';
5
5
  import { loadProjectRows, PROJECT_TABLE, } from '../Data/projects.js';
6
6
  /**
@@ -69,19 +69,22 @@ export class ProjectsScreen extends AsyncScreen {
69
69
  selectedIndex: this.selectedIndex,
70
70
  });
71
71
  }
72
- present(context) {
72
+ chrome(context) {
73
73
  const rows = this.rows;
74
- return presentation(this.surface(context), {
74
+ return {
75
75
  selectable: rows.length,
76
76
  selectedIndex: this.selectedIndex,
77
- // The node a click on each row lands on (GAIA-340 Task 11), read off the
78
- // SAME configuration that keys the rows — so the click target and the row
79
- // node cannot be spelled two ways (GAIA-345 D2).
77
+ // Click targets match listTableTree row ids (GAIA-345 D2).
80
78
  rowIds: listRowIds(PROJECT_TABLE, rows),
81
79
  title: 'GAIA projects',
82
80
  meta: `${rows.length} registered project(s) · ${context.services?.baseUrl ?? ''}`,
83
- status: ' ↑↓ select · Enter open · n new · Tab tabs · q quit ',
84
- });
81
+ };
82
+ }
83
+ actions() {
84
+ return [
85
+ actionItem('projects-new', 'n', 'new'),
86
+ actionItem('projects-tab', 'Tab', 'tabs'),
87
+ ];
85
88
  }
86
89
  activate(index, context) {
87
90
  const row = this.readyValue?.[index];
@@ -8,12 +8,20 @@
8
8
  //
9
9
  // GAIA-340 D1 — the FIRST block to hand its content over as renderables rather
10
10
  // than as rows of text: the sections are built once, keyed, and turned into a
11
- // VNode tree. It shares the minimal-markdown rule with the blocks that still
12
- // compose rows (`markdownRow`), so a body line renders the same either way.
11
+ // VNode tree.
12
+ //
13
+ // GAIA-372 D9 — IT IS `markdownRow`'S ONLY CALLER NOW, and the ATX-only rule is
14
+ // THIS SCREEN'S rule rather than a rule shared with the ticket detail. A run log
15
+ // entry is MACHINE OUTPUT, not authored Markdown: an agent line full of `*`, `_`
16
+ // and backticks would be re-styled and have characters deleted by a renderer
17
+ // meant for prose. So the log keeps the narrow rule while the ticket detail's
18
+ // `gaia_rich` bodies go through `Component/markdown/`. Nothing about this screen's
19
+ // behaviour changed; routing the log through the full subset is a separate
20
+ // ticket, not a silent side effect of that one.
13
21
  //
14
22
  // Read-only by design (the ticket's own out-of-scope list): no write, no edit, no
15
23
  // navigation out of it except Esc.
16
- import { markdownRow } from '../../../../Component/section-list/index.js';
24
+ import { markdownRow } from '../../../../Component/markdown/index.js';
17
25
  import { asString } from '../../../../lib/format.js';
18
26
  import { columnNode, textNode } from '../../../../lib/vnode.js';
19
27
  /** The sideloaded `gaia_run` with this id, or undefined. */
@@ -208,10 +216,10 @@ function flatRows(document, runId) {
208
216
  { key: `section-${section.key}`, text: section.heading, bold: true },
209
217
  ...section.rows.map((entry) => ({
210
218
  key: entry.key,
211
- // The same minimal-markdown rule the row surface applies, from the same
212
- // function — an ATX heading inside a body renders bold with its markers
213
- // stripped, and a log line starting with `#` must not render one way on
214
- // screen and another way in a pipe.
219
+ // The log's OWN minimal-markdown rule (GAIA-372 D9): an ATX heading inside
220
+ // a log entry renders bold with its markers stripped, and a log line
221
+ // starting with `#` must not render one way on screen and another way in a
222
+ // pipe. Nothing else in an entry is parsed, deliberately — see the header.
215
223
  ...markdownRow(entry.text),
216
224
  })),
217
225
  // The blank row between two sections, which the flattened surface pushed
@@ -20,3 +20,13 @@ export declare function buildRunQueryParams(projectId: string, criteria: RunCrit
20
20
  size: number;
21
21
  index: number;
22
22
  }): DrupalJsonApiParams;
23
+ /**
24
+ * Build the JSON:API query for one assignee's runs (GAIA-377 D2).
25
+ *
26
+ * Runs carry no user of their own — attribution is
27
+ * `ticket_id.assignee_user_id.id`, matching StatisticsCounter::SOURCES.
28
+ */
29
+ export declare function buildUserRunQueryParams(userId: string, criteria: RunCriteria, page?: {
30
+ size: number;
31
+ index: number;
32
+ }): DrupalJsonApiParams;
@@ -25,6 +25,22 @@ export function describeCriteria(criteria) {
25
25
  export function buildRunQueryParams(projectId, criteria, page = { size: LIST_PAGE_SIZE, index: 0 }) {
26
26
  const params = new DrupalJsonApiParams();
27
27
  params.addFilter('ticket_id.project_id.id', projectId);
28
+ applyRunCriteria(params, criteria, page);
29
+ return params;
30
+ }
31
+ /**
32
+ * Build the JSON:API query for one assignee's runs (GAIA-377 D2).
33
+ *
34
+ * Runs carry no user of their own — attribution is
35
+ * `ticket_id.assignee_user_id.id`, matching StatisticsCounter::SOURCES.
36
+ */
37
+ export function buildUserRunQueryParams(userId, criteria, page = { size: LIST_PAGE_SIZE, index: 0 }) {
38
+ const params = new DrupalJsonApiParams();
39
+ params.addFilter('ticket_id.assignee_user_id.id', userId);
40
+ applyRunCriteria(params, criteria, page);
41
+ return params;
42
+ }
43
+ function applyRunCriteria(params, criteria, page) {
28
44
  if (criteria.closed !== null)
29
45
  params.addFilter('closed', criteria.closed ? '1' : '0');
30
46
  params.addInclude(['ticket_id']);
@@ -37,5 +53,4 @@ export function buildRunQueryParams(projectId, criteria, page = { size: LIST_PAG
37
53
  // Page one carries no offset, so its query is the shortest it can be.
38
54
  if (page.index > 0)
39
55
  params.addPageOffset(page.index * page.size);
40
- return params;
41
56
  }
@@ -219,6 +219,8 @@ export type RunPage = Page<RunRow>;
219
219
  * `links.next`.
220
220
  */
221
221
  export declare function loadProjectRuns(client: DropshClient, projectId: string, criteria: RunCriteria, page?: number): Promise<RunPage>;
222
+ /** One page of runs attributed to a user via ticket assignee (GAIA-377 D2). */
223
+ export declare function loadUserRuns(client: DropshClient, userId: string, criteria: RunCriteria, page?: number): Promise<RunPage>;
222
224
  /**
223
225
  * Newest-STARTED first, and TOTAL (GAIA-255 D5).
224
226
  *
@@ -7,7 +7,7 @@
7
7
  // no-request read of the ticket document (spec D7).
8
8
  import { loadPage, } from '../../collection.js';
9
9
  import { LIST_PAGE_SIZE } from '../../cycle.js';
10
- import { buildRunQueryParams } from './query.js';
10
+ import { buildRunQueryParams, buildUserRunQueryParams, } from './query.js';
11
11
  import { deriveRunStatus, runStartedAge } from './status.js';
12
12
  /** Read a value as a string, or `undefined` when it is not one. */
13
13
  function str(value) {
@@ -217,6 +217,24 @@ export async function loadProjectRuns(client, projectId, criteria, page = 0) {
217
217
  return data.map((item) => mapRunData(item, ticketsById));
218
218
  }, page, window);
219
219
  }
220
+ /** One page of runs attributed to a user via ticket assignee (GAIA-377 D2). */
221
+ export async function loadUserRuns(client, userId, criteria, page = 0) {
222
+ const window = {
223
+ offset: page * LIST_PAGE_SIZE,
224
+ limit: LIST_PAGE_SIZE,
225
+ };
226
+ const params = buildUserRunQueryParams(userId, criteria, {
227
+ size: window.limit,
228
+ index: page,
229
+ });
230
+ return loadPage(client, 'gaia_run/gaia_run', params, (data, included) => {
231
+ const ticketsById = new Map();
232
+ for (const inc of included)
233
+ if (inc.id)
234
+ ticketsById.set(inc.id, inc);
235
+ return data.map((item) => mapRunData(item, ticketsById));
236
+ }, page, window);
237
+ }
220
238
  /**
221
239
  * Newest-STARTED first, and TOTAL (GAIA-255 D5).
222
240
  *
@@ -1,10 +1,17 @@
1
- import { BaseScreen, type ScreenContext } from '../../../Screen/screen.js';
1
+ import type { VNode } from '@opentui/core';
2
+ import { AsyncScreen } from '../../../Screen/async-screen.js';
3
+ import type { FullScreenChrome } from '../../../Screen/full-screen.js';
4
+ import type { ScreenContext } from '../../../Screen/screen.js';
2
5
  import type { TicketDetailDocument } from '../../../types.js';
3
6
  export interface RunDetailInput {
4
7
  readonly document: TicketDetailDocument;
5
8
  }
6
9
  export declare const runDetailInputContext: import("../../../Routing/context.js").ContextKey<RunDetailInput>;
7
- export declare class RunDetailScreen extends BaseScreen {
10
+ /**
11
+ * Run detail — normal {@link AsyncScreen}. The document arrives via navigation
12
+ * context (or empty); refresh is the shared R / ui-refresh path.
13
+ */
14
+ export declare class RunDetailScreen extends AsyncScreen<TicketDetailDocument> {
8
15
  readonly frameId = "run";
9
16
  topTab(): null;
10
17
  static readonly route: {
@@ -17,20 +24,13 @@ export declare class RunDetailScreen extends BaseScreen {
17
24
  context: (_params: Readonly<Record<string, string>>, state: unknown, base: import("../../../Routing/context.js").ContextBag) => import("../../../Routing/context.js").ContextBag;
18
25
  };
19
26
  readonly id: string;
20
- private document;
21
- enter(context: ScreenContext): void;
22
- /**
23
- * GAIA-340 D1 — the first screen on the renderable side of the boundary.
24
- *
25
- * It hands over NODES: the component returns the block as a tree and this
26
- * returns it with the three chrome labels beside it. There is no
27
- * `render(context): string[]` any more, because there is no second
28
- * description of the screen to keep in step with this one — the bold section
29
- * headings that were `boldRows` indices into a row surface are props on the
30
- * rows' own nodes now (D3), and the surface those indices pointed into is
31
- * gone.
32
- */
33
- present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
27
+ reload(context: ScreenContext): void;
28
+ protected load(context: ScreenContext): Promise<TicketDetailDocument>;
29
+ protected renderLoading(): string[];
30
+ protected renderReady(document: TicketDetailDocument, context: ScreenContext): VNode;
31
+ protected chrome(context: ScreenContext): FullScreenChrome;
32
+ /** Read-only — kernel basics cover ↑↓ / Esc / q; no extra chips. */
33
+ protected actions(): never[];
34
34
  /**
35
35
  * F5 — the pipe's own output, and it is `string[]`.
36
36
  *
@@ -41,13 +41,6 @@ export declare class RunDetailScreen extends BaseScreen {
41
41
  * path lays the body out on a cell grid of a fixed width, and a pipe has no
42
42
  * width — on run #4928 it clipped 420 transcript rows at 80 columns, losing
43
43
  * 97.7 % of a 1.34 MB captured agent transcript.
44
- *
45
- * This is NOT a `ScreenPresentation.lines` coming back (R3): `present()` still
46
- * hands over a tree and nothing else. Both shapes are projections of the block's
47
- * ONE keyed traversal (`flatRows`), so there is no second description of the
48
- * screen to keep in step — see `tests/Screen/run-detail-non-tty.test.ts`, which
49
- * asserts the two row for row.
50
44
  */
51
45
  renderNonInteractive(context: ScreenContext): string[];
52
- reset(): void;
53
46
  }
@@ -1,8 +1,12 @@
1
1
  import { defineContext } from '../../../Routing/context.js';
2
- import { BaseScreen, presentation, } from '../../../Screen/screen.js';
2
+ import { AsyncScreen } from '../../../Screen/async-screen.js';
3
3
  import { runDetailRows, runDetailTitle, runDetailTree, } from '../Component/run-detail/index.js';
4
4
  export const runDetailInputContext = defineContext('run-detail-input', 'input');
5
- export class RunDetailScreen extends BaseScreen {
5
+ /**
6
+ * Run detail — normal {@link AsyncScreen}. The document arrives via navigation
7
+ * context (or empty); refresh is the shared R / ui-refresh path.
8
+ */
9
+ export class RunDetailScreen extends AsyncScreen {
6
10
  frameId = 'run';
7
11
  topTab() {
8
12
  return null;
@@ -19,30 +23,28 @@ export class RunDetailScreen extends BaseScreen {
19
23
  : base,
20
24
  };
21
25
  id = RunDetailScreen.route.id;
22
- document = {};
23
- enter(context) {
26
+ reload(context) {
27
+ this.retry(context);
28
+ }
29
+ load(context) {
24
30
  if (context.contexts?.has(runDetailInputContext))
25
- this.document = context.contexts.get(runDetailInputContext).document;
26
- context.requestDraw();
31
+ return Promise.resolve(context.contexts.get(runDetailInputContext).document);
32
+ return Promise.resolve({});
27
33
  }
28
- /**
29
- * GAIA-340 D1 — the first screen on the renderable side of the boundary.
30
- *
31
- * It hands over NODES: the component returns the block as a tree and this
32
- * returns it with the three chrome labels beside it. There is no
33
- * `render(context): string[]` any more, because there is no second
34
- * description of the screen to keep in step with this one — the bold section
35
- * headings that were `boldRows` indices into a row surface are props on the
36
- * rows' own nodes now (D3), and the surface those indices pointed into is
37
- * gone.
38
- */
39
- present(context) {
40
- const runId = context.params?.runId ?? '';
41
- return presentation(runDetailTree(this.document, runId), {
42
- title: runDetailTitle(this.document, runId),
43
- meta: '↑↓ scroll · Esc back · q quit',
44
- status: ' ↑↓ scroll · Esc back · q quit ',
45
- });
34
+ renderLoading() {
35
+ return ['Loading run…'];
36
+ }
37
+ renderReady(document, context) {
38
+ return runDetailTree(document, context.params?.runId ?? '');
39
+ }
40
+ chrome(context) {
41
+ return {
42
+ title: runDetailTitle(this.readyValue ?? {}, context.params?.runId ?? ''),
43
+ };
44
+ }
45
+ /** Read-only — kernel basics cover ↑↓ / Esc / q; no extra chips. */
46
+ actions() {
47
+ return [];
46
48
  }
47
49
  /**
48
50
  * F5 — the pipe's own output, and it is `string[]`.
@@ -54,17 +56,8 @@ export class RunDetailScreen extends BaseScreen {
54
56
  * path lays the body out on a cell grid of a fixed width, and a pipe has no
55
57
  * width — on run #4928 it clipped 420 transcript rows at 80 columns, losing
56
58
  * 97.7 % of a 1.34 MB captured agent transcript.
57
- *
58
- * This is NOT a `ScreenPresentation.lines` coming back (R3): `present()` still
59
- * hands over a tree and nothing else. Both shapes are projections of the block's
60
- * ONE keyed traversal (`flatRows`), so there is no second description of the
61
- * screen to keep in step — see `tests/Screen/run-detail-non-tty.test.ts`, which
62
- * asserts the two row for row.
63
59
  */
64
60
  renderNonInteractive(context) {
65
- return runDetailRows(this.document, context.params?.runId ?? '');
66
- }
67
- reset() {
68
- this.document = {};
61
+ return runDetailRows(this.readyValue ?? {}, context.params?.runId ?? '');
69
62
  }
70
63
  }
@@ -1,9 +1,10 @@
1
+ import type { FullScreenChrome, ListFilterBadge } from '../../../Screen/full-screen.js';
1
2
  import { ListScreen } from '../../../Screen/list-screen.js';
2
- import { type ScreenContext, type ScreenPresentation } from '../../../Screen/screen.js';
3
+ import type { ScreenContext } from '../../../Screen/screen.js';
3
4
  import { type RunCriteria } from '../Data/query.js';
4
5
  import { type RunPage, type RunRow } from '../Data/runs.js';
5
6
  export declare class RunsScreen extends ListScreen<RunRow, number> {
6
- readonly frameId = "project-runs";
7
+ readonly frameId = "runs";
7
8
  readonly topTabOverlay = true;
8
9
  protected readonly table: {
9
10
  readonly noun: "run";
@@ -78,7 +79,10 @@ export declare class RunsScreen extends ListScreen<RunRow, number> {
78
79
  * clients/test doubles, as on the ticket list. */
79
80
  protected filterRow(row: RunRow): boolean;
80
81
  protected renderLoading(): string[];
81
- present(context: ScreenContext): ScreenPresentation;
82
+ /** Runs expose only the closed cycle — one badge, always shown (GAIA-375 A′). */
83
+ protected badges(): readonly ListFilterBadge[];
84
+ protected chrome(context: ScreenContext): FullScreenChrome;
85
+ protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
82
86
  activate(index: number, context: ScreenContext): boolean;
83
87
  select(index: number, context: ScreenContext): boolean;
84
88
  handleInput(input: string, context: ScreenContext): boolean;
@@ -1,10 +1,10 @@
1
+ import { actionItem } from '../../../Component/footer-actions/index.js';
1
2
  import { ListScreen } from '../../../Screen/list-screen.js';
2
- import { presentation, } from '../../../Screen/screen.js';
3
3
  import { CLOSED_FILTER_VALUES, cycleValue } from '../../cycle.js';
4
4
  import { DEFAULT_RUN_CRITERIA } from '../Data/query.js';
5
5
  import { loadProjectRuns, RUN_TABLE, selectedRunTicketId, } from '../Data/runs.js';
6
6
  export class RunsScreen extends ListScreen {
7
- frameId = 'project-runs';
7
+ frameId = 'runs';
8
8
  topTabOverlay = true;
9
9
  table = RUN_TABLE;
10
10
  topTab() {
@@ -63,55 +63,38 @@ export class RunsScreen extends ListScreen {
63
63
  renderLoading() {
64
64
  return ['Loading runs…'];
65
65
  }
66
- present(context) {
67
- const rows = this.rows;
68
- // Standing segment: stated whenever rows are hidden, including the very
69
- // first render (AC-4) — unconditional, as on the ticket list.
70
- const closedSegment = this.criteria.closed === null
71
- ? ''
72
- : this.criteria.closed
73
- ? 'closed: only'
74
- : 'closed: hidden';
75
- // The PAGE number, and only that; the collection's size is a different
76
- // quantity, stated in its own segment by `countLabel` off the `meta.count`
77
- // the page response carries (D6e). A page that offers a next page while
78
- // showing few rows is correct rather than a bug: `links.next` is the
79
- // end-of-collection signal, and a page can be full without being the last.
80
- // The whole segment describes the LOADED page, not the requested index —
81
- // that is what the `page` the value carries is for (D4); same reasoning,
82
- // same shape and the same bottom-line home as the ticket list, which must
83
- // not drift from this one. It is the same `loadedPage` the ARROW keys act
84
- // on, so the number the operator reads and the number the keys move from
85
- // are one value (R1).
86
- //
87
- // Built by `ListScreen` since GAIA-294, which is what makes "must not drift
88
- // from the ticket list" a mechanism rather than a promise.
89
- const paging = this.pagingSegments();
90
- // The rows ON SCREEN and the size of the FILTERED COLLECTION, side by side:
91
- // `7 of 1340 run(s)` when the server reported the total, `7 of ? run(s)`
92
- // when it reported none (GAIA-292 D6c/D6d). Neither quantity replaces the
93
- // other, so the row count never jumps; the total is read off the value the
94
- // rows came with (D4/D4a) and from no second screen member — which is now
95
- // structural rather than careful, because the server answered both from one
96
- // query. Empty while the page loads (D6c), which is why the segments are
97
- // joined rather than concatenated — an omitted count must not leave the line
98
- // opening with a separator.
99
- const count = this.countLabel();
100
- return presentation(this.surface(context), {
101
- selectable: rows.length,
66
+ /** Runs expose only the closed cycle — one badge, always shown (GAIA-375 A′). */
67
+ badges() {
68
+ if (this.criteria.closed === null) {
69
+ return [{ id: 'closed', label: 'x closed' }];
70
+ }
71
+ return [
72
+ {
73
+ id: 'closed',
74
+ label: 'x closed',
75
+ value: this.criteria.closed ? 'closed: only' : 'closed: hidden',
76
+ },
77
+ ];
78
+ }
79
+ chrome(context) {
80
+ // Count + page in header meta; ←/→ in footer actions (GAIA-375 / GAIA-292).
81
+ const pagingLine = [this.countLabel(), ...this.pagingSegments()]
82
+ .filter(Boolean)
83
+ .join(' · ');
84
+ return {
85
+ selectable: this.rows.length,
102
86
  selectedIndex: this.selectedIndex,
103
- // The node a click on each row lands on (GAIA-340 Task 11).
104
87
  rowIds: this.rowNodeIds,
105
88
  title: `${context.project?.name ?? context.project?.slug ?? ''} · runs`,
106
- meta: [
107
- count,
108
- closedSegment,
109
- 'Enter ticket · Tab tabs · x closed · Esc back',
110
- ]
111
- .filter(Boolean)
112
- .join(' · '),
113
- status: ` ${paging.join(' · ')} · ↑↓ select · Enter ticket · Tab tabs · x closed · Esc back · q quit `,
114
- });
89
+ meta: pagingLine,
90
+ };
91
+ }
92
+ actions() {
93
+ return [
94
+ actionItem('runs-enter', 'Enter', 'ticket'),
95
+ actionItem('runs-tab', 'Tab', 'tabs'),
96
+ ...this.pagingActionItems(),
97
+ ];
115
98
  }
116
99
  activate(index, context) {
117
100
  const id = this.readyValue
@@ -0,0 +1,79 @@
1
+ import type { FullScreenChrome, ListFilterBadge } from '../../../Screen/full-screen.js';
2
+ import { ListScreen } from '../../../Screen/list-screen.js';
3
+ import type { ScreenContext } from '../../../Screen/screen.js';
4
+ import { type RunPage, type RunRow } from '../Data/runs.js';
5
+ /** Runs attributed to one user via ticket assignee (GAIA-377 D2). */
6
+ export declare class UserRunsScreen extends ListScreen<RunRow, number> {
7
+ readonly frameId = "user-runs";
8
+ readonly topTabOverlay = true;
9
+ protected readonly table: {
10
+ readonly noun: "run";
11
+ readonly columns: readonly [{
12
+ readonly key: "run";
13
+ readonly label: "RUN";
14
+ readonly width: 8;
15
+ readonly fields: readonly ["drupal_internal__id"];
16
+ readonly get: (r: RunRow) => string;
17
+ }, {
18
+ readonly key: "ticket";
19
+ readonly label: "TICKET";
20
+ readonly width: 10;
21
+ readonly fields: readonly ["ticket_id"];
22
+ readonly get: (r: RunRow) => string;
23
+ }, {
24
+ readonly key: "state";
25
+ readonly label: "STATE";
26
+ readonly width: 8;
27
+ readonly fields: readonly ["state", "heartbeat", "claim_expires_at"];
28
+ readonly get: (r: RunRow, nowSeconds: number) => string;
29
+ }, {
30
+ readonly key: "phase";
31
+ readonly label: "PHASE";
32
+ readonly width: 13;
33
+ readonly fields: readonly ["state_at_start"];
34
+ readonly get: (r: RunRow) => string;
35
+ }, {
36
+ readonly key: "agent";
37
+ readonly label: "AGENT";
38
+ readonly width: 8;
39
+ readonly fields: readonly ["agent"];
40
+ readonly get: (r: RunRow) => string;
41
+ }, {
42
+ readonly key: "started";
43
+ readonly label: "STARTED";
44
+ readonly width: 0;
45
+ readonly grow: 1;
46
+ readonly fields: readonly ["started_at", "created"];
47
+ readonly get: (r: RunRow, nowSeconds: number) => string;
48
+ }];
49
+ readonly emptyMessage: "No runs for this project.";
50
+ };
51
+ topTab(): {
52
+ readonly level: "user";
53
+ readonly tab: "runs";
54
+ };
55
+ static readonly route: {
56
+ id: string;
57
+ path: string;
58
+ fallback: {
59
+ routeId: string;
60
+ };
61
+ tab: {
62
+ id: string;
63
+ label: string;
64
+ scope: string;
65
+ order: number;
66
+ };
67
+ };
68
+ readonly id: string;
69
+ private criteria;
70
+ protected tableContext(context: ScreenContext): number;
71
+ protected loadRows(context: ScreenContext, page: number): Promise<RunPage>;
72
+ protected filterRow(row: RunRow): boolean;
73
+ protected renderLoading(): string[];
74
+ protected badges(): readonly ListFilterBadge[];
75
+ protected chrome(context: ScreenContext): FullScreenChrome;
76
+ protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
77
+ activate(index: number, context: ScreenContext): boolean;
78
+ handleInput(input: string, context: ScreenContext): boolean;
79
+ }
@@ -0,0 +1,98 @@
1
+ import { actionItem } from '../../../Component/footer-actions/index.js';
2
+ import { ListScreen } from '../../../Screen/list-screen.js';
3
+ import { CLOSED_FILTER_VALUES, cycleValue } from '../../cycle.js';
4
+ import { DEFAULT_RUN_CRITERIA } from '../Data/query.js';
5
+ import { loadUserRuns, RUN_TABLE, selectedRunTicketId, } from '../Data/runs.js';
6
+ /** Runs attributed to one user via ticket assignee (GAIA-377 D2). */
7
+ export class UserRunsScreen extends ListScreen {
8
+ frameId = 'user-runs';
9
+ topTabOverlay = true;
10
+ table = RUN_TABLE;
11
+ topTab() {
12
+ return { level: 'user', tab: 'runs' };
13
+ }
14
+ static route = {
15
+ id: 'user.runs',
16
+ path: '/users/:userId/runs',
17
+ fallback: { routeId: 'users' },
18
+ tab: { id: 'runs', label: 'Runs', scope: 'user', order: 30 },
19
+ };
20
+ id = UserRunsScreen.route.id;
21
+ criteria = { ...DEFAULT_RUN_CRITERIA };
22
+ tableContext(context) {
23
+ return context.nowSeconds?.() ?? Math.floor(Date.now() / 1000);
24
+ }
25
+ loadRows(context, page) {
26
+ if (!context.services)
27
+ throw new Error('User runs requires services');
28
+ const userId = context.user?.id ?? context.params?.userId;
29
+ if (!userId)
30
+ throw new Error('User runs requires userId');
31
+ return loadUserRuns(context.services.client, userId, this.criteria, page);
32
+ }
33
+ filterRow(row) {
34
+ return this.criteria.closed === null || row.closed === this.criteria.closed;
35
+ }
36
+ renderLoading() {
37
+ return ['Loading runs…'];
38
+ }
39
+ badges() {
40
+ if (this.criteria.closed === null) {
41
+ return [{ id: 'closed', label: 'x closed' }];
42
+ }
43
+ return [
44
+ {
45
+ id: 'closed',
46
+ label: 'x closed',
47
+ value: this.criteria.closed ? 'closed: only' : 'closed: hidden',
48
+ },
49
+ ];
50
+ }
51
+ chrome(context) {
52
+ const name = context.user?.name ?? context.params?.userName ?? 'user';
53
+ const pagingLine = [this.countLabel(), ...this.pagingSegments()]
54
+ .filter(Boolean)
55
+ .join(' · ');
56
+ return {
57
+ selectable: this.rows.length,
58
+ selectedIndex: this.selectedIndex,
59
+ rowIds: this.rowNodeIds,
60
+ title: `${name} · runs`,
61
+ meta: pagingLine,
62
+ };
63
+ }
64
+ actions() {
65
+ return [
66
+ actionItem('user-runs-enter', 'Enter', 'ticket'),
67
+ actionItem('user-runs-tab', 'Tab', 'tabs'),
68
+ ...this.pagingActionItems(),
69
+ ];
70
+ }
71
+ activate(index, context) {
72
+ const id = this.readyValue
73
+ ? selectedRunTicketId(this.readyValue.rows, index)
74
+ : null;
75
+ if (!id || !context.navigate)
76
+ return false;
77
+ context.navigate('ticket.detail', { ticketId: id, tab: 'runs' });
78
+ return true;
79
+ }
80
+ handleInput(input, context) {
81
+ if (input === 'x') {
82
+ this.criteria = {
83
+ ...this.criteria,
84
+ closed: cycleValue(CLOSED_FILTER_VALUES, this.criteria.closed),
85
+ };
86
+ this.page = 0;
87
+ this.retry(context);
88
+ return true;
89
+ }
90
+ if (this.handlePagingInput(input, context))
91
+ return true;
92
+ if (this.handleCursorInput(input, context))
93
+ return true;
94
+ if (input === 'enter')
95
+ return this.activate(this.selectedIndex, context);
96
+ return super.handleInput(input, context);
97
+ }
98
+ }