@gaia-ai/addon-gaia-ui 0.9.2 → 0.10.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 (119) 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 +65 -6
  10. package/dist/src/Component/top-tabs/index.js +29 -6
  11. package/dist/src/Entity/collection.js +1 -1
  12. package/dist/src/Entity/conductor/Screen/conductors.d.ts +26 -41
  13. package/dist/src/Entity/conductor/Screen/conductors.js +65 -123
  14. package/dist/src/Entity/project/Screen/project-picker.d.ts +20 -31
  15. package/dist/src/Entity/project/Screen/project-picker.js +48 -58
  16. package/dist/src/Entity/project/Screen/projects-screen.d.ts +4 -2
  17. package/dist/src/Entity/project/Screen/projects-screen.js +11 -8
  18. package/dist/src/Entity/run/Component/run-detail/index.js +15 -7
  19. package/dist/src/Entity/run/Data/query.d.ts +10 -0
  20. package/dist/src/Entity/run/Data/query.js +16 -1
  21. package/dist/src/Entity/run/Data/runs.d.ts +2 -0
  22. package/dist/src/Entity/run/Data/runs.js +19 -1
  23. package/dist/src/Entity/run/Screen/run-detail.d.ts +16 -23
  24. package/dist/src/Entity/run/Screen/run-detail.js +27 -34
  25. package/dist/src/Entity/run/Screen/runs.d.ts +7 -3
  26. package/dist/src/Entity/run/Screen/runs.js +31 -48
  27. package/dist/src/Entity/run/Screen/user-runs.d.ts +79 -0
  28. package/dist/src/Entity/run/Screen/user-runs.js +98 -0
  29. package/dist/src/Entity/statistics/Data/statistics.d.ts +31 -6
  30. package/dist/src/Entity/statistics/Data/statistics.js +39 -6
  31. package/dist/src/Entity/statistics/Screen/dashboard.d.ts +10 -2
  32. package/dist/src/Entity/statistics/Screen/dashboard.js +111 -34
  33. package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +14 -1
  34. package/dist/src/Entity/ticket/Component/comment-item/index.js +15 -1
  35. package/dist/src/Entity/ticket/Component/sections/index.js +16 -1
  36. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +102 -13
  37. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +440 -72
  38. package/dist/src/Entity/ticket/Data/detail.d.ts +14 -0
  39. package/dist/src/Entity/ticket/Data/detail.js +15 -1
  40. package/dist/src/Entity/ticket/Data/query.d.ts +7 -0
  41. package/dist/src/Entity/ticket/Data/query.js +18 -2
  42. package/dist/src/Entity/ticket/Data/tickets.d.ts +24 -0
  43. package/dist/src/Entity/ticket/Data/tickets.js +41 -1
  44. package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +10 -4
  45. package/dist/src/Entity/ticket/Screen/create-ticket-form.js +37 -16
  46. package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +6 -4
  47. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +16 -15
  48. package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +6 -4
  49. package/dist/src/Entity/ticket/Screen/parent-filter-form.js +14 -11
  50. package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +5 -2
  51. package/dist/src/Entity/ticket/Screen/ticket-agents.js +14 -14
  52. package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +89 -4
  53. package/dist/src/Entity/ticket/Screen/ticket-detail.js +295 -74
  54. package/dist/src/Entity/ticket/Screen/tickets.d.ts +66 -3
  55. package/dist/src/Entity/ticket/Screen/tickets.js +140 -76
  56. package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +91 -0
  57. package/dist/src/Entity/ticket/Screen/user-tickets.js +94 -0
  58. package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +6 -4
  59. package/dist/src/Entity/ticket/Screen/who-filter-form.js +14 -11
  60. package/dist/src/Entity/user/Data/users.d.ts +49 -0
  61. package/dist/src/Entity/user/Data/users.js +46 -0
  62. package/dist/src/Entity/user/Screen/user-overview.d.ts +33 -0
  63. package/dist/src/Entity/user/Screen/user-overview.js +159 -0
  64. package/dist/src/Entity/user/Screen/users.d.ts +39 -0
  65. package/dist/src/Entity/user/Screen/users.js +136 -0
  66. package/dist/src/Extension/contract.d.ts +58 -0
  67. package/dist/src/Extension/contract.js +1 -0
  68. package/dist/src/Extension/load.d.ts +10 -0
  69. package/dist/src/Extension/load.js +21 -0
  70. package/dist/src/Extension/store.d.ts +11 -0
  71. package/dist/src/Extension/store.js +44 -0
  72. package/dist/src/Kernel/chrome.d.ts +49 -12
  73. package/dist/src/Kernel/chrome.js +150 -57
  74. package/dist/src/Kernel/key-input.js +1 -1
  75. package/dist/src/Kernel/mount.d.ts +1 -1
  76. package/dist/src/Kernel/mount.js +1 -1
  77. package/dist/src/Kernel/regions.d.ts +64 -0
  78. package/dist/src/Kernel/regions.js +328 -0
  79. package/dist/src/Kernel/renderer.d.ts +25 -0
  80. package/dist/src/Kernel/renderer.js +142 -0
  81. package/dist/src/Kernel/splash.d.ts +27 -0
  82. package/dist/src/Kernel/splash.js +72 -0
  83. package/dist/src/Kernel/tui-kernel.js +139 -434
  84. package/dist/src/Routing/context.d.ts +5 -0
  85. package/dist/src/Routing/context.js +6 -0
  86. package/dist/src/Routing/registry.js +4 -2
  87. package/dist/src/Routing/router.js +5 -1
  88. package/dist/src/Routing/routes.js +8 -0
  89. package/dist/src/Screen/async-modal-screen.d.ts +26 -0
  90. package/dist/src/Screen/async-modal-screen.js +44 -0
  91. package/dist/src/Screen/async-screen.d.ts +32 -40
  92. package/dist/src/Screen/async-screen.js +45 -41
  93. package/dist/src/Screen/full-screen.d.ts +1 -0
  94. package/dist/src/Screen/full-screen.js +3 -0
  95. package/dist/src/Screen/list-screen.d.ts +7 -1
  96. package/dist/src/Screen/list-screen.js +17 -6
  97. package/dist/src/Screen/modal-screen.d.ts +6 -0
  98. package/dist/src/Screen/modal-screen.js +2 -0
  99. package/dist/src/Screen/screen.d.ts +42 -6
  100. package/dist/src/Screen/screen.js +2 -2
  101. package/dist/src/Widget/textarea.js +10 -14
  102. package/dist/src/Widget/types.d.ts +1 -1
  103. package/dist/src/launcher.d.ts +6 -0
  104. package/dist/src/launcher.js +2 -0
  105. package/dist/src/lib/format.js +1 -1
  106. package/dist/src/lib/markdown.d.ts +53 -0
  107. package/dist/src/lib/markdown.js +522 -0
  108. package/dist/src/lib/refresh-signal.d.ts +56 -0
  109. package/dist/src/lib/refresh-signal.js +85 -0
  110. package/dist/src/lib/theme.d.ts +36 -0
  111. package/dist/src/lib/theme.js +40 -0
  112. package/dist/src/lib/version.d.ts +5 -0
  113. package/dist/src/lib/version.js +46 -0
  114. package/dist/src/lib/vnode.d.ts +86 -63
  115. package/dist/src/lib/vnode.js +112 -85
  116. package/dist/src/types.d.ts +32 -0
  117. package/dist/src/ui.d.ts +2 -0
  118. package/dist/src/ui.js +1 -0
  119. package/package.json +4 -3
