@gaia-ai/addon-gaia-ui 0.7.0 → 0.9.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 (181) hide show
  1. package/dist/src/Agent/launch.d.ts +4 -4
  2. package/dist/src/Agent/launch.js +14 -7
  3. package/dist/src/Component/section-list/index.d.ts +117 -8
  4. package/dist/src/Component/section-list/index.js +118 -46
  5. package/dist/src/Component/stats-panel/index.d.ts +17 -5
  6. package/dist/src/Component/stats-panel/index.js +50 -52
  7. package/dist/src/Component/top-tabs/index.d.ts +95 -23
  8. package/dist/src/Component/top-tabs/index.js +87 -41
  9. package/dist/src/Entity/collection.d.ts +279 -0
  10. package/dist/src/Entity/collection.js +377 -0
  11. package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +25 -0
  12. package/dist/src/Entity/conductor/Component/conductor-list/index.js +60 -0
  13. package/dist/src/Entity/conductor/Data/conductors.d.ts +216 -0
  14. package/dist/src/Entity/conductor/Data/conductors.js +280 -0
  15. package/dist/src/Entity/conductor/Data/liveness.d.ts +2 -1
  16. package/dist/src/Entity/conductor/Data/liveness.js +12 -16
  17. package/dist/src/Entity/conductor/Data/log-tail.d.ts +25 -0
  18. package/dist/src/Entity/conductor/Data/log-tail.js +133 -0
  19. package/dist/src/Entity/conductor/Data/own-conductor.d.ts +30 -0
  20. package/dist/src/Entity/conductor/Data/own-conductor.js +73 -0
  21. package/dist/src/Entity/conductor/Data/process.d.ts +78 -0
  22. package/dist/src/Entity/conductor/Data/process.js +101 -0
  23. package/dist/src/Entity/conductor/Screen/conductors.d.ts +67 -0
  24. package/dist/src/Entity/conductor/Screen/conductors.js +233 -0
  25. package/dist/src/Entity/cycle.d.ts +18 -0
  26. package/dist/src/Entity/cycle.js +30 -0
  27. package/dist/src/Entity/project/Data/project.d.ts +8 -0
  28. package/dist/src/Entity/project/Data/project.js +12 -0
  29. package/dist/src/Entity/project/Data/projects.d.ts +121 -0
  30. package/dist/src/Entity/project/Data/projects.js +115 -0
  31. package/dist/src/Entity/project/Data/with-tickets.d.ts +5 -0
  32. package/dist/src/Entity/project/Data/with-tickets.js +15 -13
  33. package/dist/src/Entity/project/Screen/project-picker.d.ts +80 -0
  34. package/dist/src/Entity/project/Screen/project-picker.js +191 -0
  35. package/dist/src/Entity/project/Screen/projects-screen.d.ts +52 -0
  36. package/dist/src/Entity/project/Screen/projects-screen.js +122 -0
  37. package/dist/src/Entity/run/Component/run-detail/index.d.ts +26 -10
  38. package/dist/src/Entity/run/Component/run-detail/index.js +147 -39
  39. package/dist/src/Entity/run/Component/run-table/index.d.ts +5 -7
  40. package/dist/src/Entity/run/Component/run-table/index.js +9 -18
  41. package/dist/src/Entity/run/Data/query.d.ts +22 -0
  42. package/dist/src/Entity/run/Data/query.js +41 -0
  43. package/dist/src/Entity/run/Data/runs.d.ts +192 -4
  44. package/dist/src/Entity/run/Data/runs.js +188 -27
  45. package/dist/src/Entity/run/Screen/run-detail.d.ts +53 -0
  46. package/dist/src/Entity/run/Screen/run-detail.js +70 -0
  47. package/dist/src/Entity/run/Screen/runs.d.ts +86 -0
  48. package/dist/src/Entity/run/Screen/runs.js +169 -0
  49. package/dist/src/Entity/statistics/Screen/dashboard.d.ts +78 -0
  50. package/dist/src/Entity/statistics/Screen/dashboard.js +228 -0
  51. package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +12 -9
  52. package/dist/src/Entity/ticket/Component/comment-item/index.js +18 -13
  53. package/dist/src/Entity/ticket/Component/create-form/index.d.ts +16 -3
  54. package/dist/src/Entity/ticket/Component/create-form/index.js +28 -14
  55. package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +11 -6
  56. package/dist/src/Entity/ticket/Component/edit-form/index.js +77 -29
  57. package/dist/src/Entity/ticket/Component/sections/index.d.ts +8 -0
  58. package/dist/src/Entity/ticket/Component/sections/index.js +42 -0
  59. package/dist/src/Entity/ticket/Component/tab-bar/index.js +6 -0
  60. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +65 -8
  61. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +171 -84
  62. package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +4 -2
  63. package/dist/src/Entity/ticket/Component/ticket-table/index.js +8 -12
  64. package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +47 -3
  65. package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -22
  66. package/dist/src/Entity/ticket/Data/parents.d.ts +13 -1
  67. package/dist/src/Entity/ticket/Data/parents.js +64 -12
  68. package/dist/src/Entity/ticket/Data/query.d.ts +44 -7
  69. package/dist/src/Entity/ticket/Data/query.js +60 -26
  70. package/dist/src/Entity/ticket/Data/tickets.d.ts +170 -4
  71. package/dist/src/Entity/ticket/Data/tickets.js +200 -12
  72. package/dist/src/Entity/ticket/Data/url.d.ts +16 -0
  73. package/dist/src/Entity/ticket/Data/url.js +20 -0
  74. package/dist/src/Entity/ticket/Data/workflow.d.ts +1 -5
  75. package/dist/src/Entity/ticket/Data/workflow.js +5 -13
  76. package/dist/src/Entity/ticket/Data/write.d.ts +32 -0
  77. package/dist/src/Entity/ticket/Data/write.js +80 -0
  78. package/dist/src/Entity/ticket/Form/create-data.d.ts +1 -11
  79. package/dist/src/Entity/ticket/Form/create-data.js +7 -36
  80. package/dist/src/Entity/ticket/Form/create.d.ts +45 -5
  81. package/dist/src/Entity/ticket/Form/create.js +45 -8
  82. package/dist/src/Entity/ticket/Form/edit-data.d.ts +1 -1
  83. package/dist/src/Entity/ticket/Form/edit-data.js +4 -0
  84. package/dist/src/Entity/ticket/Form/edit.d.ts +79 -12
  85. package/dist/src/Entity/ticket/Form/edit.js +165 -18
  86. package/dist/src/Entity/ticket/Form/keys.d.ts +7 -0
  87. package/dist/src/Entity/ticket/Form/keys.js +7 -0
  88. package/dist/src/Entity/ticket/Form/parent-filter.d.ts +36 -0
  89. package/dist/src/Entity/ticket/Form/parent-filter.js +37 -0
  90. package/dist/src/Entity/ticket/Form/parent-source.d.ts +27 -0
  91. package/dist/src/Entity/ticket/Form/parent-source.js +56 -0
  92. package/dist/src/Entity/ticket/Form/who-filter.d.ts +33 -0
  93. package/dist/src/Entity/ticket/Form/who-filter.js +33 -0
  94. package/dist/src/Entity/ticket/Form/who-source.d.ts +18 -0
  95. package/dist/src/Entity/ticket/Form/who-source.js +38 -0
  96. package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +81 -0
  97. package/dist/src/Entity/ticket/Screen/create-ticket-form.js +356 -0
  98. package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +92 -0
  99. package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +220 -0
  100. package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +44 -0
  101. package/dist/src/Entity/ticket/Screen/parent-filter-form.js +200 -0
  102. package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +136 -0
  103. package/dist/src/Entity/ticket/Screen/ticket-agents.js +257 -0
  104. package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +125 -0
  105. package/dist/src/Entity/ticket/Screen/ticket-detail.js +415 -0
  106. package/dist/src/Entity/ticket/Screen/tickets.d.ts +145 -0
  107. package/dist/src/Entity/ticket/Screen/tickets.js +538 -0
  108. package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +41 -0
  109. package/dist/src/Entity/ticket/Screen/who-filter-form.js +174 -0
  110. package/dist/src/Entity/user/Data/users.d.ts +17 -0
  111. package/dist/src/Entity/user/Data/users.js +66 -6
  112. package/dist/src/Form/field.d.ts +3 -1
  113. package/dist/src/Form/field.js +1 -0
  114. package/dist/src/Form/form.d.ts +1 -1
  115. package/dist/src/Form/form.js +61 -9
  116. package/dist/src/Form/keys.d.ts +23 -1
  117. package/dist/src/Form/keys.js +78 -23
  118. package/dist/src/Kernel/chrome.d.ts +289 -0
  119. package/dist/src/Kernel/chrome.js +404 -0
  120. package/dist/src/Kernel/clipboard.d.ts +30 -0
  121. package/dist/src/Kernel/clipboard.js +51 -0
  122. package/dist/src/Kernel/key-input.d.ts +50 -0
  123. package/dist/src/Kernel/key-input.js +158 -0
  124. package/dist/src/Kernel/mount.d.ts +187 -0
  125. package/dist/src/Kernel/mount.js +399 -0
  126. package/dist/src/Kernel/tui-kernel.d.ts +0 -11
  127. package/dist/src/Kernel/tui-kernel.js +778 -2367
  128. package/dist/src/Routing/context.d.ts +31 -0
  129. package/dist/src/Routing/context.js +52 -0
  130. package/dist/src/Routing/registry.d.ts +19 -0
  131. package/dist/src/Routing/registry.js +97 -0
  132. package/dist/src/Routing/route.d.ts +35 -0
  133. package/dist/src/Routing/route.js +1 -0
  134. package/dist/src/Routing/router.d.ts +49 -0
  135. package/dist/src/Routing/router.js +123 -0
  136. package/dist/src/Routing/routes.d.ts +17 -0
  137. package/dist/src/Routing/routes.js +57 -0
  138. package/dist/src/Screen/async-screen.d.ts +87 -0
  139. package/dist/src/Screen/async-screen.js +206 -0
  140. package/dist/src/Screen/list-screen.d.ts +193 -0
  141. package/dist/src/Screen/list-screen.js +261 -0
  142. package/dist/src/Screen/screen.d.ts +240 -0
  143. package/dist/src/Screen/screen.js +34 -0
  144. package/dist/src/Tui/controller.d.ts +13 -0
  145. package/dist/src/Tui/controller.js +1794 -0
  146. package/dist/src/Widget/autocomplete.js +75 -3
  147. package/dist/src/Widget/index.d.ts +3 -2
  148. package/dist/src/Widget/index.js +2 -1
  149. package/dist/src/Widget/option-source.d.ts +12 -1
  150. package/dist/src/Widget/option-source.js +14 -1
  151. package/dist/src/Widget/render.d.ts +23 -8
  152. package/dist/src/Widget/render.js +108 -41
  153. package/dist/src/Widget/text.js +94 -5
  154. package/dist/src/Widget/textarea.d.ts +45 -0
  155. package/dist/src/Widget/textarea.js +115 -0
  156. package/dist/src/Widget/types.d.ts +64 -1
  157. package/dist/src/launcher.d.ts +9 -2
  158. package/dist/src/launcher.js +2 -1
  159. package/dist/src/lib/ansi.js +13 -5
  160. package/dist/src/lib/clipboard.d.ts +41 -0
  161. package/dist/src/lib/clipboard.js +65 -0
  162. package/dist/src/lib/cursor.d.ts +31 -0
  163. package/dist/src/lib/cursor.js +45 -0
  164. package/dist/src/lib/format.d.ts +21 -13
  165. package/dist/src/lib/format.js +76 -28
  166. package/dist/src/lib/hyperlink.d.ts +58 -0
  167. package/dist/src/lib/hyperlink.js +104 -0
  168. package/dist/src/lib/input-trace.d.ts +48 -0
  169. package/dist/src/lib/input-trace.js +47 -0
  170. package/dist/src/lib/key-vocabulary.d.ts +19 -0
  171. package/dist/src/lib/key-vocabulary.js +25 -0
  172. package/dist/src/lib/table.d.ts +37 -0
  173. package/dist/src/lib/table.js +20 -0
  174. package/dist/src/lib/vnode.d.ts +217 -0
  175. package/dist/src/lib/vnode.js +456 -0
  176. package/dist/src/lib/width.d.ts +25 -1
  177. package/dist/src/lib/width.js +93 -12
  178. package/dist/src/plugin.js +26 -1
  179. package/dist/src/types.d.ts +67 -2
  180. package/dist/src/types.js +0 -4
  181. package/package.json +4 -2
