@gaia-ai/addon-gaia-ui 0.14.1 → 0.14.2

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 (64) hide show
  1. package/dist/src/Entity/collection.d.ts +15 -0
  2. package/dist/src/Entity/collection.js +26 -5
  3. package/dist/src/Entity/conductor/Data/detail.d.ts +39 -0
  4. package/dist/src/Entity/conductor/Data/detail.js +69 -0
  5. package/dist/src/Entity/conductor/Data/log-block.d.ts +18 -0
  6. package/dist/src/Entity/conductor/Data/log-block.js +32 -0
  7. package/dist/src/Entity/conductor/Screen/conductor-log.d.ts +36 -0
  8. package/dist/src/Entity/conductor/Screen/conductor-log.js +46 -0
  9. package/dist/src/Entity/conductor/Screen/conductor-overview.d.ts +40 -0
  10. package/dist/src/Entity/conductor/Screen/conductor-overview.js +61 -0
  11. package/dist/src/Entity/conductor/Screen/conductor-runs.d.ts +90 -0
  12. package/dist/src/Entity/conductor/Screen/conductor-runs.js +128 -0
  13. package/dist/src/Entity/conductor/Screen/conductors.d.ts +6 -13
  14. package/dist/src/Entity/conductor/Screen/conductors.js +9 -60
  15. package/dist/src/Entity/conductor/Screen/project-conductors.d.ts +6 -13
  16. package/dist/src/Entity/conductor/Screen/project-conductors.js +9 -57
  17. package/dist/src/Entity/project/Data/with-tickets.js +1 -0
  18. package/dist/src/Entity/run/Data/query.d.ts +12 -0
  19. package/dist/src/Entity/run/Data/query.js +22 -1
  20. package/dist/src/Entity/run/Data/runs.d.ts +99 -0
  21. package/dist/src/Entity/run/Data/runs.js +37 -0
  22. package/dist/src/Entity/run/Form/edit.d.ts +29 -0
  23. package/dist/src/Entity/run/Form/edit.js +23 -0
  24. package/dist/src/Entity/run/Screen/edit-run-form.d.ts +26 -0
  25. package/dist/src/Entity/run/Screen/edit-run-form.js +117 -0
  26. package/dist/src/Entity/run/Screen/run-detail.d.ts +24 -3
  27. package/dist/src/Entity/run/Screen/run-detail.js +55 -3
  28. package/dist/src/Entity/ticket/Component/ticket-content/model.js +9 -1
  29. package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +6 -3
  30. package/dist/src/Entity/ticket/Component/ticket-detail/index.js +8 -3
  31. package/dist/src/Entity/ticket/Data/parents.js +2 -0
  32. package/dist/src/Entity/ticket/Data/query.d.ts +17 -0
  33. package/dist/src/Entity/ticket/Data/query.js +29 -1
  34. package/dist/src/Entity/ticket/Data/store.js +16 -0
  35. package/dist/src/Entity/ticket/Data/tickets.d.ts +115 -1
  36. package/dist/src/Entity/ticket/Data/tickets.js +38 -0
  37. package/dist/src/Entity/ticket/Form/who-filter.d.ts +8 -0
  38. package/dist/src/Entity/ticket/Screen/conductor-tickets.d.ts +85 -0
  39. package/dist/src/Entity/ticket/Screen/conductor-tickets.js +111 -0
  40. package/dist/src/Entity/ticket/Screen/ticket-runs.js +9 -0
  41. package/dist/src/Entity/ticket/Screen/tickets.d.ts +8 -0
  42. package/dist/src/Entity/ticket/Screen/tickets.js +27 -0
  43. package/dist/src/Entity/ticket/Screen/user-tickets.d.ts +8 -0
  44. package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +7 -0
  45. package/dist/src/Entity/ticket/Screen/who-filter-form.js +20 -6
  46. package/dist/src/Kernel/braid.d.ts +4 -0
  47. package/dist/src/Kernel/braid.js +10 -0
  48. package/dist/src/Kernel/chrome.d.ts +1 -1
  49. package/dist/src/Kernel/chrome.js +4 -4
  50. package/dist/src/Kernel/splash.js +10 -3
  51. package/dist/src/Routing/registry.js +1 -0
  52. package/dist/src/Routing/routes.js +10 -0
  53. package/dist/src/Screen/async-react-screen.d.ts +4 -2
  54. package/dist/src/Screen/async-react-screen.js +26 -1
  55. package/dist/src/Statistics/figures.d.ts +1 -2
  56. package/dist/src/Statistics/figures.js +72 -29
  57. package/dist/src/Statistics/presentation.d.ts +8 -0
  58. package/dist/src/Statistics/presentation.js +23 -0
  59. package/dist/src/Statistics/screen.d.ts +21 -14
  60. package/dist/src/Statistics/screen.js +126 -27
  61. package/dist/src/Vendor/termcn/ui/dither-chart.js +1 -2
  62. package/dist/src/lib/theme.d.ts +5 -0
  63. package/dist/src/lib/theme.js +5 -0
  64. package/package.json +2 -2
