@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
@@ -24,5 +24,19 @@ import type { DropshClient, TicketDetailDocument } from '../../../types.js';
24
24
  * this include list must stay a superset of the list read's — dropping either
25
25
  * `parent_id` or `labels` degrades a rendered row to a raw uuid without failing a
26
26
  * fixture-driven test.
27
+ *
28
+ * GAIA-369 D5 added `blocked_by` for the Related tab's Blocked by rows, and the
29
+ * measurement is the reason it can be identities at all: live against the develop
30
+ * control plane under `session`, the `blocked_by` LINKAGE data is in the document
31
+ * with no include at all (`{"data": []}` on GAIA-369), and `include=blocked_by`
32
+ * sideloads `gaia_ticket--gaia_ticket` resources carrying `identifier` (measured
33
+ * on GAIA-23, GAIA-90 and GAIA-92 → GAIA-19, GAIA-97, GAIA-90). So the section
34
+ * renders rows without a second request and without a second read profile.
35
+ *
36
+ * That split is also the failure mode to keep in mind: because the linkage
37
+ * arrives regardless, dropping this include does NOT empty the section — it
38
+ * degrades every row to a raw uuid, silently. Hence the include set is pinned as
39
+ * a string in `tests/Entity/ticket/Data/detail.test.ts` rather than inferred from
40
+ * a rendered section, and this list must stay a superset of the list read's.
27
41
  */
28
42
  export declare function loadTicketDetail(client: DropshClient, ticketId: string): Promise<TicketDetailDocument>;
@@ -23,7 +23,21 @@
23
23
  * this include list must stay a superset of the list read's — dropping either
24
24
  * `parent_id` or `labels` degrades a rendered row to a raw uuid without failing a
25
25
  * fixture-driven test.
26
+ *
27
+ * GAIA-369 D5 added `blocked_by` for the Related tab's Blocked by rows, and the
28
+ * measurement is the reason it can be identities at all: live against the develop
29
+ * control plane under `session`, the `blocked_by` LINKAGE data is in the document
30
+ * with no include at all (`{"data": []}` on GAIA-369), and `include=blocked_by`
31
+ * sideloads `gaia_ticket--gaia_ticket` resources carrying `identifier` (measured
32
+ * on GAIA-23, GAIA-90 and GAIA-92 → GAIA-19, GAIA-97, GAIA-90). So the section
33
+ * renders rows without a second request and without a second read profile.
34
+ *
35
+ * That split is also the failure mode to keep in mind: because the linkage
36
+ * arrives regardless, dropping this include does NOT empty the section — it
37
+ * degrades every row to a raw uuid, silently. Hence the include set is pinned as
38
+ * a string in `tests/Entity/ticket/Data/detail.test.ts` rather than inferred from
39
+ * a rendered section, and this list must stay a superset of the list read's.
26
40
  */
27
41
  export function loadTicketDetail(client, ticketId) {
28
- return client.get(`gaia_ticket/gaia_ticket/${ticketId}?include=comments,sub_tickets,runs,referenced_tickets,conductor_id,assignee_user_id,parent_id,labels`);
42
+ return client.get(`gaia_ticket/gaia_ticket/${ticketId}?include=comments,sub_tickets,runs,referenced_tickets,conductor_id,assignee_user_id,parent_id,labels,blocked_by`);
29
43
  }
@@ -55,3 +55,10 @@ export interface TicketQueryOptions {
55
55
  includes?: boolean;
56
56
  }
57
57
  export declare function buildTicketQueryParams(projectId: string, criteria: TicketCriteria, options?: TicketQueryOptions): DrupalJsonApiParams;