@@ -0,0 +1,80 @@
1
+ import { BaseScreen, type ScreenContext } from '../../../Screen/screen.js';
2
+ /**
3
+ * A project that claims the repo.
4
+ *
5
+ * `id` is what every node of its row is keyed on, which is what lets the
6
+ * picker's rows go through the shared table at all: `ListTable` reads row
7
+ * identity as `rowNodeId(noun, row.id)` for every list.
8
+ */
9
+ export interface PickerCandidate {
10
+ id: string;
11
+ name: string;
12
+ }
13
+ export interface ProjectPickerInput {
14
+ readonly candidates: readonly PickerCandidate[];
15
+ readonly repo: string;
16
+ }
17
+ export declare const projectPickerInputContext: import("../../../Routing/context.js").ContextKey<ProjectPickerInput>;
18
+ export declare class ProjectPickerScreen extends BaseScreen {
19
+ readonly frameId = "project-picker";
20
+ topTab(): {
21
+ readonly level: "root";
22
+ readonly tab: "projects";
23
+ };
24
+ static readonly route: {
25
+ id: string;
26
+ path: string;
27
+ parent: string;
28
+ fallback: {
29
+ routeId: string;
30
+ };
31
+ context: (_params: Readonly<Record<string, string>>, state: unknown, base: import("../../../Routing/context.js").ContextBag) => import("../../../Routing/context.js").ContextBag;
32
+ };
33
+ readonly id: string;
34
+ private candidates;
35
+ private repo;
36
+ selectedIndex: number;
37
+ enter(context: ScreenContext): void;
38
+ get rows(): readonly PickerCandidate[];
39
+ /**
40
+ * The question this screen asks, and the one place it is written.
41
+ *
42
+ * Both surfaces open with it — the tree as a keyed row above the table, the
43
+ * pipe as its first line — so composing it twice is how the two would come to
44
+ * disagree about the repo they name.
45
+ */
46
+ private headingText;
47
+ /** The candidate the cursor stands on, clamped: an out-of-range index must
48
+ * move the marker, never lose it. */
49
+ private selected;
50
+ /**
51
+ * F5 — the pipe's own output, and it stays `string[]`.
52
+ *
53
+ * GAIA-345 D7/D8 — built from the SAME configuration the tree is, through
54
+ * `listTableRows` and `lineText`, plus the two heading lines and the trailing
55
+ * hint, which are the screen's and not rows of the list. The bytes are
56
+ * unchanged by construction rather than by a comparison somebody has to
57
+ * remember to make, and `tests/Entity/project/Screen/project-picker.test.ts`
58
+ * pins them line for line against the pre-conversion capture.
59
+ */
60
+ renderNonInteractive(): string[];
61
+ /**
62
+ * The picker's body: the question, a blank, then the list (GAIA-345 D8).
63
+ *
64
+ * The two heading rows are composed AROUND the table's node because they are
65
+ * not rows of it — they name no candidate, they are not selectable, and they
66
+ * carry no entry in `rowIds`. That is also what the old `selRow` offset of 2
67
+ * was compensating for before the kernel started finding the cursor by node.
68
+ *
69
+ * MOUNTED WHOLE — no window of its own, and the bound is named rather than
70
+ * assumed: a row is one `gaia_project` that claims the cwd's remote, which is
71
+ * the multisite topology GAIA-126/137 designs for. That is a handful of sites
72
+ * per repo, against `@opentui/core@0.5.1`'s ceiling of 16 382 renderables.
73
+ */
74
+ private surface;
75
+ present(_context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
76
+ handleInput(input: string, context: ScreenContext): boolean;
77
+ activate(index: number, context: ScreenContext): boolean;
78
+ select(index: number, context: ScreenContext): boolean;
79
+ reset(): void;
80
+ }
@@ -0,0 +1,191 @@
1
+ // GAIA-223 AC-8 (spec Decision 14) — the picker `gaia ui --here` shows when the
2
+ // cwd's repo is claimed by MORE THAN ONE gaia project.
3
+ //
4
+ // One repo mapping to several projects is designed for, not an edge case:
5
+ // GAIA-126/137 let `.gaia/` hold one conductor config per project, which is how
6
+ // a Drupal multisite repo maps each site to its own GAIA project. Erroring on
7
+ // N>1 would fail permanently, by design, in exactly the repos the feature is
8
+ // most useful in — so the ambiguity is shown rather than resolved behind the
9
+ // user's back.
10
+ //
11
+ // GAIA-345 D8 — `Component/project-picker/` is GONE and its two survivors live
12
+ // here, in their only consumer: the candidate type and the one-column table the
13
+ // two surfaces are both made of.
14
+ import { lineText, } from '../../../Component/section-list/index.js';
15
+ import { columnNode, textNode } from '../../../lib/vnode.js';
16
+ import { defineContext } from '../../../Routing/context.js';
17
+ import { BaseScreen, presentation, } from '../../../Screen/screen.js';
18
+ import { listRowIds, listTableRows, listTableTree, } from '../../collection.js';
19
+ /**
20
+ * The picker as a configuration (GAIA-345 D8) — one column, and NO HEADER.
21
+ *
22
+ * The header is declared away, and that is an operator decision taken against
23
+ * the direction that every other list gets one (D6): this is a question with
24
+ * choices rather than a table of records, and its single column has an empty
25
+ * label — so a header would paint an empty bold bar above the question.
26
+ *
27
+ * The `▸` stays in the cell's TEXT rather than becoming an attribute. It is
28
+ * what says "selected" to the pipe, which has no attributes to carry a
29
+ * highlight; the tree carries BOTH, the marker in the text and the inverse on
30
+ * the node, exactly as it did before this ticket.
31
+ *
32
+ * `Ctx` is the selected CANDIDATE rather than its index, because the column
33
+ * receives the row and not its position — the same reason row identity is the
34
+ * entity's uuid. The screen resolves it once per frame, clamped.
35
+ */
36
+ const PICKER_TABLE = {
37
+ noun: 'project-picker',
38
+ header: false,
39
+ emptyMessage: 'No projects.',
40
+ columns: [
41
+ {
42
+ key: 'name',
43
+ label: '',
44
+ width: 0,
45
+ fields: ['name'],
46
+ get: (candidate, selected) => `${candidate === selected ? '▸' : ' '} ${candidate.name}`,
47
+ },
48
+ ],
49
+ };
50
+ export const projectPickerInputContext = defineContext('project-picker-input', 'input');
51
+ /** Clamp a selection index into the candidate range (0 when there are none). */
52
+ const clamp = (selected, length) => {
53
+ if (length <= 0)
54
+ return 0;
55
+ return Math.max(0, Math.min(Math.trunc(selected), length - 1));
56
+ };
57
+ export class ProjectPickerScreen extends BaseScreen {
58
+ frameId = 'project-picker';
59
+ topTab() {
60
+ return { level: 'root', tab: 'projects' };
61
+ }
62
+ static route = {
63
+ id: 'project.picker',
64
+ path: '/projects/pick',
65
+ parent: 'projects',
66
+ fallback: { routeId: 'project.dashboard' },
67
+ context: (_params, state, base) => state
68
+ ? base.set(projectPickerInputContext, state)
69
+ : base,
70
+ };
71
+ id = ProjectPickerScreen.route.id;
72
+ candidates = [];
73
+ repo = '';
74
+ selectedIndex = 0;
75
+ enter(context) {
76
+ const input = context.contexts?.has(projectPickerInputContext)
77
+ ? context.contexts.get(projectPickerInputContext)
78
+ : undefined;
79
+ if (input) {
80
+ this.candidates = [...input.candidates];
81
+ this.repo = input.repo;
82
+ this.selectedIndex = 0;
83
+ }
84
+ context.requestDraw();
85
+ }
86
+ get rows() {
87
+ return this.candidates;
88
+ }
89
+ /**
90
+ * The question this screen asks, and the one place it is written.
91
+ *
92
+ * Both surfaces open with it — the tree as a keyed row above the table, the
93
+ * pipe as its first line — so composing it twice is how the two would come to
94
+ * disagree about the repo they name.
95
+ */
96
+ headingText() {
97
+ return `Several gaia projects claim ${this.repo} — which one?`;
98
+ }
99
+ /** The candidate the cursor stands on, clamped: an out-of-range index must
100
+ * move the marker, never lose it. */
101
+ selected() {
102
+ return this.candidates[clamp(this.selectedIndex, this.candidates.length)];
103
+ }
104
+ /**
105
+ * F5 — the pipe's own output, and it stays `string[]`.
106
+ *
107
+ * GAIA-345 D7/D8 — built from the SAME configuration the tree is, through
108
+ * `listTableRows` and `lineText`, plus the two heading lines and the trailing
109
+ * hint, which are the screen's and not rows of the list. The bytes are
110
+ * unchanged by construction rather than by a comparison somebody has to
111
+ * remember to make, and `tests/Entity/project/Screen/project-picker.test.ts`
112
+ * pins them line for line against the pre-conversion capture.
113
+ */
114
+ renderNonInteractive() {
115
+ return [
116
+ this.headingText(),
117
+ '',
118
+ ...listTableRows(PICKER_TABLE, this.candidates, this.selected()).map((line) => lineText(line)),
119
+ '',
120
+ 'Run gaia ui project:<name> to choose directly.',
121
+ ];
122
+ }
123
+ /**
124
+ * The picker's body: the question, a blank, then the list (GAIA-345 D8).
125
+ *
126
+ * The two heading rows are composed AROUND the table's node because they are
127
+ * not rows of it — they name no candidate, they are not selectable, and they
128
+ * carry no entry in `rowIds`. That is also what the old `selRow` offset of 2
129
+ * was compensating for before the kernel started finding the cursor by node.
130
+ *
131
+ * MOUNTED WHOLE — no window of its own, and the bound is named rather than
132
+ * assumed: a row is one `gaia_project` that claims the cwd's remote, which is
133
+ * the multisite topology GAIA-126/137 designs for. That is a handful of sites
134
+ * per repo, against `@opentui/core@0.5.1`'s ceiling of 16 382 renderables.
135
+ */
136
+ surface() {
137
+ return columnNode('project-picker', textNode({
138
+ id: 'project-picker-heading',
139
+ content: this.headingText(),
140
+ }), textNode({ id: 'project-picker-heading-gap', content: '' }), listTableTree(PICKER_TABLE, this.candidates, this.selected(), {
141
+ selectedIndex: clamp(this.selectedIndex, this.candidates.length),
142
+ }));
143
+ }
144
+ present(_context) {
145
+ return presentation(this.surface(), {
146
+ selectable: this.candidates.length,
147
+ selectedIndex: this.selectedIndex,
148
+ // The node a click on each candidate lands on (GAIA-340 Task 11), read
149
+ // off the SAME configuration that keys the rows — which retires the
150
+ // hand-spelled `project-picker-candidate-<id>` the screen used to publish
151
+ // beside a row keyed the same way by a second expression (GAIA-345 D2).
152
+ rowIds: listRowIds(PICKER_TABLE, this.candidates),
153
+ title: 'Which project?',
154
+ meta: `${this.candidates.length} project(s) claim ${this.repo} · Enter open · Esc dashboard`,
155
+ status: ' ↑↓ select · Enter open · Esc dashboard · q quit ',
156
+ });
157
+ }
158
+ handleInput(input, context) {
159
+ if (input === 'j' || input === 'down')
160
+ this.selectedIndex = Math.min(this.selectedIndex + 1, this.candidates.length - 1);
161
+ else if (input === 'k' || input === 'up')
162
+ this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
163
+ else
164
+ return false;
165
+ context.requestDraw();
166
+ return true;
167
+ }
168
+ activate(index, context) {
169
+ const picked = this.candidates[index];
170
+ if (!picked || !context.navigate)
171
+ return false;
172
+ context.navigate('project.dashboard', {
173
+ projectId: picked.id,
174
+ projectName: picked.name,
175
+ projectSlug: picked.name,
176
+ });
177
+ return true;
178
+ }
179
+ select(index, context) {
180
+ if (index < 0 || index >= this.candidates.length)
181
+ return false;
182
+ this.selectedIndex = index;
183
+ context.requestDraw();
184
+ return true;
185
+ }
186
+ reset() {
187
+ this.candidates = [];
188
+ this.repo = '';
189
+ this.selectedIndex = 0;
190
+ }
191
+ }
@@ -0,0 +1,52 @@
1
+ import type { VNode } from '@opentui/core';
2
+ import { AsyncScreen } from '../../../Screen/async-screen.js';
3
+ import { type ScreenContext } from '../../../Screen/screen.js';
4
+ import { type ProjectRow } from '../Data/projects.js';
5
+ /**
6
+ * GAIA-340 D1 — the ready page is a renderable TREE; `loading` and `error` are
7
+ * still three lines of text and stay rows (see `AsyncScreen`'s `Surface`).
8
+ *
9
+ * MOUNTED WHOLE, so the bound on its nodes is the data's own length against
10
+ * `@opentui/core@0.5.1`'s ceiling of 16 382 renderables. A project row is one
11
+ * box of 3 cells, and the rows are the user's own projects read from a single
12
+ * bounded `gaia_conductor` collection, grouped by project — three orders of
13
+ * magnitude of headroom on any control plane a person works with. This screen
14
+ * therefore windows nothing of its own, deliberately rather than by omission.
15
+ */
16
+ export declare class ProjectsScreen extends AsyncScreen<ProjectRow[]> {
17
+ readonly frameId = "projects";
18
+ topTab(): {
19
+ readonly level: "root";
20
+ readonly tab: "projects";
21
+ };
22
+ reload(context: ScreenContext): void;
23
+ static readonly route: {
24
+ id: string;
25
+ path: string;
26
+ tab: {
27
+ id: string;
28
+ label: string;
29
+ scope: string;
30
+ order: number;
31
+ };
32
+ };
33
+ readonly id: string;
34
+ private readonly cursor;
35
+ /** Kept as a settable PUBLIC member: the kernel reads it and
36
+ * `tests/Screen/project-screens.test.ts` writes it (GAIA-294 D2). */
37
+ get selectedIndex(): number;
38
+ set selectedIndex(value: number);
39
+ saveState(): {
40
+ selectedIndex: number;
41
+ };
42
+ restoreState(token: unknown): void;
43
+ get rows(): readonly ProjectRow[];
44
+ protected load(context: ScreenContext): Promise<ProjectRow[]>;
45
+ protected renderLoading(): string[];
46
+ protected renderReady(rows: ProjectRow[]): VNode;
47
+ present(context: ScreenContext): import("../../../Screen/screen.js").ScreenPresentation;
48
+ activate(index: number, context: ScreenContext): boolean;
49
+ select(index: number, context: ScreenContext): boolean;
50
+ handleInput(input: string, context: ScreenContext): boolean;
51
+ reset(): void;
52
+ }
@@ -0,0 +1,122 @@
1
+ import { ListCursor } from '../../../lib/cursor.js';
2
+ import { AsyncScreen } from '../../../Screen/async-screen.js';
3
+ import { presentation } from '../../../Screen/screen.js';
4
+ import { listRowIds, listTableTree } from '../../collection.js';
5
+ import { loadProjectRows, PROJECT_TABLE, } from '../Data/projects.js';
6
+ /**
7
+ * GAIA-340 D1 — the ready page is a renderable TREE; `loading` and `error` are
8
+ * still three lines of text and stay rows (see `AsyncScreen`'s `Surface`).
9
+ *
10
+ * MOUNTED WHOLE, so the bound on its nodes is the data's own length against
11
+ * `@opentui/core@0.5.1`'s ceiling of 16 382 renderables. A project row is one
12
+ * box of 3 cells, and the rows are the user's own projects read from a single
13
+ * bounded `gaia_conductor` collection, grouped by project — three orders of
14
+ * magnitude of headroom on any control plane a person works with. This screen
15
+ * therefore windows nothing of its own, deliberately rather than by omission.
16
+ */
17
+ export class ProjectsScreen extends AsyncScreen {
18
+ frameId = 'projects';
19
+ topTab() {
20
+ return { level: 'root', tab: 'projects' };
21
+ }
22
+ reload(context) {
23
+ this.retry(context);
24
+ }
25
+ static route = {
26
+ id: 'projects',
27
+ path: '/projects',
28
+ tab: { id: 'projects', label: 'Projects', scope: 'root', order: 20 },
29
+ };
30
+ id = ProjectsScreen.route.id;
31
+ cursor = new ListCursor();
32
+ /** Kept as a settable PUBLIC member: the kernel reads it and
33
+ * `tests/Screen/project-screens.test.ts` writes it (GAIA-294 D2). */
34
+ get selectedIndex() {
35
+ return this.cursor.index;
36
+ }
37
+ set selectedIndex(value) {
38
+ this.cursor.index = value;
39
+ }
40
+ saveState() {
41
+ return { selectedIndex: this.selectedIndex };
42
+ }
43
+ restoreState(token) {
44
+ if (token &&
45
+ typeof token === 'object' &&
46
+ typeof token.selectedIndex === 'number') {
47
+ this.selectedIndex = token.selectedIndex;
48
+ }
49
+ }
50
+ get rows() {
51
+ return this.readyValue ?? [];
52
+ }
53
+ load(context) {
54
+ if (!context.services)
55
+ throw new Error('Projects requires services');
56
+ return loadProjectRows({
57
+ client: context.services.client,
58
+ nowSeconds: context.nowSeconds ?? (() => Math.floor(Date.now() / 1000)),
59
+ });
60
+ }
61
+ renderLoading() {
62
+ return ['Loading projects…'];
63
+ }
64
+ renderReady(rows) {
65
+ // GAIA-345 AC 1 — the one table, configured. There is no `project-list`
66
+ // component any more: the columns, the noun and the empty message are
67
+ // `PROJECT_TABLE`, declared beside the row mapper that produces the rows.
68
+ return listTableTree(PROJECT_TABLE, rows, undefined, {
69
+ selectedIndex: this.selectedIndex,
70
+ });
71
+ }
72
+ present(context) {
73
+ const rows = this.rows;
74
+ return presentation(this.surface(context), {
75
+ selectable: rows.length,
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).
80
+ rowIds: listRowIds(PROJECT_TABLE, rows),
81
+ title: 'GAIA projects',
82
+ meta: `${rows.length} registered project(s) · ${context.services?.baseUrl ?? ''}`,
83
+ status: ' ↑↓ select · Enter open · n new · Tab tabs · q quit ',
84
+ });
85
+ }
86
+ activate(index, context) {
87
+ const row = this.readyValue?.[index];
88
+ if (!row || !context.navigate)
89
+ return false;
90
+ context.navigate('project.dashboard', {
91
+ projectId: row.id,
92
+ projectName: row.projectName,
93
+ projectSlug: row.projectSlug,
94
+ });
95
+ return true;
96
+ }
97
+ select(index, context) {
98
+ if (index < 0 || index >= this.rows.length)
99
+ return false;
100
+ this.selectedIndex = index;
101
+ context.requestDraw();
102
+ return true;
103
+ }
104
+ handleInput(input, context) {
105
+ if (super.handleInput(input, context))
106
+ return true;
107
+ const rows = this.readyValue;
108
+ if (!rows)
109
+ return false;
110
+ if (this.cursor.handle(input, rows.length)) {
111
+ context.requestDraw();
112
+ return true;
113
+ }
114
+ if (input === 'enter')
115
+ return this.activate(this.selectedIndex, context);
116
+ return false;
117
+ }
118
+ reset() {
119
+ this.cursor.reset();
120
+ super.reset();
121
+ }
122
+ }
@@ -1,17 +1,33 @@
1
- import type { Section } from '../../../../Component/section-list/index.js';
1
+ import type { VNode } from '@opentui/core';
2
2
  import type { JsonApiResource, TicketDetailDocument } from '../../../../types.js';