@@ -1,4 +1,3 @@
1
- import type { VNode } from '@opentui/core';
2
1
  import type { ListFilterBadge } from '../../../Component/list-chrome/index.js';
3
2
  import type { FullScreenChrome } from '../../../Screen/full-screen.js';
4
3
  import { ListScreen } from '../../../Screen/list-screen.js';
@@ -88,7 +87,6 @@ export declare class ConductorsScreen extends ListScreen<ConductorRow, Conductor
88
87
  readonly get: (row: ConductorRow) => string;
89
88
  }];
90
89
  };
91
- private logFor;
92
90
  private readonly busyIds;
93
91
  /**
94
92
  * GAIA-460 review — how `mine` failed to resolve, not an action's outcome.
@@ -113,7 +111,6 @@ export declare class ConductorsScreen extends ListScreen<ConductorRow, Conductor
113
111
  protected loadRows(context: ScreenContext, page: number): Promise<Page<ConductorRow>>;
114
112
  protected badges(): readonly ListFilterBadge[];
115
113
  protected renderLoading(): string[];
116
- protected renderReady(value: Page<ConductorRow>, context: ScreenContext): VNode;
117
114
  protected chrome(_context: ScreenContext): FullScreenChrome;
118
115
  /**
119
116
  * GAIA-450 review B9 — `s`/`x` are advertised only when the SELECTED row can
@@ -121,9 +118,9 @@ export declare class ConductorsScreen extends ListScreen<ConductorRow, Conductor
121
118
  * filesystem check, so a foreign conductor whose workspace happens to exist
122
119
  * on this disk (GAIA-281 Finding 2) is correctly excluded too.
123
120
  *
124
- * GAIA-450 review B12 — `Enter :log` joins the same gate. A row that is not
125
- * ours has no readable log on this machine (see {@link logFor}), so
126
- * advertising the key made a promise the footer could not keep.
121
+ * `Enter open` is no longer gated on `mine`: it opens the conductor detail
122
+ * scope for every row, and the log tab inside it is the one place that still
123
+ * withholds a foreign conductor's log (its `mine` derivation lives there).
127
124
  */
128
125
  protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
129
126
  /**
@@ -136,13 +133,9 @@ export declare class ConductorsScreen extends ListScreen<ConductorRow, Conductor
136
133
  protected notice(): string | undefined;
137
134
  select(index: number, context: ScreenContext): boolean;
138
135
  /**
139
- * GAIA-450 review B12 — gated here, not in `handleInput`: the kernel's own
140
- * fallback (`tui-kernel.ts` — an unhandled `enter` calls `active()?.activate?.()`
141
- * unconditionally) and a mouse click on the selected row
142
- * (`Kernel/regions.ts`'s `rows.activate(index)`) both reach this method
143
- * directly, bypassing `handleInput` entirely. Refusing here is therefore the
144
- * one place that actually withholds the log on a row that is not ours,
145
- * whichever path invoked it.
136
+ * The `mine` gate that used to sit here moved onto the log tab: a row on
137
+ * another machine still opens, and its Log tab is where "no log here" is
138
+ * now said (see `Entity/conductor/Data/log-block.ts`).
146
139
  */
147
140
  activate(index: number, context: ScreenContext): boolean;
148
141
  handleInput(input: string, context: ScreenContext): boolean;
@@ -1,44 +1,11 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { configHomeOf } from '@gaia-ai/core';
3
2
  import { actionItem } from '../../../Component/footer-actions/index.js';
4
- import { columnNode, textNode } from '../../../lib/vnode.js';
5
3
  import { ListScreen } from '../../../Screen/list-screen.js';
6
4
  import { runRowAction } from '../../../Screen/row-action.js';
7
5
  import { resolveWhoCandidate } from '../../user/Data/users.js';
8
6
  import { CONDUCTOR_TABLE, countConductors, listConductorRowPage, listProjectNames, } from '../Data/conductors.js';
9
- import { tailConductorLog } from '../Data/log-tail.js';
10
7
  import { DEFAULT_CONDUCTOR_MINE, hiddenNotice, mineBadges, OwnerIdentity, } from '../Data/own-rows.js';
11
8
  import { createConductorProcessHost } from '../Data/process.js';
