@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,115 @@
1
+ // GAIA-340 (ticket decision 6) — the widget layer's own `Renderable`, and the
2
+ // one control the cockpit does not hand-paint.
3
+ //
4
+ // It arrived in GAIA-326 as `ViewOnlyTextarea` inside `Kernel/caret-slot.ts`,
5
+ // where it was placed ABSOLUTELY over rows the frame had already composed. Here
6
+ // it is a node in the tree the form returns, laid out by Yoga like every other
7
+ // node — same class, same reasons, one owner. Task 9 Step 5 deleted that module
8
+ // with the last screen that composed a popup out of rows, so this is the only
9
+ // place the cockpit's insertion point has ever to be looked for.
10
+ //
11
+ // WHY A SUBCLASS AT ALL, since `@opentui/core` already ships the control:
12
+ // `Kernel/mount.ts` patches a standing tree by ASSIGNING the props the next
13
+ // frame declares, and `TextareaRenderable` exposes no assignable text. `setText`
14
+ // is a method, `plainText` is read-only, and `initialValue`'s setter is guarded
15
+ // by `_initialValueSet` so it applies exactly once. A raw
16
+ // `h(TextareaRenderable, { initialValue })` would therefore paint the value the
17
+ // field held when it was mounted and never change again — and the first frame
18
+ // would look right, which is the kind of defect the PTY drivers exist for.
19
+ // `value` and `caretOffset` are the two assignable props that fix it.
20
+ import { RGBA, TextareaRenderable, } from '@opentui/core';
21
+ /**
22
+ * Every colour the field uses, explicitly the terminal's own.
23
+ *
24
+ * `TextareaRenderable` defaults `textColor` to `#FFFFFF` and `placeholderColor`
25
+ * to `#666666`, and `EditBufferRenderable` defaults `cursorColor` to
26
+ * `rgba(1,1,1,1)` — hardcoded hex, i.e. TRUECOLOR on the wire, which is the one
27
+ * thing `tests/pty/verify-colors.mjs` fails the cockpit for and the exact defect
28
+ * that froze an operator's theme once already.
29
+ */
30
+ const COLORS = () => ({
31
+ textColor: RGBA.defaultForeground(),
32
+ backgroundColor: RGBA.defaultBackground(),
33
+ focusedTextColor: RGBA.defaultForeground(),
34
+ focusedBackgroundColor: RGBA.defaultBackground(),
35
+ placeholderColor: RGBA.defaultForeground(),
36
+ cursorColor: RGBA.defaultForeground(),
37
+ });
38
+ /**
39
+ * A text field's value block: a Textarea that paints and never listens.
40
+ *
41
+ * CONTROLLED, and that word carries the whole design. The form reducer stays the
42
+ * single source of truth: this renderable is handed the current value and the
43
+ * current offset on every draw and owns NOTHING. So it must not also process
44
+ * keys — a focused renderable's `handleKeyPress` runs after the kernel's global
45
+ * listener, which does not `preventDefault()` (and could not do so per-widget),
46
+ * so both would apply the same keystroke. Measured against the stock class:
47
+ * typing `X` into a focused Textarea holding `ab` left `abX` in the widget AND
48
+ * `abX` in the reducer.
49
+ */
50
+ export class FieldTextarea extends TextareaRenderable {
51
+ constructor(ctx, options) {
52
+ super(ctx, {
53
+ ...COLORS(),
54
+ // The widget layer's own wrap already decided where the lines break and
55
+ // the caller reserved exactly that many rows, so the renderable must not
56
+ // wrap again: it is handed the finished rows and lays them out verbatim.
57
+ // Measured, and the reason this is not `'word'`: the native word wrap
58
+ // breaks a line that exactly fills the width, so it disagreed with
59
+ // `wrapToWidthRows` on 3 of 10 sampled values — and a disagreement is a
60
+ // row the caller did not reserve.
61
+ wrapMode: 'none',
62
+ // No context margin: the viewport is sized to hold the whole row plus the
63
+ // cursor's cell, so there is nothing to scroll towards and a margin would
64
+ // only invent a reason to scroll.
65
+ scrollMargin: 0,
66
+ ...options,
67
+ });
68
+ // A picture of a value, not a document: a drag over it would paint a
69
+ // selection the cockpit never asked for, exactly as on the composed rows.
70
+ this.selectable = false;
71
+ // `instantiate` passes props to the CONSTRUCTOR and assigns nothing, while
72
+ // `patchTree` assigns and constructs nothing — so both entry points have to
73
+ // be served, and the first frame goes through here.
74
+ if (options.value !== undefined)
75
+ this.value = options.value;
76
+ if (options.caretOffset !== undefined)
77
+ this.caretOffset = options.caretOffset;
78
+ }
79
+ /** The reducer owns every key. This one sees them and declines all of them. */
80
+ handleKeyPress() {
81
+ return false;
82
+ }
83
+ /** …and a bracketed paste is just a very fast sequence of them. */
84
+ handlePaste() { }
85
+ get value() {
86
+ return this.plainText;
87
+ }
88
+ /**
89
+ * Guarded because `setText` resets the whole buffer through FFI, and a draw
90
+ * happens on every keystroke, wheel notch and resize — most of which leave the
91
+ * value alone and move only the offset.
92
+ */
93
+ set value(text) {
94
+ if (this.plainText !== text)
95
+ this.setText(text);
96
+ }
97
+ get caretOffset() {
98
+ return this.cursorOffset;
99
+ }
100
+ /**
101
+ * Focus before the offset: `renderCursor` draws nothing unless the renderable
102
+ * is focused, and the offset is what it resolves into a cell. Re-asserted on
103
+ * every draw rather than once, so a focus the renderer moved elsewhere (a
104
+ * click's `autoFocus`) is taken back on the next repaint instead of leaving
105
+ * the operator without a cursor.
106
+ *
107
+ * A node declaring both props must state `value` BEFORE `caretOffset`:
108
+ * `patchTree` assigns in declaration order and `setText` resets the offset.
109
+ */
110
+ set caretOffset(offset) {
111
+ if (!this.focused)
112
+ this.focus();
113
+ this.cursorOffset = offset;
114
+ }
115
+ }
@@ -5,7 +5,8 @@ export interface WidgetContext {
5
5
  readonly field: FieldDecl;
6
6
  /** Whether the form's cursor is on this field. */
7
7
  readonly active: boolean;
8
- /** The kernel's within-field cursor (a multiselect row, a suggestion row). */
8
+ /** The kernel's within-field cursor — per kind: a character offset in a
9
+ * text value (GAIA-283), a multiselect row, a suggestion row. */
9
10
  readonly cursor: number;
10
11
  /** Display columns the popup body may use, border excluded. */
11
12
  readonly width: number;
@@ -36,6 +37,58 @@ export interface WidgetContext {
36
37
  /** The text typed into this field's search box (an `autocomplete` field). */
37
38
  query(): string;
38
39
  }
40
+ /**
41
+ * GAIA-326 — where a kind's insertion point goes, for the kinds that have one.
42
+ *
43
+ * NOT A DRAWING. The `text` field used to splice an inverse-video cell into its
44
+ * own row; on an operator decision — they drive the cockpit over mosh from a
45
+ * phone, where mosh's local predictive echo needs a real cursor to predict at —
46
+ * the insertion point became a terminal cursor instead. A terminal cursor is not
47
+ * a character, so it cannot be expressed in the rows a widget returns: it is
48
+ * published as a POSITION and `renderWidget` turns it into a `FieldTextarea`
49
+ * node, which owns the cell and the cursor.
50
+ *
51
+ * IT LIVES HERE NOW, and that is the whole shape of GAIA-340 Task 9. It used to
52
+ * be `types.ts`'s `TextCaret`, in the shared types, because three layers that may
53
+ * not import each other all spoke it — `Widget/` published it, `Screen/` carried
54
+ * it on the presentation and `Kernel/` consumed it, each restating the one
55
+ * coordinate space it owned (a widget row, a `lines` index, a box row). The
56
+ * screens hand over trees, so nothing rebases it: `renderWidget` spends `row` and
57
+ * `column` as the block's position in the layout and Yoga places it. One layer
58
+ * speaks it, and it is this one.
59
+ *
60
+ * `row` indexes the rows `render` returned and `column` counts display columns
61
+ * from the popup body's left edge.
62
+ */
63
+ export interface FieldCaret {
64
+ /** The row the value block starts on, among the rows `render` returned. */
65
+ readonly row: number;
66
+ /** Display columns from the body's left edge to the value block. */
67
+ readonly column: number;
68
+ /** The value block's display width in columns... */
69
+ readonly width: number;
70
+ /** ...and its height in rows. */
71
+ readonly height: number;
72
+ /**
73
+ * The value block's rows joined by `\n`.
74
+ *
75
+ * ONE WRAP: the widget wraps once and hands the rows here, so the height the
76
+ * layout reserves and the text the renderable paints cannot disagree about
77
+ * where a line breaks.
78
+ */
79
+ readonly text: string;
80
+ /** The insertion point's offset into {@link text} (UTF-16 code units). */
81
+ readonly offset: number;
82
+ /**
83
+ * What to paint when {@link text} is empty.
84
+ *
85
+ * An empty `text` field still occupies a row so the popup's rows keep lining
86
+ * up with its fields, and the block is what stands on that row — so it has to
87
+ * be told the same word the composed row said, or an empty active field would
88
+ * go blank the moment it got the cursor.
89
+ */
90
+ readonly placeholder: string | undefined;
91
+ }
39
92
  export interface FieldWidget {
40
93
  /**
41
94
  * The rows this field occupies, WITHOUT the active marker: the caller prefixes
@@ -43,6 +96,16 @@ export interface FieldWidget {
43
96
  * absolute indent rather than a relative one.
44
97
  */
45
98
  render(ctx: WidgetContext): string[];
99
+ /**
100
+ * Where this kind's insertion point goes, when it is ACTIVE and has one.
101
+ *
102
+ * Optional because most kinds have none at all: a `select` is a ring, a
103
+ * `multiselect` a checklist, an `action` a button. Two kinds implement it —
104
+ * `text` (the value block) and, since GAIA-326's widget-fit F2 conversion,
105
+ * `autocomplete` (the query row, whose `▌` character it replaced with a real
106
+ * terminal cursor).
107
+ */
108
+ caret?(ctx: WidgetContext): FieldCaret | undefined;
46
109
  /**
47
110
  * Whether this kind draws its bag's failure itself.
48
111
  *
@@ -50,8 +50,15 @@ export interface GaiaUiLauncherOptions {
50
50
  connection: HomeConnection;
51
51
  /** Repo/home root the launched agents run in. */
52
52
  cwd: string;
53
- /** Default project (opaque group), from the conductor registry. */
53
+ /** Default project (opaque group) — `gaia ui --project`, when given. */
54
54
  project?: string | undefined;
55
+ /**
56
+ * GAIA-232 (D9): the `.gaia` dir the entry is standing in. The renderer's
57
+ * `run()` resolves it against the control plane into the default `project`
58
+ * and `ownConductorMachineId`; the entry cannot, holding no JSON:API client.
59
+ * An explicit `project` above still wins.
60
+ */
61
+ gaiaDir?: string | undefined;
55
62
  /** Herdr-backed agent host, already adapted prompt→command (injected). */
56
63
  agents?: TuiAgentService | undefined;
57
64
  /** Preselected new-ticket assignee (this machine's user id). */
@@ -83,6 +90,6 @@ export type { UiInitialRoute, UiViewMode };
83
90
  * Build + run the dropsh `--format tui` program with the TUI renderer and the
84
91
  * injected agent host. A trivial `search gaia_project` seeds the renderer's
85
92
  * `run()`; the renderer discards the seed document and boots its own projects
86
- * screen from the conductor registry (`~/.gaia/conductors.json`).
93
+ * screen from the `gaia_conductor` records it reads through this connection.
87
94
  */
88
95
  export declare function runGaiaUi(opts: GaiaUiLauncherOptions): Promise<void>;
@@ -3,7 +3,7 @@ import gaiaUiPlugin from './plugin.js';
3
3
  * Build + run the dropsh `--format tui` program with the TUI renderer and the
4
4
  * injected agent host. A trivial `search gaia_project` seeds the renderer's
5
5
  * `run()`; the renderer discards the seed document and boots its own projects
6
- * screen from the conductor registry (`~/.gaia/conductors.json`).
6
+ * screen from the `gaia_conductor` records it reads through this connection.
7
7
  */
8
8
  export async function runGaiaUi(opts) {
9
9
  // Point dropsh's own in-process context factory at the home dropsh config so
@@ -22,6 +22,7 @@ export async function runGaiaUi(opts) {
22
22
  const renderer = makeRenderer({
23
23
  cwd: opts.cwd,
24
24
  ...(opts.project !== undefined ? { project: opts.project } : {}),
25
+ ...(opts.gaiaDir !== undefined ? { gaiaDir: opts.gaiaDir } : {}),
25
26
  ...(opts.agents ? { agents: opts.agents } : {}),
26
27
  ...(opts.currentUser !== undefined
27
28
  ? { currentUser: opts.currentUser }
@@ -12,10 +12,16 @@ export function sanitizeDisplay(value) {
12
12
  // map every C0 whitespace control (TAB, LF, VT, FF, CR) to a space — keeping
13
13
  // words separated — then strip the remaining C0/C1 controls. A single field can
14
14
  // therefore never carry an embedded newline/tab into a "single row".
15
- return (String(value)
15
+ //
16
+ // The ONE exception is SGR (`\x1b[…m`): it occupies no column and can neither
17
+ // move the cursor nor clear the screen, and the text field's inverse-video
18
+ // caret (GAIA-283) rides through this pipe on its way to the frame. Every
19
+ // OTHER escape sequence keeps losing its ESC and collapsing to inert text.
20
+ return String(value)
16
21
  .replace(/[\t\n\v\f\r]/g, ' ')
17
- // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping the C0/C1 control range is the whole point of this sanitizer.
18
- .replace(/[\x00-\x1f\x7f-\x9f]/g, ''));
22
+ .replace(
23
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping the C0/C1 control range (SGR excepted) is the whole point of this sanitizer.
24
+ /(\x1b\[[0-9;]*m)|[\x00-\x1f\x7f-\x9f]/g, (_m, sgr) => sgr ?? '');
19
25
  }
20
26
  // Fit a value to exactly `width` DISPLAY COLUMNS. Measuring code units instead
21
27
  // (`String.length`/`padEnd`) is one column short per wide character — `⚡` alone
@@ -60,8 +66,10 @@ export function fitLineKeptWidth(value, width) {
60
66
  return displayWidth(truncateToWidth(text, Math.max(0, width - 1)));
61
67
  }
62
68
  // Fit + style the visible window: selected row inverse, heading rows bold —
63
- // both applied AFTER fitLine() so the ANSI wraps the sanitized text, never the
64
- // reverse (fitting an ANSI-wrapped string strips the ESC and leaks CSI text).
69
+ // both applied AFTER fitLine() so the ANSI wraps whole fitted rows. (SGR also
70
+ // survives INSIDE a value now — sanitizeDisplay keeps it for the GAIA-283
71
+ // caret — but a style that must cover the row's PADDING still has to wrap the
72
+ // fitted row, which is what happens here.)
65
73
  //
66
74
  // INVARIANT (AC-4): every returned entry is exactly ONE physical row. Two things
67
75
  // make that true, and BOTH are needed:
@@ -0,0 +1,41 @@
1
+ import type { ClipboardOutcome, ClipboardWriter } from '../types.js';
2
+ /**
3
+ * GAIA-326 — what the operator READS after asking for a copy, and the one rule
4
+ * the wording follows: never claim more than the outcome does.
5
+ *
6
+ * WHY THERE IS FEEDBACK AT ALL. An OSC 52 write paints nothing. The escape
7
+ * leaves for the operator's terminal emulator and either lands in their system
8
+ * clipboard or does not, and the cockpit is never told which. A copy that
9
+ * silently succeeds is therefore indistinguishable from one that silently
10
+ * failed — so the footer states the outcome, every time, and states the four
11
+ * outcomes differently.
12
+ *
13
+ * WHY FOUR OUTCOMES AND NOT TWO. `@opentui/core@0.5.1` probes the terminal and
14
+ * reports `supported | unknown | unsupported`
15
+ * (`TerminalCapabilities.osc52_support`), and `Clipboard.copyToClipboardOSC52`
16
+ * refuses ONLY on `unsupported`. So an `unknown` terminal — one that never
17
+ * answered the probe, which is what a plain `TERM=xterm-256color` pty is —
18
+ * really does get the escape, and the copy really may have worked. Reporting
19
+ * that as `copied` would be a claim the cockpit cannot back; reporting it as a
20
+ * failure would be a lie in the other direction. It gets its own word.
21
+ *
22
+ * The wording stays SHORT because the kernel's `fitLine` truncates the footer
23
+ * from the right (`Kernel/frame.ts`), and the notice shares that row with the
24
+ * screen's own key hints.
25
+ */
26
+ export declare function clipboardStatus(outcome: ClipboardOutcome, what: string): string;
27
+ /**
28
+ * Put `text` in the operator's clipboard and return the line that says so.
29
+ *
30
+ * SECURITY, and the reason this takes the text as an argument rather than
31
+ * reaching for it: OSC 52 writes into the operator's REAL system clipboard,
32
+ * clobbering whatever was there. So it is only ever reached from a keystroke
33
+ * that explicitly asked for a copy — never from navigation, a reload, a resize
34
+ * or a draw — and it copies exactly the one thing that keystroke named.
35
+ *
36
+ * `absent` is what to say when the caller has nothing to copy (a ticket with no
37
+ * link because the session has no base URL). Nothing is written in that case:
38
+ * an empty OSC 52 write would silently WIPE the operator's clipboard, which is
39
+ * a worse outcome than the copy not happening.
40
+ */
41
+ export declare function copyThrough(writer: ClipboardWriter | undefined, text: string | undefined, what: string, absent: string): string;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * GAIA-326 — what the operator READS after asking for a copy, and the one rule
3
+ * the wording follows: never claim more than the outcome does.
4
+ *
5
+ * WHY THERE IS FEEDBACK AT ALL. An OSC 52 write paints nothing. The escape
6
+ * leaves for the operator's terminal emulator and either lands in their system
7
+ * clipboard or does not, and the cockpit is never told which. A copy that
8
+ * silently succeeds is therefore indistinguishable from one that silently
9
+ * failed — so the footer states the outcome, every time, and states the four
10
+ * outcomes differently.
11
+ *
12
+ * WHY FOUR OUTCOMES AND NOT TWO. `@opentui/core@0.5.1` probes the terminal and
13
+ * reports `supported | unknown | unsupported`
14
+ * (`TerminalCapabilities.osc52_support`), and `Clipboard.copyToClipboardOSC52`
15
+ * refuses ONLY on `unsupported`. So an `unknown` terminal — one that never
16
+ * answered the probe, which is what a plain `TERM=xterm-256color` pty is —
17
+ * really does get the escape, and the copy really may have worked. Reporting
18
+ * that as `copied` would be a claim the cockpit cannot back; reporting it as a
19
+ * failure would be a lie in the other direction. It gets its own word.
20
+ *
21
+ * The wording stays SHORT because the kernel's `fitLine` truncates the footer
22
+ * from the right (`Kernel/frame.ts`), and the notice shares that row with the
23
+ * screen's own key hints.
24
+ */
25
+ export function clipboardStatus(outcome, what) {
26
+ switch (outcome) {
27
+ case 'copied':
28
+ return `copied ${what}`;
29
+ case 'unconfirmed':
30
+ // Deliberately still opens with `copied`: the escape WAS sent, and on
31
+ // Ghostty — where the operator actually is — it works. The qualifier is
32
+ // what stops it being a promise. Kept to four words because it shares the
33
+ // footer with the screen's key ladder; the long version pushed `q quit`
34
+ // off a 120-column terminal, measured.
35
+ return `copied ${what} · OSC 52 unconfirmed`;
36
+ case 'unsupported':
37
+ return 'not copied · this terminal refuses OSC 52 clipboard writes';
38
+ default:
39
+ return 'not copied · the OSC 52 write failed';
40
+ }
41
+ }
42
+ /**
43
+ * Put `text` in the operator's clipboard and return the line that says so.
44
+ *
45
+ * SECURITY, and the reason this takes the text as an argument rather than
46
+ * reaching for it: OSC 52 writes into the operator's REAL system clipboard,
47
+ * clobbering whatever was there. So it is only ever reached from a keystroke
48
+ * that explicitly asked for a copy — never from navigation, a reload, a resize
49
+ * or a draw — and it copies exactly the one thing that keystroke named.
50
+ *
51
+ * `absent` is what to say when the caller has nothing to copy (a ticket with no
52
+ * link because the session has no base URL). Nothing is written in that case:
53
+ * an empty OSC 52 write would silently WIPE the operator's clipboard, which is
54
+ * a worse outcome than the copy not happening.
55
+ */
56
+ export function copyThrough(writer, text, what, absent) {
57
+ if (text === undefined || text === '')
58
+ return absent;
59
+ // No writer means no terminal — the non-interactive `gaia ui` path, which
60
+ // writes plain lines to a pipe and never sees a keystroke. Stated rather than
61
+ // silently treated as a failure of the terminal's.
62
+ if (writer === undefined)
63
+ return 'not copied · no terminal to copy through';
64
+ return clipboardStatus(writer(text), what);
65
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The list selection cursor: an index plus the four move keys (GAIA-294).
3
+ *
4
+ * Extracted from FOUR verbatim copies — `ticket/Screen/tickets.ts`,
5
+ * `run/Screen/runs.ts`, `project/Screen/projects-screen.ts` and
6
+ * `ticket/Screen/ticket-agents.ts`. It is a plain composed object, not a base
7
+ * class or a mixin, because two of those four are `AsyncScreen<Row[]>` while the
8
+ * other two are `AsyncScreen<Page<Row>>`: no single base class spans them, and
9
+ * putting the index on `AsyncScreen` itself would hand a `selectedIndex` to every
10
+ * form, detail and dashboard screen that has no rows.
11
+ *
12
+ * It claims the move keys and NOTHING else — not `Enter`, which resolves to a
13
+ * per-screen `activate()`, and not the LEFT/RIGHT arrows, which page. So a caller
14
+ * keeps its own dispatch order simply by calling `handle` where its move keys used
15
+ * to be.
16
+ */
17
+ export declare class ListCursor {
18
+ /** The selected row, or -1 on an empty list (see `handle`). */
19
+ index: number;
20
+ /**
21
+ * Apply `input` as a cursor move over `length` rows; false when it is not a
22
+ * cursor key, so the caller's `else if` chain continues unchanged.
23
+ *
24
+ * The clamping arithmetic is copied EXACTLY from the four call sites, including
25
+ * `Math.min(index + 1, length - 1)` yielding -1 when `length` is 0. That is
26
+ * today's behaviour, `present()` maps it back to -1, and GAIA-294 is
27
+ * behaviour-neutral — fixing it here would be a change smuggled into a refactor.
28
+ */
29
+ handle(input: string, length: number): boolean;
30
+ reset(): void;
31
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The list selection cursor: an index plus the four move keys (GAIA-294).
3
+ *
4
+ * Extracted from FOUR verbatim copies — `ticket/Screen/tickets.ts`,
5
+ * `run/Screen/runs.ts`, `project/Screen/projects-screen.ts` and
6
+ * `ticket/Screen/ticket-agents.ts`. It is a plain composed object, not a base
7
+ * class or a mixin, because two of those four are `AsyncScreen<Row[]>` while the
8
+ * other two are `AsyncScreen<Page<Row>>`: no single base class spans them, and
9
+ * putting the index on `AsyncScreen` itself would hand a `selectedIndex` to every
10
+ * form, detail and dashboard screen that has no rows.
11
+ *
12
+ * It claims the move keys and NOTHING else — not `Enter`, which resolves to a
13
+ * per-screen `activate()`, and not the LEFT/RIGHT arrows, which page. So a caller
14
+ * keeps its own dispatch order simply by calling `handle` where its move keys used
15
+ * to be.
16
+ */
17
+ export class ListCursor {
18
+ /** The selected row, or -1 on an empty list (see `handle`). */
19
+ index = 0;
20
+ /**
21
+ * Apply `input` as a cursor move over `length` rows; false when it is not a
22
+ * cursor key, so the caller's `else if` chain continues unchanged.
23
+ *
24
+ * The clamping arithmetic is copied EXACTLY from the four call sites, including
25
+ * `Math.min(index + 1, length - 1)` yielding -1 when `length` is 0. That is
26
+ * today's behaviour, `present()` maps it back to -1, and GAIA-294 is
27
+ * behaviour-neutral — fixing it here would be a change smuggled into a refactor.
28
+ */
29
+ handle(input, length) {
30
+ if (input === 'j' || input === 'down')
31
+ this.index = Math.min(this.index + 1, length - 1);
32
+ else if (input === 'k' || input === 'up')
33
+ this.index = Math.max(this.index - 1, 0);
34
+ else if (input === 'g')
35
+ this.index = 0;
36
+ else if (input === 'G')
37
+ this.index = Math.max(0, length - 1);
38
+ else
39
+ return false;
40
+ return true;
41
+ }
42
+ reset() {
43
+ this.index = 0;
44
+ }
45
+ }
@@ -1,16 +1,3 @@
1
- /**
2
- * A value fitted to exactly `width` DISPLAY COLUMNS — the unit a terminal lays
3
- * out in, not UTF-16 code units. Padding by `String.length` drifts one column
4
- * per wide character, which both misaligns table columns and (once the row is
5
- * assembled with borders) overflows the line into a second physical row.
6
- *
7
- * The value is SANITIZED before it is measured, and that ordering is the point:
8
- * every row `pad` builds passes through `fitLine`, which sanitizes too — mapping
9
- * TAB/LF/CR to a space and stripping the rest of the C0/C1 range. Measuring the
10
- * raw value counted a TAB as zero columns here and one column there, so the field
11
- * came out a column off and every later column of the table drifted with it.
12
- */
13
- export declare function pad(value: unknown, width: number): string;
14
1
  /** Read a value as a string, or `undefined` when it is not one. */
15
2
  export declare function asString(value: unknown): string | undefined;
16
3
  /**
@@ -22,3 +9,24 @@ export declare function asString(value: unknown): string | undefined;
22
9
  * removes those two cross-entity edges instead of grandfathering them.
23
10
  */
24
11
  export declare function humanizeAge(lastSeenSeconds: number | null, nowSeconds: number): string;
12
+ /**
13
+ * Parse a human duration into SECONDS, or `null` when it is not one.
14
+ *
15
+ * Accepts `1d 2h 30m`, `1h30m`, `3h`, `90m` and a BARE INTEGER read as minutes
16
+ * — the last because the common case in a popup is typing a number, and every
17
+ * other tracker reads it the same way. Storage is seconds everywhere else
18
+ * (GAIA-308 D5); this grammar exists only at the terminal edge.
19
+ *
20
+ * An EMPTY input is 0, not null: clearing the field is a real edit, and the
21
+ * caller needs to tell "the operator cleared it" from "the operator typed
22
+ * nonsense". A negative or unknown-unit input is nonsense and yields null.
23
+ */
24
+ export declare function parseDuration(input: string): number | null;
25
+ /**
26
+ * Render SECONDS as a compact `1d 2h 30m`, largest unit first.
27
+ *
28
+ * Zero-valued leading units are dropped; a zero total reads `0m` rather than an
29
+ * empty string, so a deliberate zero is never mistaken for an absent value —
30
+ * which is `—`, the same em dash `humanizeAge` uses for an unknown timestamp.
31
+ */
32
+ export declare function formatDuration(seconds: number | null | undefined): string;
@@ -1,31 +1,20 @@
1
- import { sanitizeDisplay } from './ansi.js';
2
- import { displayWidth, truncateToWidth } from './width.js';
3
- /**
4
- * A value fitted to exactly `width` DISPLAY COLUMNS — the unit a terminal lays
5
- * out in, not UTF-16 code units. Padding by `String.length` drifts one column
6
- * per wide character, which both misaligns table columns and (once the row is
7
- * assembled with borders) overflows the line into a second physical row.
8
- *
9
- * The value is SANITIZED before it is measured, and that ordering is the point:
10
- * every row `pad` builds passes through `fitLine`, which sanitizes too — mapping
11
- * TAB/LF/CR to a space and stripping the rest of the C0/C1 range. Measuring the
12
- * raw value counted a TAB as zero columns here and one column there, so the field
13
- * came out a column off and every later column of the table drifted with it.
14
- */
15
- export function pad(value, width) {
16
- // As in `fitLine`: a zero budget renders nothing, not a lone ellipsis.
17
- if (width <= 0)
18
- return '';
19
- const s = value == null ? '' : sanitizeDisplay(value);
20
- const columns = displayWidth(s);
21
- if (columns <= width)
22
- return s + ' '.repeat(Math.max(0, width - columns));
23
- // Truncation may stop a column short rather than split a wide character, so
24
- // pad whatever the ellipsis does not fill.
25
- const head = truncateToWidth(s, Math.max(0, width - 1));
26
- const out = `${head}…`;
27
- return out + ' '.repeat(Math.max(0, width - displayWidth(out)));
28
- }
1
+ // GAIA-340 Task 13 — `pad` AND `sanitizeDisplay` USED TO LIVE HERE, and are
2
+ // deliberately gone.
3
+ //
4
+ // `pad` fitted a value to a field of exactly N DISPLAY COLUMNS, which is why it
5
+ // reached `lib/width.ts` — and `sanitizeDisplay` existed only to make that
6
+ // measurement honest (a TAB counted as zero columns where it rendered as one, so
7
+ // the field came out a column short and every later column of the table drifted).
8
+ // Its two callers were the dashboard's bars and the ticket detail's Runs table,
9
+ // both of which composed their columns into ONE STRING because the surface they
10
+ // fed was a pipe as well as a screen.
11
+ //
12
+ // Neither composes now: each states its cells and their Yoga budgets
13
+ // (`lib/vnode.ts`'s `Cell`), the renderer places the values, and the pipe prints
14
+ // them one space apart and unaligned. That last part is a real change to what
15
+ // `gaia ui … | cat` emits, made with the operator's approval and re-pinned in
16
+ // `docs/evidence/GAIA-340/`; it is what takes the last display-column
17
+ // measurement off the non-TTY path (R5/D8).
29
18
  /** Read a value as a string, or `undefined` when it is not one. */
30
19
  export function asString(value) {
31
20
  return typeof value === 'string' ? value : undefined;
@@ -50,3 +39,62 @@ export function humanizeAge(lastSeenSeconds, nowSeconds) {
50
39
  return `${Math.round(delta / 3600)}h ago`;
51
40
  return `${Math.round(delta / 86_400)}d ago`;
52
41
  }
42
+ /** Seconds per accepted unit suffix, largest first — the order both functions read. */
43
+ const DURATION_UNITS = [
44
+ ['d', 86_400],
45
+ ['h', 3600],
46
+ ['m', 60],
47
+ ];
48
+ /**
49
+ * Parse a human duration into SECONDS, or `null` when it is not one.
50
+ *
51
+ * Accepts `1d 2h 30m`, `1h30m`, `3h`, `90m` and a BARE INTEGER read as minutes
52
+ * — the last because the common case in a popup is typing a number, and every
53
+ * other tracker reads it the same way. Storage is seconds everywhere else
54
+ * (GAIA-308 D5); this grammar exists only at the terminal edge.
55
+ *
56
+ * An EMPTY input is 0, not null: clearing the field is a real edit, and the
57
+ * caller needs to tell "the operator cleared it" from "the operator typed
58
+ * nonsense". A negative or unknown-unit input is nonsense and yields null.
59
+ */
60
+ export function parseDuration(input) {
61
+ const text = input.trim().toLowerCase();
62
+ if (text === '')
63
+ return 0;
64
+ if (/^\d+$/.test(text))
65
+ return Number(text) * 60;
66
+ // One or more `<digits><unit>` groups, whitespace optional between them, and
67
+ // nothing else — anchored, so `1x` and `-5m` fall through to null.
68
+ if (!/^(\d+[dhm]\s*)+$/.test(text))
69
+ return null;
70
+ let seconds = 0;
71
+ for (const [, amount, unit] of text.matchAll(/(\d+)([dhm])/g)) {
72
+ const factor = DURATION_UNITS.find(([suffix]) => suffix === unit)?.[1];
73
+ if (factor === undefined)
74
+ return null;
75
+ seconds += Number(amount) * factor;
76
+ }
77
+ return seconds;
78
+ }
79
+ /**
80
+ * Render SECONDS as a compact `1d 2h 30m`, largest unit first.
81
+ *
82
+ * Zero-valued leading units are dropped; a zero total reads `0m` rather than an
83
+ * empty string, so a deliberate zero is never mistaken for an absent value —
84
+ * which is `—`, the same em dash `humanizeAge` uses for an unknown timestamp.
85
+ */
86
+ export function formatDuration(seconds) {
87
+ if (seconds == null)
88
+ return '—';
89
+ if (seconds <= 0)
90
+ return '0m';
91
+ let rest = Math.floor(seconds);
92
+ const parts = [];
93
+ for (const [suffix, factor] of DURATION_UNITS) {
94
+ const value = Math.floor(rest / factor);
95
+ if (value > 0)
96
+ parts.push(`${value}${suffix}`);
97
+ rest -= value * factor;
98
+ }
99
+ return parts.length > 0 ? parts.join(' ') : '0m';
100
+ }