@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.
- package/dist/src/Agent/launch.d.ts +4 -4
- package/dist/src/Agent/launch.js +14 -7
- package/dist/src/Component/section-list/index.d.ts +117 -8
- package/dist/src/Component/section-list/index.js +118 -46
- package/dist/src/Component/stats-panel/index.d.ts +17 -5
- package/dist/src/Component/stats-panel/index.js +50 -52
- package/dist/src/Component/top-tabs/index.d.ts +95 -23
- package/dist/src/Component/top-tabs/index.js +87 -41
- package/dist/src/Entity/collection.d.ts +279 -0
- package/dist/src/Entity/collection.js +377 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.d.ts +25 -0
- package/dist/src/Entity/conductor/Component/conductor-list/index.js +60 -0
- package/dist/src/Entity/conductor/Data/conductors.d.ts +216 -0
- package/dist/src/Entity/conductor/Data/conductors.js +280 -0
- package/dist/src/Entity/conductor/Data/liveness.d.ts +2 -1
- package/dist/src/Entity/conductor/Data/liveness.js +12 -16
- package/dist/src/Entity/conductor/Data/log-tail.d.ts +25 -0
- package/dist/src/Entity/conductor/Data/log-tail.js +133 -0
- package/dist/src/Entity/conductor/Data/own-conductor.d.ts +30 -0
- package/dist/src/Entity/conductor/Data/own-conductor.js +73 -0
- package/dist/src/Entity/conductor/Data/process.d.ts +78 -0
- package/dist/src/Entity/conductor/Data/process.js +101 -0
- package/dist/src/Entity/conductor/Screen/conductors.d.ts +67 -0
- package/dist/src/Entity/conductor/Screen/conductors.js +233 -0
- package/dist/src/Entity/cycle.d.ts +18 -0
- package/dist/src/Entity/cycle.js +30 -0
- package/dist/src/Entity/project/Data/project.d.ts +8 -0
- package/dist/src/Entity/project/Data/project.js +12 -0
- package/dist/src/Entity/project/Data/projects.d.ts +121 -0
- package/dist/src/Entity/project/Data/projects.js +115 -0
- package/dist/src/Entity/project/Data/with-tickets.d.ts +5 -0
- package/dist/src/Entity/project/Data/with-tickets.js +15 -13
- package/dist/src/Entity/project/Screen/project-picker.d.ts +80 -0
- package/dist/src/Entity/project/Screen/project-picker.js +191 -0
- package/dist/src/Entity/project/Screen/projects-screen.d.ts +52 -0
- package/dist/src/Entity/project/Screen/projects-screen.js +122 -0
- package/dist/src/Entity/run/Component/run-detail/index.d.ts +26 -10
- package/dist/src/Entity/run/Component/run-detail/index.js +147 -39
- package/dist/src/Entity/run/Component/run-table/index.d.ts +5 -7
- package/dist/src/Entity/run/Component/run-table/index.js +9 -18
- package/dist/src/Entity/run/Data/query.d.ts +22 -0
- package/dist/src/Entity/run/Data/query.js +41 -0
- package/dist/src/Entity/run/Data/runs.d.ts +192 -4
- package/dist/src/Entity/run/Data/runs.js +188 -27
- package/dist/src/Entity/run/Screen/run-detail.d.ts +53 -0
- package/dist/src/Entity/run/Screen/run-detail.js +70 -0
- package/dist/src/Entity/run/Screen/runs.d.ts +86 -0
- package/dist/src/Entity/run/Screen/runs.js +169 -0
- package/dist/src/Entity/statistics/Screen/dashboard.d.ts +78 -0
- package/dist/src/Entity/statistics/Screen/dashboard.js +228 -0
- package/dist/src/Entity/ticket/Component/comment-item/index.d.ts +12 -9
- package/dist/src/Entity/ticket/Component/comment-item/index.js +18 -13
- package/dist/src/Entity/ticket/Component/create-form/index.d.ts +16 -3
- package/dist/src/Entity/ticket/Component/create-form/index.js +28 -14
- package/dist/src/Entity/ticket/Component/edit-form/index.d.ts +11 -6
- package/dist/src/Entity/ticket/Component/edit-form/index.js +77 -29
- package/dist/src/Entity/ticket/Component/sections/index.d.ts +8 -0
- package/dist/src/Entity/ticket/Component/sections/index.js +42 -0
- package/dist/src/Entity/ticket/Component/tab-bar/index.js +6 -0
- package/dist/src/Entity/ticket/Component/ticket-detail/index.d.ts +65 -8
- package/dist/src/Entity/ticket/Component/ticket-detail/index.js +171 -84
- package/dist/src/Entity/ticket/Component/ticket-table/index.d.ts +4 -2
- package/dist/src/Entity/ticket/Component/ticket-table/index.js +8 -12
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.d.ts +47 -3
- package/dist/src/Entity/ticket/Component/ticket-teaser/index.js +76 -22
- package/dist/src/Entity/ticket/Data/parents.d.ts +13 -1
- package/dist/src/Entity/ticket/Data/parents.js +64 -12
- package/dist/src/Entity/ticket/Data/query.d.ts +44 -7
- package/dist/src/Entity/ticket/Data/query.js +60 -26
- package/dist/src/Entity/ticket/Data/tickets.d.ts +170 -4
- package/dist/src/Entity/ticket/Data/tickets.js +200 -12
- package/dist/src/Entity/ticket/Data/url.d.ts +16 -0
- package/dist/src/Entity/ticket/Data/url.js +20 -0
- package/dist/src/Entity/ticket/Data/workflow.d.ts +1 -5
- package/dist/src/Entity/ticket/Data/workflow.js +5 -13
- package/dist/src/Entity/ticket/Data/write.d.ts +32 -0
- package/dist/src/Entity/ticket/Data/write.js +80 -0
- package/dist/src/Entity/ticket/Form/create-data.d.ts +1 -11
- package/dist/src/Entity/ticket/Form/create-data.js +7 -36
- package/dist/src/Entity/ticket/Form/create.d.ts +45 -5
- package/dist/src/Entity/ticket/Form/create.js +45 -8
- package/dist/src/Entity/ticket/Form/edit-data.d.ts +1 -1
- package/dist/src/Entity/ticket/Form/edit-data.js +4 -0
- package/dist/src/Entity/ticket/Form/edit.d.ts +79 -12
- package/dist/src/Entity/ticket/Form/edit.js +165 -18
- package/dist/src/Entity/ticket/Form/keys.d.ts +7 -0
- package/dist/src/Entity/ticket/Form/keys.js +7 -0
- package/dist/src/Entity/ticket/Form/parent-filter.d.ts +36 -0
- package/dist/src/Entity/ticket/Form/parent-filter.js +37 -0
- package/dist/src/Entity/ticket/Form/parent-source.d.ts +27 -0
- package/dist/src/Entity/ticket/Form/parent-source.js +56 -0
- package/dist/src/Entity/ticket/Form/who-filter.d.ts +33 -0
- package/dist/src/Entity/ticket/Form/who-filter.js +33 -0
- package/dist/src/Entity/ticket/Form/who-source.d.ts +18 -0
- package/dist/src/Entity/ticket/Form/who-source.js +38 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.d.ts +81 -0
- package/dist/src/Entity/ticket/Screen/create-ticket-form.js +356 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.d.ts +92 -0
- package/dist/src/Entity/ticket/Screen/edit-assignment-form.js +220 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.d.ts +44 -0
- package/dist/src/Entity/ticket/Screen/parent-filter-form.js +200 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.d.ts +136 -0
- package/dist/src/Entity/ticket/Screen/ticket-agents.js +257 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.d.ts +125 -0
- package/dist/src/Entity/ticket/Screen/ticket-detail.js +415 -0
- package/dist/src/Entity/ticket/Screen/tickets.d.ts +145 -0
- package/dist/src/Entity/ticket/Screen/tickets.js +538 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.d.ts +41 -0
- package/dist/src/Entity/ticket/Screen/who-filter-form.js +174 -0
- package/dist/src/Entity/user/Data/users.d.ts +17 -0
- package/dist/src/Entity/user/Data/users.js +66 -6
- package/dist/src/Form/field.d.ts +3 -1
- package/dist/src/Form/field.js +1 -0
- package/dist/src/Form/form.d.ts +1 -1
- package/dist/src/Form/form.js +61 -9
- package/dist/src/Form/keys.d.ts +23 -1
- package/dist/src/Form/keys.js +78 -23
- package/dist/src/Kernel/chrome.d.ts +289 -0
- package/dist/src/Kernel/chrome.js +404 -0
- package/dist/src/Kernel/clipboard.d.ts +30 -0
- package/dist/src/Kernel/clipboard.js +51 -0
- package/dist/src/Kernel/key-input.d.ts +50 -0
- package/dist/src/Kernel/key-input.js +158 -0
- package/dist/src/Kernel/mount.d.ts +187 -0
- package/dist/src/Kernel/mount.js +399 -0
- package/dist/src/Kernel/tui-kernel.d.ts +0 -11
- package/dist/src/Kernel/tui-kernel.js +778 -2367
- package/dist/src/Routing/context.d.ts +31 -0
- package/dist/src/Routing/context.js +52 -0
- package/dist/src/Routing/registry.d.ts +19 -0
- package/dist/src/Routing/registry.js +97 -0
- package/dist/src/Routing/route.d.ts +35 -0
- package/dist/src/Routing/route.js +1 -0
- package/dist/src/Routing/router.d.ts +49 -0
- package/dist/src/Routing/router.js +123 -0
- package/dist/src/Routing/routes.d.ts +17 -0
- package/dist/src/Routing/routes.js +57 -0
- package/dist/src/Screen/async-screen.d.ts +87 -0
- package/dist/src/Screen/async-screen.js +206 -0
- package/dist/src/Screen/list-screen.d.ts +193 -0
- package/dist/src/Screen/list-screen.js +261 -0
- package/dist/src/Screen/screen.d.ts +240 -0
- package/dist/src/Screen/screen.js +34 -0
- package/dist/src/Tui/controller.d.ts +13 -0
- package/dist/src/Tui/controller.js +1794 -0
- package/dist/src/Widget/autocomplete.js +75 -3
- package/dist/src/Widget/index.d.ts +3 -2
- package/dist/src/Widget/index.js +2 -1
- package/dist/src/Widget/option-source.d.ts +12 -1
- package/dist/src/Widget/option-source.js +14 -1
- package/dist/src/Widget/render.d.ts +23 -8
- package/dist/src/Widget/render.js +108 -41
- package/dist/src/Widget/text.js +94 -5
- package/dist/src/Widget/textarea.d.ts +45 -0
- package/dist/src/Widget/textarea.js +115 -0
- package/dist/src/Widget/types.d.ts +64 -1
- package/dist/src/launcher.d.ts +9 -2
- package/dist/src/launcher.js +2 -1
- package/dist/src/lib/ansi.js +13 -5
- package/dist/src/lib/clipboard.d.ts +41 -0
- package/dist/src/lib/clipboard.js +65 -0
- package/dist/src/lib/cursor.d.ts +31 -0
- package/dist/src/lib/cursor.js +45 -0
- package/dist/src/lib/format.d.ts +21 -13
- package/dist/src/lib/format.js +76 -28
- package/dist/src/lib/hyperlink.d.ts +58 -0
- package/dist/src/lib/hyperlink.js +104 -0
- package/dist/src/lib/input-trace.d.ts +48 -0
- package/dist/src/lib/input-trace.js +47 -0
- package/dist/src/lib/key-vocabulary.d.ts +19 -0
- package/dist/src/lib/key-vocabulary.js +25 -0
- package/dist/src/lib/table.d.ts +37 -0
- package/dist/src/lib/table.js +20 -0
- package/dist/src/lib/vnode.d.ts +217 -0
- package/dist/src/lib/vnode.js +456 -0
- package/dist/src/lib/width.d.ts +25 -1
- package/dist/src/lib/width.js +93 -12
- package/dist/src/plugin.js +26 -1
- package/dist/src/types.d.ts +67 -2
- package/dist/src/types.js +0 -4
- 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
|
|
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
|
*
|
package/dist/src/launcher.d.ts
CHANGED
|
@@ -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)
|
|
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
|
|
93
|
+
* screen from the `gaia_conductor` records it reads through this connection.
|
|
87
94
|
*/
|
|
88
95
|
export declare function runGaiaUi(opts: GaiaUiLauncherOptions): Promise<void>;
|
package/dist/src/launcher.js
CHANGED
|
@@ -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
|
|
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 }
|
package/dist/src/lib/ansi.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
|
64
|
-
//
|
|
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
|
+
}
|
package/dist/src/lib/format.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/lib/format.js
CHANGED
|
@@ -1,31 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
}
|