12
- /**
13
- * The open row's log block: where it is, and its tail.
14
- *
15
- * A conductor on another machine has a workspace this machine cannot read, so
16
- * the block states that instead of showing an empty tail — the same rule the
17
- * row's `elsewhere` mark states for its actions.
18
- *
19
- * GAIA-450 review B12: keyed on `mine`, not `isLocal`. `isLocal` is a
20
- * filesystem existence check on `workspace_root`, so a FOREIGN conductor that
21
- * happens to share this machine's directory layout (GAIA-281 Finding 2 — two
22
- * `gaia_conductor` records naming one checkout under different machines) still
23
- * has `isLocal === true`. Keying the log on it tailed THIS machine's own
24
- * `log.txt` under the foreign conductor's heading, while the footer
25
- * simultaneously read "view-only". `mine` is the identity gate `actions()` and
26
- * `act()` already use; the log now agrees with them.
27
- */
28
- const logFor = (row) => {
29
- // GAIA-450 D4: workspace_root is the checkout root; the log lives under its
30
- // own .gaia dir, not directly at the root.
31
- const logPath = `${configHomeOf(row.workspaceRoot)}/log.txt`;
32
- return row.mine
33
- ? {
34
- heading: `log — ${logPath}`,
35
- lines: tailConductorLog(logPath, { maxLines: 40 }).lines,
36
- }
37
- : {
38
- heading: `log — ${row.label} runs on another machine; no log here`,
39
- lines: [],
40
- };
41
- };
42
9
  /**
43
10
  * Conductors list — an ordinary {@link ListScreen}, paged and filtered to the
44
11
  * operator's own conductors by default (GAIA-460 D1).
@@ -56,7 +23,6 @@ export class ConductorsScreen extends ListScreen {
56
23
  id = ConductorsScreen.route.id;
57
24
  frameId = 'conductors';
58
25
  table = CONDUCTOR_TABLE;
59
- logFor = null;
60
26
  busyIds = new Set();
61
27
  /**
62
28
  * GAIA-460 review — how `mine` failed to resolve, not an action's outcome.
@@ -129,17 +95,6 @@ export class ConductorsScreen extends ListScreen {
129
95
  renderLoading() {
130
96
  return ['Loading conductors…'];
131
97
  }
132
- renderReady(value, context) {
133
- const open = value.rows.find((row) => row.id === this.logFor);
134
- const log = open ? logFor(open) : undefined;
135
- return columnNode('conductor-list', super.renderReady(value, context), ...(log
136
- ? [
137
- textNode({ id: 'conductor-log-gap', content: '' }),
138
- textNode({ id: 'conductor-log-heading', content: log.heading }),
139
- ...log.lines.map((line, index) => textNode({ id: `conductor-log-${index}`, content: line })),
140
- ]
141
- : []));
142
- }
143
98
  chrome(_context) {
144
99
  return {
145
100
  selectable: this.rows.length,
@@ -161,9 +116,9 @@ export class ConductorsScreen extends ListScreen {
161
116
  * filesystem check, so a foreign conductor whose workspace happens to exist
162
117
  * on this disk (GAIA-281 Finding 2) is correctly excluded too.
163
118
  *
164
- * GAIA-450 review B12 — `Enter :log` joins the same gate. A row that is not
165
- * ours has no readable log on this machine (see {@link logFor}), so
166
- * advertising the key made a promise the footer could not keep.
119
+ * `Enter open` is no longer gated on `mine`: it opens the conductor detail
120
+ * scope for every row, and the log tab inside it is the one place that still
121
+ * withholds a foreign conductor's log (its `mine` derivation lives there).
167
122
  */
168
123
  actions() {
169
124
  const canAct = this.rows[this.selectedIndex]?.mine ?? false;
@@ -172,9 +127,9 @@ export class ConductorsScreen extends ListScreen {
172
127
  ? [
173
128
  actionItem('conductors-start', 's', 'start'),
174
129
  actionItem('conductors-stop', 'x', 'stop'),
175
- actionItem('conductors-enter', 'Enter', 'log'),
176
130
  ]
177
131
  : []),
132
+ actionItem('conductors-enter', 'Enter', 'open'),
178
133
  actionItem('conductors-tab', 'Tab', 'tabs'),
179
134
  ...this.pagingActionItems(),
180
135
  ];
@@ -203,20 +158,15 @@ export class ConductorsScreen extends ListScreen {
203
158
  return true;
204
159
  }
205
160
  /**
206
- * GAIA-450 review B12 — gated here, not in `handleInput`: the kernel's own
207
- * fallback (`tui-kernel.ts` — an unhandled `enter` calls `active()?.activate?.()`
208
- * unconditionally) and a mouse click on the selected row
209
- * (`Kernel/regions.ts`'s `rows.activate(index)`) both reach this method
210
- * directly, bypassing `handleInput` entirely. Refusing here is therefore the
211
- * one place that actually withholds the log on a row that is not ours,
212
- * whichever path invoked it.
161
+ * The `mine` gate that used to sit here moved onto the log tab: a row on
162
+ * another machine still opens, and its Log tab is where "no log here" is
163
+ * now said (see `Entity/conductor/Data/log-block.ts`).
213
164
  */
214
165
  activate(index, context) {
215
166
  const row = this.rows[index];
216
- if (!row?.mine)
167
+ if (!row?.id || !context.navigate)
217
168
  return false;
218
- this.logFor = this.logFor === row.id ? null : row.id;
219
- context.requestDraw();
169
+ context.navigate('conductor.overview', { conductorId: row.id });
220
170
  return true;
221
171
  }