@@ -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
+ }
@@ -36,10 +36,22 @@ export interface StatisticsRow {
36
36
  * the blocks the document carries — an empty array would render as an empty
37
37
  * `PER PROJECT` heading.
38
38
  */
39
+ /**
40
+ * One UTC calendar day in a user-scoped `by_day` series (GAIA-377).
41
+ *
42
+ * Present only under `?user_id=`; the user overview draws the last seven days
43
+ * as weekday rows. `label` is the English short weekday the server computed.
44
+ */
45
+ export interface StatisticsDayRow {
46
+ date: string;
47
+ label: string;
48
+ count: number;
49
+ }
39
50
  export interface StatisticsBlock {
40
51
  total: StatisticsWindows;
41
52
  byProject?: StatisticsRow[];
42
53
  byUser?: StatisticsRow[];
54
+ byDay?: StatisticsDayRow[];
43
55
  }
44
56
  /** The whole document, as the Dashboard consumes it. */
45
57
  export interface Statistics {
@@ -68,20 +80,33 @@ export type StatisticsResult = {
68
80
  };
69
81
  /** How an `id: null` breakdown row is named on screen (spec D8/D11). */
70
82
  export declare const UNATTRIBUTED_LABEL = "\u2014 unattributed";
83
+ /**
84
+ * Closed tickets per run for each trailing window (GAIA-377 D4).
85
+ *
86
+ * Derived on the client from totals that already travel together — a
87
+ * server-computed copy would be a second source of truth that can disagree.
88
+ * `null` when the window has zero runs: never `0`, never `Infinity`.
89
+ */
90
+ export declare function closedPerRun(tickets: StatisticsWindows, runs: StatisticsWindows): {
91
+ day: number | null;
92
+ week: number | null;
93
+ month: number | null;
94
+ };
95
+ /** Format a closed-per-run value for the screen: two decimals, or `—`. */
96
+ export declare const formatClosedPerRun: (value: number | null) => string;
71
97
  /**
72
98
  * Read the control plane's ticket and run statistics — the ONE source of every
73
99
  * figure the Dashboard renders (AC-8).
74
100
  *
75
101
  * With `projectId` the read is scoped and the server drops `by_project` from
76
- * both blocks (spec D5). The parameter name is `project_id` and NOT `project`:
77
- * JSON:API requires an implementation-specific query parameter to contain at
78
- * least one non-`a-z` character, and Drupal enforces it — `?project=` is
79
- * rejected with a 400 by `JsonApiRequestValidator` before the resource is ever
80
- * reached, so it cannot even be accepted as an alias.
102
+ * both blocks (spec D5). With `userId` (GAIA-377 D3) it drops `by_user` and
103
+ * keeps `by_project`. The parameter names are `project_id` / `user_id` and NOT
104
+ * bare letters: JSON:API requires an implementation-specific query parameter to
105
+ * contain at least one non-`a-z` character, and Drupal enforces it.
81
106
  *
82
107
  * The client is the caller's, singular and used once: the TUI reads everything
83
108
  * through `services.client` (the `session` profile), and `pm` cannot read
84
109
  * `gaia_run` — a loader that reached for another client would render zero runs
85
110
  * as a real measurement (spec R3).
86
111
  */
87
- export declare function loadStatistics(client: DropshClient, projectId?: string): Promise<StatisticsResult>;
112
+ export declare function loadStatistics(client: DropshClient, projectId?: string, userId?: string): Promise<StatisticsResult>;
@@ -25,14 +25,28 @@ function rowsOf(value) {
25
25
  };
26
26
  });