58
+ /**
59
+ * Build the JSON:API query for one assignee's tickets (GAIA-377 D2).
60
+ *
61
+ * Same paging, fieldset and closed-filter cycle as the project builder; the
62
+ * scope is `assignee_user_id.id` instead of `project_id.id`.
63
+ */
64
+ export declare function buildUserTicketQueryParams(userId: string, criteria: TicketCriteria, options?: TicketQueryOptions): DrupalJsonApiParams;
@@ -61,6 +61,22 @@ export function describeCriteria(criteria) {
61
61
  export function buildTicketQueryParams(projectId, criteria, options = {}) {
62
62
  const params = new DrupalJsonApiParams();
63
63
  params.addFilter('project_id.id', projectId);
64
+ applyTicketCriteria(params, criteria, options);
65
+ return params;
66
+ }
67
+ /**
68
+ * Build the JSON:API query for one assignee's tickets (GAIA-377 D2).
69
+ *
70
+ * Same paging, fieldset and closed-filter cycle as the project builder; the
71
+ * scope is `assignee_user_id.id` instead of `project_id.id`.
72
+ */
73
+ export function buildUserTicketQueryParams(userId, criteria, options = {}) {
74
+ const params = new DrupalJsonApiParams();
75
+ params.addFilter('assignee_user_id.id', userId);
76
+ applyTicketCriteria(params, criteria, options);
77
+ return params;
78
+ }
79
+ function applyTicketCriteria(params, criteria, options) {
64
80
  if (criteria.query) {
65
81
  params.addGroup('search', 'OR');
66
82
  params.addFilter('identifier', criteria.query, 'CONTAINS', 'search');
@@ -77,7 +93,8 @@ export function buildTicketQueryParams(projectId, criteria, options = {}) {
77
93
  if (criteria.parent)
78
94
  params.addFilter('parent_id.id', criteria.parent.id);
79
95
  // Assigned to this user only; a top-level AND like parent, never part of
80
- // the search OR group (GAIA-299 AC-4).
96
+ // the search OR group (GAIA-299 AC-4). Skipped when the builder itself is
97
+ // already scoped to an assignee (user ticket list).
81
98
  if (criteria.who)
82
99
  params.addFilter('assignee_user_id.id', criteria.who.id);
83
100
  const { size, index } = options.page ?? { size: PAGE_SIZE, index: 0 };
@@ -96,5 +113,4 @@ export function buildTicketQueryParams(projectId, criteria, options = {}) {
96
113
  // Page one carries no offset, so its query is the shortest it can be.
97
114
  if (index > 0)
98
115
  params.addPageOffset(index * size);
99
- return params;
100
116
  }
@@ -204,8 +204,32 @@ export declare function mapTicketDocument(document: {
204
204
  * {@link Page}, whose four members and their meaning are documented there
205
205
  * (GAIA-294). */
206
206
  export type TicketPage = Page<TicketRow>;
207
+ /**
208
+ * How many tickets the criteria match once the `closed` criterion is DROPPED —
209
+ * GAIA-370.
210
+ *
211
+ * The size of the COLLECTION, never of a page: it is the server's own
212
+ * `meta.count` (GAIA-305), read off a one-row response, so an answer of 285 costs
213
+ * the same single read as an answer of 1 and cannot be a recount of whatever the
214
+ * screen happens to be holding. `null` when the server reports no count, which
215
+ * `readTotal` already treats as the explicit unknown rather than as a zero.
216
+ *
217
+ * It builds the SAME query as {@link loadFilteredTickets} — the same OR group over
218
+ * identifier/title, the same project/state/parent/who ANDs — with exactly one
219
+ * criterion removed. That is what makes the number it returns an answer to "how
220
+ * many is `closed` holding back" and not to some other question; a second,
221
+ * hand-written query here could drift from the one the rows came from, and then
222
+ * the count beside `No tickets.` would describe a different collection than the
223
+ * emptiness it explains.
224
+ *
225
+ * `includes: false` and `page[limit]=1`: no row is mapped, so no sideload is worth
226
+ * requesting.
227
+ */
228
+ export declare function countTicketsIgnoringClosed(client: DropshClient, projectId: string, criteria: TicketCriteria): Promise<number | null>;
207
229
  /** Server-side filtered/searched ticket list for a project — ONE page of it. The
208
230
  * query sideloads `parent_id` + `conductor_id`; each row's parent/conductor is
209
231
  * resolved from the document's `included` (AC-5). */
210
232
  export declare function loadFilteredTickets(client: DropshClient, projectId: string, criteria: TicketCriteria, page?: number): Promise<TicketPage>;
233
+ /** One page of tickets assigned to a user (GAIA-377 D2). */
234
+ export declare function loadUserTickets(client: DropshClient, userId: string, criteria: TicketCriteria, page?: number): Promise<TicketPage>;
211
235
  export {};
@@ -5,7 +5,7 @@
5
5
  import { asString } from '../../../lib/format.js';
6
6
  import { loadPage, } from '../../collection.js';
7
7
  import { LIST_PAGE_SIZE } from '../../cycle.js';
8
- import { buildTicketQueryParams } from './query.js';
8
+ import { buildTicketQueryParams, buildUserTicketQueryParams, } from './query.js';
9
9
  /** Build the {@link IncludedIndex} from a document's `included` array. */
10
10
  function indexIncluded(included) {
11
11
  const index = new Map();
@@ -247,6 +247,32 @@ export function mapTicketData(item, included) {
247
247
  export function mapTicketDocument(document) {
248
248
  return mapTicketData(document.data ?? {}, indexIncluded(document.included));
249
249
  }
250
+ /**
251
+ * How many tickets the criteria match once the `closed` criterion is DROPPED —
252
+ * GAIA-370.
253
+ *
254
+ * The size of the COLLECTION, never of a page: it is the server's own
255
+ * `meta.count` (GAIA-305), read off a one-row response, so an answer of 285 costs
256
+ * the same single read as an answer of 1 and cannot be a recount of whatever the
257
+ * screen happens to be holding. `null` when the server reports no count, which
258
+ * `readTotal` already treats as the explicit unknown rather than as a zero.
259
+ *
260
+ * It builds the SAME query as {@link loadFilteredTickets} — the same OR group over
261
+ * identifier/title, the same project/state/parent/who ANDs — with exactly one
262
+ * criterion removed. That is what makes the number it returns an answer to "how
263
+ * many is `closed` holding back" and not to some other question; a second,
264
+ * hand-written query here could drift from the one the rows came from, and then
265
+ * the count beside `No tickets.` would describe a different collection than the
266
+ * emptiness it explains.
267
+ *
268
+ * `includes: false` and `page[limit]=1`: no row is mapped, so no sideload is worth
269
+ * requesting.
270
+ */
271
+ export async function countTicketsIgnoringClosed(client, projectId, criteria) {
272
+ const params = buildTicketQueryParams(projectId, { ...criteria, closed: null }, { page: { size: 1, index: 0 }, includes: false });
273
+ const page = await loadPage(client, 'gaia_ticket/gaia_ticket', params, () => [], 0, { offset: 0, limit: 1 });
274
+ return page.total;
275
+ }
250
276
  /** Server-side filtered/searched ticket list for a project — ONE page of it. The
251
277
  * query sideloads `parent_id` + `conductor_id`; each row's parent/conductor is
252
278
  * resolved from the document's `included` (AC-5). */
@@ -266,3 +292,17 @@ export async function loadFilteredTickets(client, projectId, criteria, page = 0)
266
292
  return data.map((item) => mapTicketData(item, index));
267
293
  }, page, window);
268
294
  }
295
+ /** One page of tickets assigned to a user (GAIA-377 D2). */
296
+ export async function loadUserTickets(client, userId, criteria, page = 0) {
297
+ const window = {
298
+ offset: page * LIST_PAGE_SIZE,
299
+ limit: LIST_PAGE_SIZE,
300
+ };
301
+ const params = buildUserTicketQueryParams(userId, criteria, {
302
+ page: { size: window.limit, index: page },
303
+ });
304
+ return loadPage(client, 'gaia_ticket/gaia_ticket', params, (data, included) => {
305
+ const index = indexIncluded(included);
306
+ return data.map((item) => mapTicketData(item, index));
307
+ }, page, window);
308
+ }
@@ -1,8 +1,9 @@
1
1
  import type { VNode } from '@opentui/core';
2
- import { AsyncScreen } from '../../../Screen/async-screen.js';
3
- import { type ScreenAction, type ScreenContext } from '../../../Screen/screen.js';
2
+ import { AsyncModalScreen } from '../../../Screen/async-modal-screen.js';
3
+ import type { ModalChrome } from '../../../Screen/modal-screen.js';
4
+ import type { ScreenAction, ScreenContext } from '../../../Screen/screen.js';
4
5
  import { type CreateFormOptions } from '../Form/create.js';
5
- export declare class CreateTicketFormScreen extends AsyncScreen<CreateFormOptions> {
6
+ export declare class CreateTicketFormScreen extends AsyncModalScreen<CreateFormOptions> {
6
7
  readonly modal = true;
7
8
  static readonly route: {
8
9
  id: string;
@@ -66,7 +67,12 @@ export declare class CreateTicketFormScreen extends AsyncScreen<CreateFormOption
66
67
  takeAction(): ScreenAction | undefined;
67
68
  handleInput(input: string, context: ScreenContext): boolean;
68
69
  reset(): void;
69
- present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
70
+ protected modalChrome(context: ScreenContext): ModalChrome;
71
+ protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
72
+ /** Two status rows so Ctrl+D is not clipped in the modal box (AC-1). */
73
+ protected modalStatusLines(): string[];
74
+ private fieldActions;
75
+ private submitActions;
70
76
  /**
71
77
  * Write the ticket straight to the control plane (GAIA-309).
72
78
  *
@@ -2,10 +2,10 @@ import { writeFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import { buildCreateLaunch, shortName } from '../../../Agent/launch.js';
5
+ import { actionItem, formatActionLadder, } from '../../../Component/footer-actions/index.js';
5
6
  import { serverMessage } from '../../../lib/jsonapi-error.js';
6
7
  import { columnNode } from '../../../lib/vnode.js';
7
- import { AsyncScreen } from '../../../Screen/async-screen.js';
8
- import { presentation, } from '../../../Screen/screen.js';
8
+ import { AsyncModalScreen } from '../../../Screen/async-modal-screen.js';
9
9
  import { createOptionLoader, } from '../../../Widget/option-source.js';
10
10
  import { CREATE_FORM_REGION, createFormTree, } from '../Component/create-form/index.js';
11
11
  import { buildTicketCreateBody, createTicket } from '../Data/write.js';
@@ -28,10 +28,7 @@ import { keyToIntent } from '../Form/keys.js';
28
28
  // is the only clue the label list is togglable — to buy room for the new keys.
29
29
  // The modal frame splits a body row on `\n` already (`tui-kernel.ts`), so a
30
30
  // second row costs no kernel change; R3 already accepts that the popup grew.
31
- const CHANGE_KEYS = ' ↑↓ field · ←→ change · Space toggle';
32
- const PICK_KEYS = ' ↑↓ field · ←→ suggestion · Enter pick';
33
- const SUBMIT_KEYS = ' Ctrl+S agent · Ctrl+D direct · Esc cancel ';
34
- export class CreateTicketFormScreen extends AsyncScreen {
31
+ export class CreateTicketFormScreen extends AsyncModalScreen {
35
32
  modal = true;
36
33
  static route = {
37
34
  id: 'ticket.create',
@@ -238,21 +235,45 @@ export class CreateTicketFormScreen extends AsyncScreen {
238
235
  this.sources = {};
239
236
  super.reset();
240
237
  }
241
- present(context) {
242
- return presentation(this.surface(context), {
238
+ modalChrome(context) {
239
+ return {
243
240
  title: '⚡ New ticket',
244
241
  meta: context.project?.name ??
245
242
  context.project?.slug ??
246
243
  context.options?.project ??
247
244
  '',
248
- status: this.form && CREATE_FORM.activeField(this.form).kind === 'autocomplete'
249
- ? `${PICK_KEYS}\n${SUBMIT_KEYS}`
250
- : `${CHANGE_KEYS}\n${SUBMIT_KEYS}`,
251
- // The popup's box supplies its own border and padding, so the body it
252
- // holds must not also carry the body window's inset — see
253
- // `presentation()`.
254
- modal: true,
255
- });
245
+ };
246
+ }
247
+ actions() {
248
+ return [...this.fieldActions(), ...this.submitActions()];
249
+ }
250
+ /** Two status rows so Ctrl+D is not clipped in the modal box (AC-1). */
251
+ modalStatusLines() {
252
+ return [
253
+ formatActionLadder(this.fieldActions()),
254
+ formatActionLadder(this.submitActions()),
255
+ ];
256
+ }
257
+ fieldActions() {
258
+ return this.form &&
259
+ CREATE_FORM.activeField(this.form).kind === 'autocomplete'
260
+ ? [
261
+ actionItem('create-field', '↑↓', 'field'),
262
+ actionItem('create-suggest', '←→', 'suggestion'),
263
+ actionItem('create-pick', 'Enter', 'pick'),
264
+ ]
265
+ : [
266
+ actionItem('create-field', '↑↓', 'field'),
267
+ actionItem('create-change', '←→', 'change'),
268
+ actionItem('create-toggle', 'Space', 'toggle'),
269
+ ];
270
+ }
271
+ submitActions() {
272
+ return [
273
+ actionItem('create-agent', 'Ctrl+S', 'agent'),
274
+ actionItem('create-direct', 'Ctrl+D', 'direct'),
275
+ actionItem('create-esc', 'Esc', 'cancel'),
276
+ ];
256
277
  }
257
278
  /**
258
279
  * Write the ticket straight to the control plane (GAIA-309).
@@ -1,6 +1,7 @@
1
1
  import type { VNode } from '@opentui/core';
2
- import { AsyncScreen } from '../../../Screen/async-screen.js';
3
- import { type ScreenAction, type ScreenContext } from '../../../Screen/screen.js';
2
+ import { AsyncModalScreen } from '../../../Screen/async-modal-screen.js';
3
+ import type { ModalChrome } from '../../../Screen/modal-screen.js';
4
+ import type { ScreenAction, ScreenContext } from '../../../Screen/screen.js';
4
5
  import type { TicketDetailDocument } from '../../../types.js';
5
6
  import { type EditFormOptions } from '../Form/edit.js';
6
7
  interface EditAssignmentData {
@@ -11,7 +12,7 @@ interface EditAssignmentInput {
11
12
  readonly document: TicketDetailDocument;
12
13
  }
13
14
  export declare const editAssignmentInputContext: import("../../../Routing/context.js").ContextKey<EditAssignmentInput>;
14
- export declare class EditAssignmentFormScreen extends AsyncScreen<EditAssignmentData> {
15
+ export declare class EditAssignmentFormScreen extends AsyncModalScreen<EditAssignmentData> {
15
16
  static result(ticketId: string, assigneeId: string): {
16
17
  type: "ticket-assignment-updated";
17
18
  ticketId: string;
@@ -77,6 +78,7 @@ export declare class EditAssignmentFormScreen extends AsyncScreen<EditAssignment
77
78
  */
78
79
  leave(): void;
79
80
  reset(): void;
80
- present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
81
+ protected modalChrome(context: ScreenContext): ModalChrome;
82
+ protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
81
83
  }
82
84
  export {};
@@ -1,8 +1,8 @@
1
+ import { actionItem } from '../../../Component/footer-actions/index.js';
1
2
  import { serverMessage } from '../../../lib/jsonapi-error.js';
2
3
  import { columnNode } from '../../../lib/vnode.js';
3
4
  import { defineContext } from '../../../Routing/context.js';
4
- import { AsyncScreen } from '../../../Screen/async-screen.js';
5
- import { presentation, } from '../../../Screen/screen.js';
5
+ import { AsyncModalScreen } from '../../../Screen/async-modal-screen.js';
6
6
  import { createOptionLoader, } from '../../../Widget/option-source.js';
7
7
  import { activeRunInfo } from '../../run/Data/active-run.js';
8
8
  import { EDIT_FORM_REGION, editFormTree, } from '../Component/edit-form/index.js';
@@ -18,7 +18,7 @@ const relationshipId = (document, name) => {
18
18
  return undefined;
19
19
  return Array.isArray(data) ? data[0]?.id : data.id;
20
20
  };
21
- export class EditAssignmentFormScreen extends AsyncScreen {
21
+ export class EditAssignmentFormScreen extends AsyncModalScreen {
22
22
  static result(ticketId, assigneeId) {
23
23
  return { type: 'ticket-assignment-updated', ticketId, assigneeId };
24
24
  }
@@ -251,21 +251,22 @@ export class EditAssignmentFormScreen extends AsyncScreen {
251
251
  this.context = undefined;
252
252
  super.reset();
253
253
  }
254
- present(context) {
255
- return presentation(this.surface(context), {
256
- // GAIA-308: the popup is no longer only about assignment — it carries the
257
- // estimate and the spent time too, so the chrome stops naming one half.
254
+ modalChrome(context) {
255
+ // GAIA-308: chrome names the ticket, not only assignment.
256
+ return {
258
257
  title: '✎ Edit ticket',
259
258
  meta: String(this.readyValue?.document.data?.attributes?.identifier ??
260
259
  context.params?.ticketId ??
261
260
  ''),
262
- // GAIA-360 D12 — Esc closes the popup; Cancel ticket is the lifecycle
263
- // action. Status must not say "Esc cancel".
264
- status: ' ↑↓ field · ←→ change · Ctrl+S save · Esc close ',
265
- // The popup's box supplies its own border and padding, so the body it
266
- // holds must not also carry the body window's inset — see
267
- // `presentation()`.
268
- modal: true,
269
- });
261
+ };
262
+ }
263
+ actions() {
264
+ // GAIA-360 D12 — Esc:close (not cancel); Cancel ticket is lifecycle.
265
+ return [
266
+ actionItem('edit-field', '↑↓', 'field'),
267
+ actionItem('edit-change', '←→', 'change'),
268
+ actionItem('edit-save', 'Ctrl+S', 'save'),
269
+ actionItem('edit-esc', 'Esc', 'close'),
270
+ ];
270
271
  }
271
272
  }
@@ -1,8 +1,9 @@
1
1
  import type { VNode } from '@opentui/core';
2
- import { AsyncScreen } from '../../../Screen/async-screen.js';
3
- import { type ScreenAction, type ScreenContext } from '../../../Screen/screen.js';
2
+ import { AsyncModalScreen } from '../../../Screen/async-modal-screen.js';
3
+ import type { ModalChrome } from '../../../Screen/modal-screen.js';
4
+ import type { ScreenAction, ScreenContext } from '../../../Screen/screen.js';
4
5
  import { type ParentFilterOptions } from '../Form/parent-filter.js';
5
- export declare class ParentFilterScreen extends AsyncScreen<ParentFilterOptions> {
6
+ export declare class ParentFilterScreen extends AsyncModalScreen<ParentFilterOptions> {
6
7
  readonly modal = true;
7
8
  static readonly route: {
8
9
  id: string;
@@ -40,5 +41,6 @@ export declare class ParentFilterScreen extends AsyncScreen<ParentFilterOptions>
40
41
  takeAction(): ScreenAction | undefined;
41
42
  handleInput(input: string, context: ScreenContext): boolean;
42
43
  reset(): void;
43
- present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
44
+ protected modalChrome(context: ScreenContext): ModalChrome;
45
+ protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
44
46
  }
@@ -9,15 +9,15 @@
9
9
  // loads through, never a second lookup. The source builds its query with
10
10
  // `parent: null` criteria (F7), so an active parent filter can never narrow
11
11
  // the picker to that parent's own children.
12
+ import { actionItem } from '../../../Component/footer-actions/index.js';
12
13
  import { columnNode } from '../../../lib/vnode.js';
13
- import { AsyncScreen } from '../../../Screen/async-screen.js';
14
- import { presentation, } from '../../../Screen/screen.js';
14
+ import { AsyncModalScreen } from '../../../Screen/async-modal-screen.js';
15
15
  import { createOptionLoader, primeSources, } from '../../../Widget/option-source.js';
16
16
  import { FIELDS_REGION, renderDeclaredFieldsTree, } from '../../../Widget/render.js';
17
17
  import { keyToIntent } from '../Form/keys.js';
18
18
  import { PARENT_FILTER_FORM, } from '../Form/parent-filter.js';
19
19
  import { parentOptionSource } from '../Form/parent-source.js';
20
- export class ParentFilterScreen extends AsyncScreen {
20
+ export class ParentFilterScreen extends AsyncModalScreen {
21
21
  modal = true;
22
22
  static route = {
23
23
  id: 'project.tickets.parent-filter',
@@ -186,15 +186,18 @@ export class ParentFilterScreen extends AsyncScreen {
186
186
  this.sources = {};
187
187
  super.reset();
188
188
  }
189
- present(context) {
190
- return presentation(this.surface(context), {
189
+ modalChrome(context) {
190
+ return {
191
191
  title: '⚡ Filter by parent',
192
192
  meta: context.project?.name ?? context.project?.slug ?? '',
193
- status: ' ↑↓/←→ suggestion · Enter pick · Del clear · Esc cancel ',
194
- // The popup's box supplies its own border and padding, so the body it
195
- // holds must not also carry the body window's inset — see
196
- // `presentation()`.
197
- modal: true,
198
- });
193
+ };
194
+ }
195
+ actions() {
196
+ return [
197
+ actionItem('parent-suggest', '↑↓/←→', 'suggestion'),
198
+ actionItem('parent-pick', 'Enter', 'pick'),
199
+ actionItem('parent-clear', 'Del', 'clear'),
200
+ actionItem('parent-esc', 'Esc', 'cancel'),
201
+ ];
199
202
  }
200
203
  }
@@ -1,6 +1,7 @@
1
1
  import type { VNode } from '@opentui/core';
2
2
  import { AsyncScreen } from '../../../Screen/async-screen.js';
3
- import { type ScreenContext } from '../../../Screen/screen.js';
3
+ import type { FullScreenChrome } from '../../../Screen/full-screen.js';
4
+ import type { ScreenContext } from '../../../Screen/screen.js';
4
5
  import type { HostedAgent } from '../../../types.js';
5
6
  /**
6
7
  * The ticket agents list, declared once (GAIA-345 D2/D10).
@@ -127,7 +128,9 @@ export declare class TicketAgentsScreen extends AsyncScreen<HostedAgent[]> {
127
128
  protected load(context: ScreenContext): Promise<HostedAgent[]>;
128
129
  protected renderLoading(): string[];
129
130
  protected renderReady(rows: HostedAgent[]): VNode;
130
- present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
131
+ protected chrome(_context: ScreenContext): FullScreenChrome;
132
+ protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
133
+ protected notice(): string | undefined;
131
134
  focus(index: number, context: ScreenContext): boolean;
132
135
  activate(index: number, context: ScreenContext): boolean;
133
136
  select(index: number, context: ScreenContext): boolean;
@@ -18,9 +18,9 @@
18
18
  // `Agent/Data/` for it to sit in. `PICKER_TABLE` is the precedent for exactly
19
19
  // that case, and it sits in `Entity/project/Screen/project-picker.ts` for exactly
20
20
  // this reason.
21
+ import { actionItem } from '../../../Component/footer-actions/index.js';
21
22
  import { ListCursor } from '../../../lib/cursor.js';
22
23
  import { AsyncScreen } from '../../../Screen/async-screen.js';
23
- import { presentation } from '../../../Screen/screen.js';
24
24
  import { listRowIds, listTableTree } from '../../collection.js';
25
25
  /**
26
26
  * The ticket agents list, declared once (GAIA-345 D2/D10).
@@ -179,24 +179,24 @@ export class TicketAgentsScreen extends AsyncScreen {
179
179
  selectedIndex: this.selectedIndex,
180
180
  });
181
181
  }
182
- present(context) {
182
+ chrome(_context) {
183
183
  const rows = this.rows;
184
- return presentation(this.surface(context), {
184
+ return {
185
185
  selectable: rows.length,
186
186
  selectedIndex: this.selectedIndex,
187
- // Row 0 is the table's own header, so the selected AGENT is one row below
188
- // its index. Still the row the kernel scrolls into view and nothing more:
189
- // the highlight is a prop on that row's cells now (GAIA-340 D3).
190
- // The node a click on each row lands on (GAIA-340 Task 11), read off the
191
- // SAME configuration that keys the rows (GAIA-345 D2) — which retires the
192
- // hand-written `agentRowIds`, a second expression of an id the component
193
- // spelled independently.
194
187
  rowIds: listRowIds(AGENT_TABLE, rows),
195
188
  title: 'Ticket-Agents',
196
- meta: `${rows.length} agent(s) · Enter switch · z overview · r retry · x close · Esc back`,
197
- status: this.status ||
198
- ' ↑↓ select · Enter switch · z overview · r retry · x close · Tab tabs · Esc back · q quit ',
199
- });
189
+ meta: `${rows.length} agent(s)`,
190
+ };
191
+ }
192
+ actions() {
193
+ return [
194
+ actionItem('agents-enter', 'Enter', 'switch'),
195
+ actionItem('agents-tab', 'Tab', 'tabs'),
196
+ ];
197
+ }
198
+ notice() {
199
+ return this.status || undefined;
200
200
  }
201
201
  focus(index, context) {
202
202
  const row = this.readyValue?.[index];