222
172
  handleInput(input, context) {
@@ -256,7 +206,6 @@ export class ConductorsScreen extends ListScreen {
256
206
  this.identity.reset();
257
207
  this.ownerId = undefined;
258
208
  this.hidden = null;
259
- this.logFor = null;
260
209
  this.busyIds.clear();
261
210
  this.identityReason = '';
262
211
  super.reset();
@@ -1,4 +1,3 @@
1
- import type { VNode } from '@opentui/core';
2
1
  import type { ListFilterBadge } from '../../../Component/list-chrome/index.js';
3
2
  import type { FullScreenChrome } from '../../../Screen/full-screen.js';
4
3
  import { ListScreen } from '../../../Screen/list-screen.js';
@@ -83,7 +82,6 @@ export declare class ProjectConductorsScreen extends ListScreen<ConductorRow, Co
83
82
  readonly noun: "conductor";
84
83
  readonly emptyMessage: "No conductors on this control plane.";
85
84
  };
86
- private logFor;
87
85
  private readonly busyIds;
88
86
  /**
89
87
  * GAIA-460 review — how `mine` failed to resolve, not an action's outcome.
@@ -107,7 +105,6 @@ export declare class ProjectConductorsScreen extends ListScreen<ConductorRow, Co
107
105
  protected loadRows(context: ScreenContext, page: number): Promise<Page<ConductorRow>>;
108
106
  protected badges(): readonly ListFilterBadge[];
109
107
  protected renderLoading(): string[];
110
- protected renderReady(value: Page<ConductorRow>, context: ScreenContext): VNode;
111
108
  protected chrome(context: ScreenContext): FullScreenChrome;
112
109
  /**
113
110
  * GAIA-450 review B9 — `s`/`x` are advertised only when the SELECTED row can
@@ -115,9 +112,9 @@ export declare class ProjectConductorsScreen extends ListScreen<ConductorRow, Co
115
112
  * filesystem check, so a foreign conductor whose workspace happens to exist
116
113
  * on this disk (GAIA-281 Finding 2) is correctly excluded too.
117
114
  *
118
- * GAIA-450 review B12 — `Enter :log` joins the same gate. A row that is not
119
- * ours has no readable log on this machine (see {@link logFor}), so
120
- * advertising the key made a promise the footer could not keep.
115
+ * `Enter open` is no longer gated on `mine`: it opens the conductor detail
116
+ * scope for every row; its Log tab is the one place that still withholds a
117
+ * foreign conductor's log.
121
118
  */
122
119
  protected actions(): import("../../../Component/footer-actions/index.js").ActionItem[];
123
120
  /**
@@ -130,13 +127,9 @@ export declare class ProjectConductorsScreen extends ListScreen<ConductorRow, Co
130
127
  protected notice(): string | undefined;
131
128
  select(index: number, context: ScreenContext): boolean;
132
129
  /**
133
- * GAIA-450 review B12 — gated here, not in `handleInput`: the kernel's own
134
- * fallback (`tui-kernel.ts` — an unhandled `enter` calls `active()?.activate?.()`
135
- * unconditionally) and a mouse click on the selected row
136
- * (`Kernel/regions.ts`'s `rows.activate(index)`) both reach this method
137
- * directly, bypassing `handleInput` entirely. Refusing here is therefore the
138
- * one place that actually withholds the log on a row that is not ours,
139
- * whichever path invoked it.
130
+ * The `mine` gate that used to sit here moved onto the log tab: a row on
131
+ * another machine still opens, and its Log tab is where "no log here" is
132
+ * now said (see `Entity/conductor/Data/log-block.ts`).
140
133
  */
141
134
  activate(index: number, context: ScreenContext): boolean;
142
135
  handleInput(input: string, context: ScreenContext): boolean;
@@ -1,38 +1,11 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { configHomeOf } from '@gaia-ai/core';
3
2
  import { actionItem } from '../../../Component/footer-actions/index.js';
4
- import { columnNode, textNode } from '../../../lib/vnode.js';
5
3
  import { ListScreen } from '../../../Screen/list-screen.js';
6
4
  import { runRowAction } from '../../../Screen/row-action.js';
7
5
  import { resolveWhoCandidate } from '../../user/Data/users.js';
8
6
  import { countConductors, listConductorRowPage, PROJECT_CONDUCTOR_TABLE, } from '../Data/conductors.js';
9
- import { tailConductorLog } from '../Data/log-tail.js';
10
7
  import { DEFAULT_CONDUCTOR_MINE, hiddenNotice, mineBadges, OwnerIdentity, } from '../Data/own-rows.js';
11
8
  import { createConductorProcessHost } from '../Data/process.js';
12
- /**
13
- * The open row's log block: where it is, and its tail.
14
- *
15
- * A conductor on another machine has a workspace this machine cannot read, so
16
- * the block states that instead of showing an empty tail — the same rule the
17
- * row's `elsewhere` mark states for its actions.
18
- *
19
- * GAIA-450 review B12: keyed on `mine`, not `isLocal` — see the root screen's
20
- * copy of this comment for the GAIA-281 Finding 2 shape this closes.
21
- */
22
- const logFor = (row) => {
23
- // GAIA-450 D4: workspace_root is the checkout root; the log lives under its
24
- // own .gaia dir, not directly at the root.
25
- const logPath = `${configHomeOf(row.workspaceRoot)}/log.txt`;
26
- return row.mine
27
- ? {
28
- heading: `log — ${logPath}`,
29
- lines: tailConductorLog(logPath, { maxLines: 40 }).lines,
30
- }
31
- : {
32
- heading: `log — ${row.label} runs on another machine; no log here`,
33
- lines: [],
34
- };
35
- };
36
9
  /**
37
10
  * The project-scoped Conductors tab — an ordinary {@link ListScreen}, paged
38
11
  * and filtered to the operator's own conductors by default (GAIA-460 D1),
@@ -57,7 +30,6 @@ export class ProjectConductorsScreen extends ListScreen {
57
30
  id = ProjectConductorsScreen.route.id;
58
31
  frameId = 'project-conductors';
59
32
  table = PROJECT_CONDUCTOR_TABLE;
60
- logFor = null;
61
33
  busyIds = new Set();
62
34
  /**
63
35
  * GAIA-460 review — how `mine` failed to resolve, not an action's outcome.
@@ -126,20 +98,6 @@ export class ProjectConductorsScreen extends ListScreen {
126
98
  renderLoading() {
127
99
  return ['Loading conductors…'];
128
100
  }
129
- renderReady(value, context) {
130
- const open = value.rows.find((row) => row.id === this.logFor);
131
- const log = open ? logFor(open) : undefined;
132
- return columnNode('project-conductor-list', super.renderReady(value, context), ...(log
133
- ? [
134
- textNode({ id: 'project-conductor-log-gap', content: '' }),
135
- textNode({
136
- id: 'project-conductor-log-heading',
137
- content: log.heading,
138
- }),
139
- ...log.lines.map((line, index) => textNode({ id: `project-conductor-log-${index}`, content: line })),
140
- ]
141
- : []));
142
- }
143
101
  chrome(context) {
144
102
  return {
145
103
  selectable: this.rows.length,
@@ -161,9 +119,9 @@ export class ProjectConductorsScreen extends ListScreen {
161
119
  * filesystem check, so a foreign conductor whose workspace happens to exist
162
120
  * on this disk (GAIA-281 Finding 2) is correctly excluded too.
163
121
  *
164
- * GAIA-450 review B12 — `Enter :log` joins the same gate. A row that is not
165
- * ours has no readable log on this machine (see {@link logFor}), so
166
- * advertising the key made a promise the footer could not keep.
122
+ * `Enter open` is no longer gated on `mine`: it opens the conductor detail
123
+ * scope for every row; its Log tab is the one place that still withholds a
124
+ * foreign conductor's log.
167
125
  */
168
126
  actions() {
169
127
  const canAct = this.rows[this.selectedIndex]?.mine ?? false;
@@ -172,9 +130,9 @@ export class ProjectConductorsScreen extends ListScreen {
172
130
  ? [
173
131
  actionItem('project-conductors-start', 's', 'start'),
174
132
  actionItem('project-conductors-stop', 'x', 'stop'),
175
- actionItem('project-conductors-enter', 'Enter', 'log'),
176
133
  ]
177
134
  : []),
135
+ actionItem('project-conductors-enter', 'Enter', 'open'),
178
136
  actionItem('project-conductors-tab', 'Tab', 'tabs'),
179
137
  ...this.pagingActionItems(),
180
138
  ];
@@ -203,20 +161,15 @@ export class ProjectConductorsScreen extends ListScreen {
203
161
  return true;
204
162
  }
205
163
  /**
206
- * GAIA-450 review B12 — gated here, not in `handleInput`: the kernel's own
207
- * fallback (`tui-kernel.ts` — an unhandled `enter` calls `active()?.activate?.()`
208
- * unconditionally) and a mouse click on the selected row
209
- * (`Kernel/regions.ts`'s `rows.activate(index)`) both reach this method
210
- * directly, bypassing `handleInput` entirely. Refusing here is therefore the
211
- * one place that actually withholds the log on a row that is not ours,
212
- * whichever path invoked it.
164
+ * The `mine` gate that used to sit here moved onto the log tab: a row on
165
+ * another machine still opens, and its Log tab is where "no log here" is
166
+ * now said (see `Entity/conductor/Data/log-block.ts`).
213
167
  */
214
168
  activate(index, context) {
215
169
  const row = this.rows[index];
216
- if (!row?.mine)
170
+ if (!row?.id || !context.navigate)
217
171
  return false;
218
- this.logFor = this.logFor === row.id ? null : row.id;
219
- context.requestDraw();
172
+ context.navigate('conductor.overview', { conductorId: row.id });
220
173
  return true;
221
174
  }
222
175
  handleInput(input, context) {
@@ -256,7 +209,6 @@ export class ProjectConductorsScreen extends ListScreen {
256
209
  this.identity.reset();
257
210
  this.ownerId = undefined;
258
211
  this.hidden = null;
259
- this.logFor = null;
260
212
  this.busyIds.clear();
261
213
  this.identityReason = '';
262
214
  super.reset();
@@ -30,6 +30,7 @@ export async function loadProjectWithTickets(client, projectSlug) {
30
30
  closed: null,
31
31
  parent: null,
32
32
  who: null,
33
+ owner: null,
33
34
  });
34
35
  return {
35
36
  project,
@@ -1,4 +1,7 @@
1
1
  import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
2
+ /** Non-terminal run states — the ONLY states "currently held work" means.
3
+ * Never `closed`, which tracks something else entirely. */
4
+ export declare const HELD_RUN_STATES: readonly ["claimed", "running"];
2
5
  /** The active filter criteria for the project run list. */
3
6
  export interface RunCriteria {
4
7
  /** false: open only (the default) · true: closed only · null: all. */
@@ -36,3 +39,12 @@ export declare function buildTeamMemberRunQueryParams(userId: string, projectIds
36
39
  size: number;
37
40
  index: number;
38
41
  }): DrupalJsonApiParams;
42
+ /**
43
+ * Build the JSON:API query for one conductor's held work: runs whose
44
+ * `conductor_id` matches and whose state is still non-terminal
45
+ * (claimed/running), newest-started first. Never filters on `closed`.
46
+ */
47
+ export declare function buildConductorRunQueryParams(conductorId: string, page?: {
48
+ size: number;
49
+ index: number;
50
+ }): DrupalJsonApiParams;
@@ -3,7 +3,10 @@
3
3
  // Runs offer only the closed cycle; search/state are not part of GAIA-282.
4
4
  import { DrupalJsonApiParams } from 'drupal-jsonapi-params';
5
5
  import { LIST_PAGE_SIZE } from '../../cycle.js';
6
- import { RUN_LIST_FIELDS, RUN_TICKET_INCLUDE_FIELDS } from './runs.js';
6
+ import { CONDUCTOR_RUN_TICKET_INCLUDE_FIELDS, RUN_LIST_FIELDS, RUN_TICKET_INCLUDE_FIELDS, } from './runs.js';
7
+ /** Non-terminal run states — the ONLY states "currently held work" means.
8
+ * Never `closed`, which tracks something else entirely. */
9
+ export const HELD_RUN_STATES = ['claimed', 'running'];
7
10
  /** The shipped default: closed runs hidden (GAIA-282 D1). */
8
11
  export const DEFAULT_RUN_CRITERIA = { closed: false };
9
12
  /** "Active" means the view is narrowed relative to showing every row —
@@ -49,6 +52,24 @@ export function buildTeamMemberRunQueryParams(userId, projectIds, criteria, page
49
52
  applyRunCriteria(params, criteria, page);
50
53
  return params;
51
54
  }
55
+ /**
56
+ * Build the JSON:API query for one conductor's held work: runs whose
57
+ * `conductor_id` matches and whose state is still non-terminal
58
+ * (claimed/running), newest-started first. Never filters on `closed`.
59
+ */
60
+ export function buildConductorRunQueryParams(conductorId, page = { size: LIST_PAGE_SIZE, index: 0 }) {
61
+ const params = new DrupalJsonApiParams();
62
+ params.addFilter('conductor_id.id', conductorId);
63
+ params.addFilter('state', [...HELD_RUN_STATES], 'IN');
64
+ params.addInclude(['ticket_id']);
65
+ params.addFields('gaia_ticket--gaia_ticket', CONDUCTOR_RUN_TICKET_INCLUDE_FIELDS);
66
+ params.addFields('gaia_run--gaia_run', RUN_LIST_FIELDS);
67
+ params.addSort('started_at', 'DESC');
68
+ params.addPageLimit(page.size);
69
+ if (page.index > 0)
70
+ params.addPageOffset(page.index * page.size);
71
+ return params;
72
+ }
52
73
  function applyRunCriteria(params, criteria, page) {
53
74
  if (criteria.closed !== null)
54
75
  params.addFilter('closed', criteria.closed ? '1' : '0');
@@ -10,6 +10,9 @@ export interface RunRow {
10
10
  ticketId: string | null;
11
11
  /** The ticket's `identifier` from the sideloaded include, or '—'. */
12
12
  ticketIdentifier: string;
13
+ /** The ticket's `title` from the sideloaded include, or '' when the
14
+ * include did not carry one (only the conductor-scoped query asks). */
15
+ ticketTitle: string;
13
16
  /** Authoritative run state (`claimed|running|done|expired|failed`). */
14
17
  state: string;
15
18
  /** The ticket state the run was dispatched for (`state_at_start`). */
@@ -199,6 +202,102 @@ export declare const RUN_LIST_FIELDS: string[];
199
202
  * out of 254.7 KB total, and took 4732 ms.
200
203
  */
201
204
  export declare const RUN_TICKET_INCLUDE_FIELDS: string[];
205
+ /**
206
+ * The conductor-scoped held-work table also names the ticket's title, so its
207
+ * ticket include fieldset is wider than the project run list's — declared
208
+ * separately so that list's own payload does not widen.
209
+ */
210
+ export declare const CONDUCTOR_RUN_TICKET_INCLUDE_FIELDS: string[];
211
+ /**
212
+ * The held-work table's columns: RUN, TICKET, TITLE, STATE, PHASE, STARTED.
213
+ * AGENT is dropped — the conductor is constant on this page, and so is its
214
+ * agent-per-run detail of little use here.
215
+ */
216
+ export declare const CONDUCTOR_RUN_COLUMNS: readonly [{
217
+ readonly key: "run";
218
+ readonly label: "RUN";
219
+ readonly width: 8;
220
+ readonly fields: readonly ["drupal_internal__id"];
221
+ readonly get: (r: RunRow) => string;
222
+ }, {
223
+ readonly key: "ticket";
224
+ readonly label: "TICKET";
225
+ readonly width: 10;
226
+ readonly fields: readonly ["ticket_id"];
227
+ readonly get: (r: RunRow) => string;
228
+ }, {
229
+ readonly key: "title";
230
+ readonly label: "TITLE";
231
+ readonly width: 0;
232
+ readonly grow: 2;
233
+ readonly min: 24;
234
+ readonly fields: readonly ["ticket_id"];
235
+ readonly get: (r: RunRow) => string;
236
+ }, {
237
+ readonly key: "state";
238
+ readonly label: "STATE";
239
+ readonly width: 8;
240
+ readonly fields: readonly ["state", "heartbeat", "claim_expires_at"];
241
+ readonly get: (r: RunRow, nowSeconds: number) => string;
242
+ }, {
243
+ readonly key: "phase";
244
+ readonly label: "PHASE";
245
+ readonly width: 13;
246
+ readonly fields: readonly ["state_at_start"];
247
+ readonly get: (r: RunRow) => string;
248
+ }, {
249
+ readonly key: "started";
250
+ readonly label: "STARTED";
251
+ readonly width: 0;
252
+ readonly grow: 1;
253
+ readonly fields: readonly ["started_at", "created"];
254
+ readonly get: (r: RunRow, nowSeconds: number) => string;
255
+ }];
256
+ /** Held work: one conductor's non-terminal runs (GAIA-282-style table). */
257
+ export declare const CONDUCTOR_RUN_TABLE: {
258
+ readonly noun: "run";
259
+ readonly columns: readonly [{
260
+ readonly key: "run";
261
+ readonly label: "RUN";
262
+ readonly width: 8;
263
+ readonly fields: readonly ["drupal_internal__id"];
264
+ readonly get: (r: RunRow) => string;
265
+ }, {
266
+ readonly key: "ticket";
267
+ readonly label: "TICKET";
268
+ readonly width: 10;
269
+ readonly fields: readonly ["ticket_id"];
270
+ readonly get: (r: RunRow) => string;
271
+ }, {
272
+ readonly key: "title";
273
+ readonly label: "TITLE";
274
+ readonly width: 0;
275
+ readonly grow: 2;
276
+ readonly min: 24;
277
+ readonly fields: readonly ["ticket_id"];
278
+ readonly get: (r: RunRow) => string;
279
+ }, {
280
+ readonly key: "state";
281
+ readonly label: "STATE";
282
+ readonly width: 8;
283
+ readonly fields: readonly ["state", "heartbeat", "claim_expires_at"];
284
+ readonly get: (r: RunRow, nowSeconds: number) => string;
285
+ }, {
286
+ readonly key: "phase";
287
+ readonly label: "PHASE";
288
+ readonly width: 13;
289
+ readonly fields: readonly ["state_at_start"];
290
+ readonly get: (r: RunRow) => string;
291
+ }, {
292
+ readonly key: "started";
293
+ readonly label: "STARTED";
294
+ readonly width: 0;
295
+ readonly grow: 1;
296
+ readonly fields: readonly ["started_at", "created"];
297
+ readonly get: (r: RunRow, nowSeconds: number) => string;
298
+ }];
299
+ readonly emptyMessage: "This conductor holds no run.";
300
+ };
202
301
  /**
203
302
  * Map a RAW JSON:API `data[]` entry (from `client.get`) to a run row, resolving
204
303
  * the ticket identifier from the sideloaded includes indexed by id. Mirrors
@@ -154,6 +154,42 @@ export const RUN_LIST_FIELDS = [
154
154
  * out of 254.7 KB total, and took 4732 ms.
155
155
  */
156
156
  export const RUN_TICKET_INCLUDE_FIELDS = ['identifier'];
157
+ /**
158
+ * The conductor-scoped held-work table also names the ticket's title, so its
159
+ * ticket include fieldset is wider than the project run list's — declared
160
+ * separately so that list's own payload does not widen.
161
+ */
162
+ export const CONDUCTOR_RUN_TICKET_INCLUDE_FIELDS = [
163
+ 'identifier',
164
+ 'title',
165
+ ];
166
+ /**
167
+ * The held-work table's columns: RUN, TICKET, TITLE, STATE, PHASE, STARTED.
168
+ * AGENT is dropped — the conductor is constant on this page, and so is its
169
+ * agent-per-run detail of little use here.
170
+ */
171
+ export const CONDUCTOR_RUN_COLUMNS = [
172
+ RUN_COLUMNS[0], // RUN
173
+ RUN_COLUMNS[1], // TICKET
174
+ {
175
+ key: 'title',
176
+ label: 'TITLE',
177
+ width: 0,
178
+ grow: 2,
179
+ min: 24,
180
+ fields: ['ticket_id'],
181
+ get: (r) => r.ticketTitle,
182
+ },
183
+ RUN_COLUMNS[2], // STATE
184
+ RUN_COLUMNS[3], // PHASE
185
+ RUN_COLUMNS[5], // STARTED
186
+ ];
187
+ /** Held work: one conductor's non-terminal runs (GAIA-282-style table). */
188
+ export const CONDUCTOR_RUN_TABLE = {
189
+ noun: 'run',
190
+ columns: CONDUCTOR_RUN_COLUMNS,
191
+ emptyMessage: 'This conductor holds no run.',
192
+ };
157
193
  /**
158
194
  * Map a RAW JSON:API `data[]` entry (from `client.get`) to a run row, resolving
159
195
  * the ticket identifier from the sideloaded includes indexed by id. Mirrors
@@ -181,6 +217,7 @@ export function mapRunData(item, ticketsById) {
181
217
  drupalId,
182
218
  ticketId,
183
219
  ticketIdentifier: str(ticket?.attributes?.identifier) ?? (ticketId ? ticketId : '—'),
220
+ ticketTitle: str(ticket?.attributes?.title) ?? '',
184
221
  state: text('state') ?? 'unknown',
185
222
  phase: text('state_at_start') ?? '—',
186
223
  agent: text('agent') ?? '—',
@@ -0,0 +1,29 @@
1
+ export declare const CANCELLED_RUN_STATE = "cancelled";
2
+ /**
3
+ * The run states `cancel` leaves, mirroring `gaia_run.transitions.cancel.from`
4
+ * in `gaia_core.workflows.yml` by hand.
5
+ *
6
+ * No served fact carries workflow transition edges, so widen this set together
7
+ * with the YAML transition whenever its source states change.
8
+ */
9
+ export declare const CANCELLABLE_RUN_STATES: ReadonlySet<string>;
10
+ export declare const CANCEL_RUN_ACTION_ID = "cancelRun";
11
+ export type RunEditFieldId = typeof CANCEL_RUN_ACTION_ID;
12
+ export interface CancelRunPayload {
13
+ attributes: {
14
+ state: typeof CANCELLED_RUN_STATE;
15
+ };
16
+ }
17
+ export declare const cancelRunPayload: () => CancelRunPayload;
18
+ declare const EDIT_FORM: {
19
+ fields: import("../../../Form/field.js").FieldDecl[];
20
+ activeField: (s: import("../../../Form/form.js").FormState<Record<string, never>>) => import("../../../Form/field.js").FieldDecl;
21
+ move: (s: import("../../../Form/form.js").FormState<Record<string, never>>, d: number) => import("../../../Form/form.js").FormState<Record<string, never>>;
22
+ initial(options: Record<string, never>, prefill?: Partial<Record<string, never>>): import("../../../Form/form.js").FormState<Record<string, never>>;
23
+ apply(s: import("../../../Form/form.js").FormState<Record<string, never>>, options: Record<string, never>, intent: import("../../../Form/form.js").Intent): import("../../../Form/form.js").FormState<Record<string, never>>;
24
+ toObject: (s: import("../../../Form/form.js").FormState<Record<string, never>>, options: Record<string, never>) => CancelRunPayload;
25
+ };
26
+ export type EditRunFormDeclaration = typeof EDIT_FORM;
27
+ export declare const isRunCancellable: (state: string) => boolean;
28
+ export declare const editRunFormFor: (runState: string) => EditRunFormDeclaration | undefined;
29
+ export {};
@@ -0,0 +1,23 @@
1
+ import { declareForm } from '../../../Form/index.js';
2
+ export const CANCELLED_RUN_STATE = 'cancelled';
3
+ /**
4
+ * The run states `cancel` leaves, mirroring `gaia_run.transitions.cancel.from`
5
+ * in `gaia_core.workflows.yml` by hand.
6
+ *
7
+ * No served fact carries workflow transition edges, so widen this set together
8
+ * with the YAML transition whenever its source states change.
9
+ */
10
+ export const CANCELLABLE_RUN_STATES = new Set([
11
+ 'claimed',
12
+ 'running',
13
+ ]);
14
+ export const CANCEL_RUN_ACTION_ID = 'cancelRun';
15
+ export const cancelRunPayload = () => ({
16
+ attributes: { state: CANCELLED_RUN_STATE },
17
+ });
18
+ const EDIT_FORM = declareForm({
19
+ fields: (f) => [f.action('Cancel run', CANCEL_RUN_ACTION_ID)],
20
+ toObject: () => cancelRunPayload(),
21
+ });
22
+ export const isRunCancellable = (state) => CANCELLABLE_RUN_STATES.has(state);
23
+ export const editRunFormFor = (runState) => isRunCancellable(runState) ? EDIT_FORM : undefined;