27
27
  }
28
+ function dayRowsOf(value) {
29
+ if (!Array.isArray(value))
30
+ return undefined;
31
+ return value.map((entry) => {
32
+ const r = (entry ?? {});
33
+ return {
34
+ date: typeof r.date === 'string' ? r.date : '',
35
+ label: typeof r.label === 'string' ? r.label : '',
36
+ count: num(r.count),
37
+ };
38
+ });
39
+ }
28
40
  function blockOf(value) {
29
41
  const b = (value ?? {});
30
42
  const byProject = rowsOf(b.by_project);
31
43
  const byUser = rowsOf(b.by_user);
44
+ const byDay = dayRowsOf(b.by_day);
32
45
  return {
33
46
  total: windowsOf(b.total),
34
47
  ...(byProject ? { byProject } : {}),
35
48
  ...(byUser ? { byUser } : {}),
49
+ ...(byDay ? { byDay } : {}),
36
50
  };
37
51
  }
38
52
  /** The HTTP status of an unknown throw — dropsh's `HttpError` carries one. */
@@ -86,27 +100,46 @@ function jsonApiErrorDetail(caught) {
86
100
  // fall back to a non-empty marker rather than to `undefined`.
87
101
  return typeof detail === 'string' && detail !== '' ? detail : 'HTTP 404';
88
102
  }
103
+ /**
104
+ * Closed tickets per run for each trailing window (GAIA-377 D4).
105
+ *
106
+ * Derived on the client from totals that already travel together — a
107
+ * server-computed copy would be a second source of truth that can disagree.
108
+ * `null` when the window has zero runs: never `0`, never `Infinity`.
109
+ */
110
+ export function closedPerRun(tickets, runs) {
111
+ const ratio = (t, r) => (r > 0 ? t / r : null);
112
+ return {
113
+ day: ratio(tickets.day, runs.day),
114
+ week: ratio(tickets.week, runs.week),
115
+ month: ratio(tickets.month, runs.month),
116
+ };
117
+ }
118
+ /** Format a closed-per-run value for the screen: two decimals, or `—`. */
119
+ export const formatClosedPerRun = (value) => value === null ? '—' : value.toFixed(2);
89
120
  /**
90
121
  * Read the control plane's ticket and run statistics — the ONE source of every
91
122
  * figure the Dashboard renders (AC-8).
92
123
  *
93
124
  * With `projectId` the read is scoped and the server drops `by_project` from
94
- * both blocks (spec D5). The parameter name is `project_id` and NOT `project`:
95
- * JSON:API requires an implementation-specific query parameter to contain at
96
- * least one non-`a-z` character, and Drupal enforces it — `?project=` is
97
- * rejected with a 400 by `JsonApiRequestValidator` before the resource is ever
98
- * reached, so it cannot even be accepted as an alias.
125
+ * both blocks (spec D5). With `userId` (GAIA-377 D3) it drops `by_user` and
126
+ * keeps `by_project`. The parameter names are `project_id` / `user_id` and NOT
127
+ * bare letters: JSON:API requires an implementation-specific query parameter to
128
+ * contain at least one non-`a-z` character, and Drupal enforces it.
99
129
  *
100
130
  * The client is the caller's, singular and used once: the TUI reads everything
101
131
  * through `services.client` (the `session` profile), and `pm` cannot read
102
132
  * `gaia_run` — a loader that reached for another client would render zero runs
103
133
  * as a real measurement (spec R3).
104
134
  */
105
- export async function loadStatistics(client, projectId) {
135
+ export async function loadStatistics(client, projectId, userId) {
106
136
  const params = new DrupalJsonApiParams();
107
137
  if (projectId !== undefined) {
108
138
  params.addCustomParam({ project_id: projectId });
109
139
  }
140
+ if (userId !== undefined) {
141
+ params.addCustomParam({ user_id: userId });
142
+ }
110
143
  let doc;
111
144
  try {
112
145
  doc = (await client.get('gaia/statistics', params));