3
3
  /** The sideloaded `gaia_run` with this id, or undefined. */
4
4
  export declare function runResource(document: TicketDetailDocument, runId: string | null | undefined): JsonApiResource | undefined;
5
5
  /** `#<id> · <ticket> · <state>` — the run screen's header-box title row. */
6
6
  export declare function runDetailTitle(document: TicketDetailDocument, runId: string | null | undefined): string;
7
7
  /**
8
- * The run's sections, in the order the screen draws them. An unresolvable run
9
- * yields ONE section saying so, rather than an empty screen that looks like a
10
- * failed render.
8
+ * The run block as the renderable tree the screen hands over (GAIA-340 D1).
9
+ *
10
+ * The FIRST screen on this side of the boundary, and the shape every other one
11
+ * follows: one node per row, each with an id that says what the row is, and the
12
+ * styling as props on the node rather than as indices into a surface the kernel
13
+ * has to interpret. The root is the component's own `run-detail`;
14
+ * `presentation()` puts the kernel's body-window root over it, because the id of
15
+ * that slot is the kernel's to know and a component may not import it.
11
16
  */
12
- export declare function runDetailSections(document: TicketDetailDocument, runId: string | null | undefined): Section[];
13
- /** The run block flattened — the screen's body, and the piped render (D7). */
14
- export declare function runDetailLines(document: TicketDetailDocument, runId: string | null | undefined): {
15
- lines: string[];
16
- boldRows: Set<number>;
17
- };
17
+ export declare function runDetailTree(document: TicketDetailDocument, runId: string | null | undefined): VNode;
18
+ /**
19
+ * The run block as PLAIN TEXT — the pipe's own output (spec F5).
20
+ *
21
+ * The operator's ruling after plan Task 10: a run is handled exactly like every
22
+ * other entity. `ticket.detail`, the dashboard and the project picker each print
23
+ * a deliberately different text into a pipe rather than a rendering of their
24
+ * surface; the run detail was the only entity detail that did not, which is the
25
+ * only reason its rows went through the tree→text path at all. That path renders
26
+ * a CELL GRID of a fixed width, and a pipe has no width: measured on run #4928,
27
+ * 420 transcript rows were clipped at 80 columns and 97.7 % of a 1.34 MB
28
+ * captured transcript never reached stdout.
29
+ *
30
+ * Nothing here measures a string (R5): a pipe's text is unpadded by definition,
31
+ * so this is the same rows the tree carries and no column arithmetic at all.
32
+ */
33
+ export declare function runDetailRows(document: TicketDetailDocument, runId: string | null | undefined): string[];