@timurproko/a1 0.1.1-dev.6 → 0.1.1-dev.7

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 (151) hide show
  1. package/README.md +32 -9
  2. package/bin/a1-guardian.js +22 -0
  3. package/bin/a1-ui.js +7 -4
  4. package/dist/native/darwin-arm64/a1-process-guardian +0 -0
  5. package/dist/native/darwin-arm64/manifest.json +20 -0
  6. package/dist/native/linux-x64/a1-process-guardian +0 -0
  7. package/dist/native/linux-x64/manifest.json +20 -0
  8. package/dist/native/win32-x64/a1-process-guardian.exe +0 -0
  9. package/dist/native/win32-x64/manifest.json +20 -0
  10. package/dist/src/composition/index.d.ts +26 -0
  11. package/dist/src/composition/index.js +88 -2
  12. package/dist/src/features/launch/runtime-selection.d.ts +2 -1
  13. package/dist/src/features/launch/runtime-selection.js +2 -2
  14. package/dist/src/features/owned-ui/index.d.ts +1 -0
  15. package/dist/src/features/owned-ui/index.js +1 -0
  16. package/dist/src/features/owned-ui/run.d.ts +6 -0
  17. package/dist/src/features/owned-ui/run.js +3 -1
  18. package/dist/src/features/owned-ui/settings-app.d.ts +22 -0
  19. package/dist/src/features/owned-ui/settings-app.js +631 -0
  20. package/dist/src/foundation/agent-engine-contracts/domain.d.ts +26 -0
  21. package/dist/src/foundation/launch-guardian/index.d.ts +1 -0
  22. package/dist/src/foundation/launch-guardian/index.js +1 -0
  23. package/dist/src/foundation/launch-guardian/main.d.ts +31 -0
  24. package/dist/src/foundation/launch-guardian/main.js +165 -0
  25. package/dist/src/foundation/lifecycle/commands.d.ts +40 -0
  26. package/dist/src/foundation/lifecycle/commands.js +1 -0
  27. package/dist/src/foundation/lifecycle/index.d.ts +2 -0
  28. package/dist/src/foundation/lifecycle/index.js +2 -0
  29. package/dist/src/foundation/lifecycle/launch-instance.d.ts +46 -0
  30. package/dist/src/foundation/lifecycle/launch-instance.js +134 -0
  31. package/dist/src/foundation/lifecycle/model.d.ts +6 -42
  32. package/dist/src/foundation/owned-ui-settings/declarations.d.ts +13 -0
  33. package/dist/src/foundation/owned-ui-settings/declarations.js +39 -0
  34. package/dist/src/foundation/owned-ui-settings/index.d.ts +6 -0
  35. package/dist/src/foundation/owned-ui-settings/index.js +6 -0
  36. package/dist/src/foundation/owned-ui-settings/migrations.d.ts +13 -0
  37. package/dist/src/foundation/owned-ui-settings/migrations.js +28 -0
  38. package/dist/src/foundation/owned-ui-settings/resolution.d.ts +45 -0
  39. package/dist/src/foundation/owned-ui-settings/resolution.js +147 -0
  40. package/dist/src/foundation/owned-ui-settings/sections.d.ts +70 -0
  41. package/dist/src/foundation/owned-ui-settings/sections.js +93 -0
  42. package/dist/src/foundation/owned-ui-settings/session.d.ts +44 -0
  43. package/dist/src/foundation/owned-ui-settings/session.js +145 -0
  44. package/dist/src/foundation/owned-ui-settings/store.d.ts +28 -0
  45. package/dist/src/foundation/owned-ui-settings/store.js +90 -0
  46. package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -0
  47. package/dist/src/foundation/pi-component-adapter/index.js +1 -0
  48. package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.d.ts +9 -1
  49. package/dist/src/foundation/pi-component-adapter/shell-selectors-dialogs.js +10 -8
  50. package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +12 -3
  51. package/dist/src/foundation/pi-engine-adapter/adapter.js +73 -7
  52. package/dist/src/foundation/pi-engine-adapter/pi-settings-metadata.json +374 -0
  53. package/dist/src/foundation/pi-engine-adapter/public-main-entry.js +10 -1
  54. package/dist/src/foundation/pi-engine-adapter/settings-integration.d.ts +42 -2
  55. package/dist/src/foundation/pi-engine-adapter/settings-integration.js +185 -24
  56. package/dist/src/foundation/pi-engine-adapter/workflows.d.ts +9 -0
  57. package/dist/src/foundation/pi-owned-ui-integration/index.d.ts +1 -0
  58. package/dist/src/foundation/pi-owned-ui-integration/index.js +1 -0
  59. package/dist/src/foundation/pi-owned-ui-integration/route-host.d.ts +29 -0
  60. package/dist/src/foundation/pi-owned-ui-integration/route-host.js +1 -0
  61. package/dist/src/foundation/pi-owned-ui-integration/session-shell.d.ts +6 -0
  62. package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +71 -1
  63. package/dist/src/foundation/pi-tui-runtime-adapter/adapter.d.ts +6 -0
  64. package/dist/src/foundation/pi-tui-runtime-adapter/adapter.js +9 -0
  65. package/dist/src/foundation/process-containment/artifact.d.ts +1 -0
  66. package/dist/src/foundation/process-containment/artifact.js +34 -0
  67. package/dist/src/foundation/process-containment/contracts.d.ts +31 -0
  68. package/dist/src/foundation/process-containment/contracts.js +35 -0
  69. package/dist/src/foundation/process-containment/index.d.ts +5 -0
  70. package/dist/src/foundation/process-containment/index.js +5 -0
  71. package/dist/src/foundation/process-containment/linux-process-inspector.d.ts +6 -0
  72. package/dist/src/foundation/process-containment/linux-process-inspector.js +26 -0
  73. package/dist/src/foundation/process-containment/native-guardian-containment.d.ts +16 -0
  74. package/dist/src/foundation/process-containment/native-guardian-containment.js +139 -0
  75. package/dist/src/foundation/process-containment/windows-process-inspector.d.ts +17 -0
  76. package/dist/src/foundation/process-containment/windows-process-inspector.js +53 -0
  77. package/dist/src/foundation/protocol/client.d.ts +2 -1
  78. package/dist/src/foundation/protocol/client.js +3 -0
  79. package/dist/src/foundation/protocol/messages.d.ts +5 -5
  80. package/dist/src/foundation/protocol/messages.js +1 -1
  81. package/dist/src/foundation/release/bootstrap.js +29 -10
  82. package/dist/src/foundation/release/cohort-selection.js +3 -3
  83. package/dist/src/foundation/release/cohort-state.d.ts +3 -2
  84. package/dist/src/foundation/release/process-cleanup.d.ts +1 -1
  85. package/dist/src/foundation/release/process-cleanup.js +2 -2
  86. package/dist/src/foundation/release/update.js +2 -2
  87. package/dist/src/foundation/storage/control-store.d.ts +8 -7
  88. package/dist/src/foundation/storage/control-store.js +105 -55
  89. package/dist/src/foundation/supervision/server.d.ts +4 -2
  90. package/dist/src/foundation/supervision/server.js +254 -87
  91. package/dist/src/foundation/transparent-terminal/command-resolution.js +6 -3
  92. package/dist/src/foundation/transparent-terminal/foreground-broker.d.ts +5 -13
  93. package/dist/src/foundation/transparent-terminal/foreground-broker.js +11 -68
  94. package/dist/src/foundation/transparent-terminal/main.js +1 -8
  95. package/dist/src/foundation/ui-apps/contracts.d.ts +42 -0
  96. package/dist/src/foundation/ui-apps/contracts.js +12 -0
  97. package/dist/src/foundation/ui-apps/host.d.ts +42 -0
  98. package/dist/src/foundation/ui-apps/host.js +152 -0
  99. package/dist/src/foundation/ui-apps/index.d.ts +3 -0
  100. package/dist/src/foundation/ui-apps/index.js +3 -0
  101. package/dist/src/foundation/ui-apps/registry.d.ts +11 -0
  102. package/dist/src/foundation/ui-apps/registry.js +32 -0
  103. package/dist/src/foundation/ui-components/dialog-panel.d.ts +36 -0
  104. package/dist/src/foundation/ui-components/dialog-panel.js +47 -0
  105. package/dist/src/foundation/ui-components/frame.d.ts +21 -0
  106. package/dist/src/foundation/ui-components/frame.js +54 -0
  107. package/dist/src/foundation/ui-components/index.d.ts +18 -0
  108. package/dist/src/foundation/ui-components/index.js +18 -0
  109. package/dist/src/foundation/ui-components/label.d.ts +8 -0
  110. package/dist/src/foundation/ui-components/label.js +36 -0
  111. package/dist/src/foundation/ui-components/line-input.d.ts +74 -0
  112. package/dist/src/foundation/ui-components/line-input.js +245 -0
  113. package/dist/src/foundation/ui-components/list-block.d.ts +66 -0
  114. package/dist/src/foundation/ui-components/list-block.js +176 -0
  115. package/dist/src/foundation/ui-components/list-view.d.ts +66 -0
  116. package/dist/src/foundation/ui-components/list-view.js +76 -0
  117. package/dist/src/foundation/ui-components/mouse.d.ts +15 -0
  118. package/dist/src/foundation/ui-components/mouse.js +46 -0
  119. package/dist/src/foundation/ui-components/pane.d.ts +33 -0
  120. package/dist/src/foundation/ui-components/pane.js +7 -0
  121. package/dist/src/foundation/ui-components/revision.d.ts +35 -0
  122. package/dist/src/foundation/ui-components/revision.js +79 -0
  123. package/dist/src/foundation/ui-components/scrollbar.d.ts +62 -0
  124. package/dist/src/foundation/ui-components/scrollbar.js +102 -0
  125. package/dist/src/foundation/ui-components/shortcuts.d.ts +57 -0
  126. package/dist/src/foundation/ui-components/shortcuts.js +81 -0
  127. package/dist/src/foundation/ui-components/spans.d.ts +7 -0
  128. package/dist/src/foundation/ui-components/spans.js +59 -0
  129. package/dist/src/foundation/ui-components/status-line.d.ts +16 -0
  130. package/dist/src/foundation/ui-components/status-line.js +14 -0
  131. package/dist/src/foundation/ui-components/stepper.d.ts +24 -0
  132. package/dist/src/foundation/ui-components/stepper.js +33 -0
  133. package/dist/src/foundation/ui-components/surface.d.ts +20 -0
  134. package/dist/src/foundation/ui-components/surface.js +38 -0
  135. package/dist/src/foundation/ui-components/text.d.ts +24 -0
  136. package/dist/src/foundation/ui-components/text.js +123 -0
  137. package/dist/src/foundation/ui-components/theme.d.ts +17 -0
  138. package/dist/src/foundation/ui-components/theme.js +12 -0
  139. package/dist/src/foundation/ui-components/value-menu.d.ts +41 -0
  140. package/dist/src/foundation/ui-components/value-menu.js +33 -0
  141. package/dist/src/product-identity.d.ts +2 -2
  142. package/dist/src/product-identity.js +2 -1
  143. package/dist/src/product-identity.json +3 -0
  144. package/docs/architecture/boundaries.md +31 -13
  145. package/docs/architecture/process-guardian-provenance.md +26 -0
  146. package/docs/architecture/toolchain.md +6 -0
  147. package/docs/architecture/ui-reference-provenance.md +50 -0
  148. package/docs/ci-release-runbook.md +38 -42
  149. package/docs/features/launch-profiles.md +7 -1
  150. package/docs/manual-launch-instance-acceptance.md +66 -0
  151. package/package.json +11 -12
@@ -0,0 +1,42 @@
1
+ import type { PaneInputResult, PaneMouseEvent, PaneRect, UiTheme } from "../ui-components/index.js";
2
+ export interface AppSize {
3
+ readonly width: number;
4
+ readonly height: number;
5
+ }
6
+ /**
7
+ * What a host offers a presented app. An app reaches the terminal, the pinned
8
+ * shell, or another app only through these — never around them.
9
+ */
10
+ export interface AppHostServices {
11
+ getSize(): AppSize;
12
+ requestRender(): void;
13
+ close(): void;
14
+ /** Restores the surface that opened this app without closing the host. */
15
+ returnToPrevious(): void;
16
+ /** Leaves A1 entirely, as the interrupt chord does from any screen. */
17
+ exit(): void;
18
+ /** True once one interrupt has been seen and the chord is waiting for the second. */
19
+ readonly interruptArmed: boolean;
20
+ /**
21
+ * Whether an idle interrupt closes the presented app. Every host chooses
22
+ * explicitly: a standalone screen closes, a persistent host stays open.
23
+ */
24
+ readonly closeOnInterrupt: boolean;
25
+ /** Colours for the presented app. Absent renders plain text. */
26
+ readonly theme?: UiTheme;
27
+ }
28
+ export interface UiApp {
29
+ readonly id: string;
30
+ render(rect: PaneRect, host: AppHostServices): readonly string[];
31
+ onInput?(data: string, host: AppHostServices): PaneInputResult;
32
+ onMouse?(event: PaneMouseEvent, host: AppHostServices): PaneInputResult;
33
+ onActivate?(host: AppHostServices): void;
34
+ onClose?(host: AppHostServices): void;
35
+ }
36
+ export interface AppRegistration {
37
+ readonly id: string;
38
+ /** Route that opens the app, without its leading slash. */
39
+ readonly route: string;
40
+ create(): UiApp;
41
+ }
42
+ export declare function assertAppRegistration(registration: AppRegistration): void;
@@ -0,0 +1,12 @@
1
+ const ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
2
+ export function assertAppRegistration(registration) {
3
+ if (!ID_PATTERN.test(registration.id)) {
4
+ throw new Error(`owned UI app id is not a bounded slug: ${registration.id}`);
5
+ }
6
+ if (!ID_PATTERN.test(registration.route)) {
7
+ throw new Error(`owned UI app ${registration.id} has an invalid route: ${registration.route}`);
8
+ }
9
+ if (typeof registration.create !== "function") {
10
+ throw new TypeError(`owned UI app ${registration.id} has no create()`);
11
+ }
12
+ }
@@ -0,0 +1,42 @@
1
+ import { type PaneInputResult, type PaneMouseEvent, type UiTheme } from "../ui-components/index.js";
2
+ import type { AppSize, UiApp } from "./contracts.js";
3
+ import type { UiAppRegistry } from "./registry.js";
4
+ export interface AppHostSurface {
5
+ /** Current viewport, re-read on every frame so a resize is picked up. */
6
+ size(): AppSize;
7
+ /** Asks the surrounding runtime to repaint. */
8
+ requestRender(): void;
9
+ /** Presents these rows, or clears the surface when null. */
10
+ present(lines: readonly string[] | null): void;
11
+ /** Reports a failure that closed an app. */
12
+ reportFailure?(appId: string, error: unknown): void;
13
+ /** Leaves A1 entirely. Invoked by the interrupt chord from a presented app. */
14
+ exit?(): void;
15
+ }
16
+ export interface AppHostOptions {
17
+ readonly registry: UiAppRegistry;
18
+ readonly surface: AppHostSurface;
19
+ /** Whether an idle interrupt closes the presented app. Chosen explicitly. */
20
+ readonly closeOnInterrupt: boolean;
21
+ /** Colours handed to every presented app. Absent renders plain text. */
22
+ readonly theme?: UiTheme;
23
+ }
24
+ /**
25
+ * Presents at most one app. Input reaches the presented app first; anything it
26
+ * does not consume continues to the caller. A failure while rendering or
27
+ * handling input closes the app rather than leaving a broken surface on screen.
28
+ */
29
+ export declare class UiAppHost {
30
+ #private;
31
+ constructor(options: AppHostOptions);
32
+ get presented(): UiApp | null;
33
+ get isPresenting(): boolean;
34
+ /** Opens a registered app, replacing whatever was presented. */
35
+ open(id: string): void;
36
+ openRoute(route: string): boolean;
37
+ render(): void;
38
+ handleInput(data: string): PaneInputResult;
39
+ handleMouse(event: PaneMouseEvent): PaneInputResult;
40
+ get interruptArmed(): boolean;
41
+ close(): void;
42
+ }
@@ -0,0 +1,152 @@
1
+ import { FrameCache, finalizeFrame, } from "../ui-components/index.js";
2
+ // The interrupt byte, written as an escape: as a raw byte it is invisible to
3
+ // every text tool, and this constant had already been emptied by one.
4
+ const INTERRUPT = "";
5
+ /** How long the first interrupt of the chord stays armed. */
6
+ const INTERRUPT_CHORD_MS = 1_500;
7
+ /**
8
+ * Presents at most one app. Input reaches the presented app first; anything it
9
+ * does not consume continues to the caller. A failure while rendering or
10
+ * handling input closes the app rather than leaving a broken surface on screen.
11
+ */
12
+ export class UiAppHost {
13
+ #registry;
14
+ #surface;
15
+ #closeOnInterrupt;
16
+ #theme;
17
+ /** When the first interrupt of a chord was seen, or null when disarmed. */
18
+ #interruptArmedAt = null;
19
+ #cache = new FrameCache();
20
+ #app = null;
21
+ constructor(options) {
22
+ this.#registry = options.registry;
23
+ this.#surface = options.surface;
24
+ this.#closeOnInterrupt = options.closeOnInterrupt;
25
+ this.#theme = options.theme;
26
+ }
27
+ get presented() {
28
+ return this.#app;
29
+ }
30
+ get isPresenting() {
31
+ return this.#app !== null;
32
+ }
33
+ /** Opens a registered app, replacing whatever was presented. */
34
+ open(id) {
35
+ const registration = this.#registry.get(id);
36
+ if (registration === null)
37
+ throw new Error(`owned UI app is not registered: ${id}`);
38
+ if (this.#app !== null)
39
+ this.#closeCurrent();
40
+ const app = registration.create();
41
+ this.#app = app;
42
+ this.#cache.invalidate();
43
+ this.#guard(() => app.onActivate?.(this.#services()));
44
+ this.render();
45
+ }
46
+ openRoute(route) {
47
+ const registration = this.#registry.forRoute(route);
48
+ if (registration === null)
49
+ return false;
50
+ this.open(registration.id);
51
+ return true;
52
+ }
53
+ render() {
54
+ const app = this.#app;
55
+ if (app === null) {
56
+ this.#surface.present(null);
57
+ return;
58
+ }
59
+ const size = this.#surface.size();
60
+ const rect = { width: Math.max(0, size.width), height: Math.max(0, size.height) };
61
+ const lines = this.#guard(() => finalizeFrame(app.render(rect, this.#services()), rect, app.id));
62
+ this.#surface.present(lines ?? null);
63
+ }
64
+ handleInput(data) {
65
+ const app = this.#app;
66
+ if (app === null)
67
+ return { consumed: false };
68
+ const result = this.#guard(() => app.onInput?.(data, this.#services())) ?? { consumed: false };
69
+ if (this.#app === null)
70
+ return { consumed: true, render: true };
71
+ if (result.consumed) {
72
+ if (result.render !== false)
73
+ this.render();
74
+ return result;
75
+ }
76
+ if (data === INTERRUPT)
77
+ return this.#interrupt();
78
+ return { consumed: false };
79
+ }
80
+ handleMouse(event) {
81
+ const app = this.#app;
82
+ if (app === null)
83
+ return { consumed: false };
84
+ const result = this.#guard(() => app.onMouse?.(event, this.#services())) ?? { consumed: false };
85
+ if (result.consumed && result.render !== false)
86
+ this.render();
87
+ return result;
88
+ }
89
+ /**
90
+ * Two interrupts leave A1, from any screen. One arms the chord and repaints so
91
+ * the screen can say so; a second within the window exits rather than merely
92
+ * closing the screen, which is what the chord means everywhere else.
93
+ */
94
+ #interrupt() {
95
+ const now = Date.now();
96
+ const armed = this.#interruptArmedAt;
97
+ if (armed !== null && now - armed <= INTERRUPT_CHORD_MS) {
98
+ this.#interruptArmedAt = null;
99
+ this.close();
100
+ this.#surface.exit?.();
101
+ return { consumed: true, render: true };
102
+ }
103
+ this.#interruptArmedAt = now;
104
+ return { consumed: true, render: true };
105
+ }
106
+ get interruptArmed() {
107
+ const armed = this.#interruptArmedAt;
108
+ return armed !== null && Date.now() - armed <= INTERRUPT_CHORD_MS;
109
+ }
110
+ close() {
111
+ if (this.#app === null)
112
+ return;
113
+ this.#closeCurrent();
114
+ this.#surface.present(null);
115
+ }
116
+ #closeCurrent() {
117
+ const app = this.#app;
118
+ this.#app = null;
119
+ this.#cache.invalidate();
120
+ if (app !== null)
121
+ this.#guard(() => app.onClose?.(this.#services()));
122
+ }
123
+ #services() {
124
+ return {
125
+ getSize: () => this.#surface.size(),
126
+ requestRender: () => this.#surface.requestRender(),
127
+ close: () => this.close(),
128
+ returnToPrevious: () => this.close(),
129
+ closeOnInterrupt: this.#closeOnInterrupt,
130
+ exit: () => {
131
+ this.close();
132
+ this.#surface.exit?.();
133
+ },
134
+ interruptArmed: this.interruptArmed,
135
+ ...(this.#theme === undefined ? {} : { theme: this.#theme }),
136
+ };
137
+ }
138
+ /** Contains a failure: the app is closed and the surface restored, never left broken. */
139
+ #guard(work) {
140
+ try {
141
+ return work();
142
+ }
143
+ catch (error) {
144
+ const app = this.#app;
145
+ this.#app = null;
146
+ this.#cache.invalidate();
147
+ this.#surface.present(null);
148
+ this.#surface.reportFailure?.(app?.id ?? "unknown", error);
149
+ return undefined;
150
+ }
151
+ }
152
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./contracts.js";
2
+ export * from "./host.js";
3
+ export * from "./registry.js";
@@ -0,0 +1,3 @@
1
+ export * from "./contracts.js";
2
+ export * from "./host.js";
3
+ export * from "./registry.js";
@@ -0,0 +1,11 @@
1
+ import { type AppRegistration } from "./contracts.js";
2
+ export declare class UiAppRegistry {
3
+ #private;
4
+ /** Registering a known id replaces its definition rather than adding a second app. */
5
+ register(registration: AppRegistration): () => void;
6
+ get(id: string): AppRegistration | null;
7
+ forRoute(route: string): AppRegistration | null;
8
+ ids(): readonly string[];
9
+ routes(): readonly string[];
10
+ clear(): void;
11
+ }
@@ -0,0 +1,32 @@
1
+ import { assertAppRegistration } from "./contracts.js";
2
+ export class UiAppRegistry {
3
+ #byId = new Map();
4
+ /** Registering a known id replaces its definition rather than adding a second app. */
5
+ register(registration) {
6
+ assertAppRegistration(registration);
7
+ const existing = [...this.#byId.values()].find(candidate => candidate.route === registration.route && candidate.id !== registration.id);
8
+ if (existing) {
9
+ throw new Error(`owned UI route ${registration.route} is already taken by ${existing.id}`);
10
+ }
11
+ this.#byId.set(registration.id, registration);
12
+ return () => {
13
+ if (this.#byId.get(registration.id) === registration)
14
+ this.#byId.delete(registration.id);
15
+ };
16
+ }
17
+ get(id) {
18
+ return this.#byId.get(id) ?? null;
19
+ }
20
+ forRoute(route) {
21
+ return [...this.#byId.values()].find(registration => registration.route === route) ?? null;
22
+ }
23
+ ids() {
24
+ return Object.freeze([...this.#byId.keys()].sort());
25
+ }
26
+ routes() {
27
+ return Object.freeze([...this.#byId.values()].map(registration => registration.route).sort());
28
+ }
29
+ clear() {
30
+ this.#byId.clear();
31
+ }
32
+ }
@@ -0,0 +1,36 @@
1
+ import type { UiTheme } from "./theme.js";
2
+ /**
3
+ * The panel a value with parts is edited in: at the foot of the screen, over the
4
+ * surface it was opened from, ruled off above and below. It shows every part,
5
+ * marks the one in hand, says what that part does, and says how to change it.
6
+ */
7
+ export interface DialogRow {
8
+ readonly label: string;
9
+ readonly value: string;
10
+ /** What this part does, shown while it is the one in hand. */
11
+ readonly description?: string;
12
+ }
13
+ export interface DialogPanelState {
14
+ readonly rows: readonly DialogRow[];
15
+ /** The row in hand. */
16
+ readonly index: number;
17
+ /** How to change it, in the words the engine uses. */
18
+ readonly hint: string;
19
+ }
20
+ /** Where the panel sits, for reading a pointer against its rows. */
21
+ export interface DialogPanelFrame {
22
+ /** Screen row the panel's first row is drawn on. */
23
+ readonly firstRow: number;
24
+ readonly rows: number;
25
+ /** Column the values start at, so a label stays a label. */
26
+ readonly valueColumn: number;
27
+ }
28
+ /** The column values start at: past the widest label, capped so one long name cannot push them off. */
29
+ export declare function dialogValueColumn(rows: readonly DialogRow[]): number;
30
+ /**
31
+ * The panel's lines. The first line is its rule, so the row at index N is drawn
32
+ * one line below the panel's top.
33
+ */
34
+ export declare function renderDialogPanel(state: DialogPanelState, width: number, theme: UiTheme): readonly string[];
35
+ /** The row a pointer report lands on, or null when it is not on one. */
36
+ export declare function dialogRowAt(frame: DialogPanelFrame, row: number, column: number, valueWidth: number): number | null;
@@ -0,0 +1,47 @@
1
+ import { displayWidth, truncateToWidth } from "./text.js";
2
+ const LABEL_COLUMN_CAP = 30;
3
+ /** The column values start at: past the widest label, capped so one long name cannot push them off. */
4
+ export function dialogValueColumn(rows) {
5
+ const widest = Math.min(LABEL_COLUMN_CAP, Math.max(0, ...rows.map(row => displayWidth(row.label))));
6
+ return 2 + widest + 2;
7
+ }
8
+ /**
9
+ * The panel's lines. The first line is its rule, so the row at index N is drawn
10
+ * one line below the panel's top.
11
+ */
12
+ export function renderDialogPanel(state, width, theme) {
13
+ const labelColumn = Math.min(LABEL_COLUMN_CAP, Math.max(0, ...state.rows.map(row => displayWidth(row.label))));
14
+ const rule = theme.fg("border", "─".repeat(Math.max(0, width)));
15
+ const rows = state.rows.map((row, index) => {
16
+ const selected = index === state.index;
17
+ const padded = `${row.label}${" ".repeat(Math.max(0, labelColumn - displayWidth(row.label)))}`;
18
+ const cursor = selected ? "→ " : " ";
19
+ const raw = `${cursor}${padded} ${row.value}`;
20
+ // An unselected label is left unpainted and its value quietened, both lifted
21
+ // to the accent on the row the cursor is on — as the engine paints its own.
22
+ const painted = `${theme.fg("accent", cursor)}${selected ? theme.fg("accent", padded) : padded} ${theme.fg("muted", row.value)}`;
23
+ return pad(truncateToWidth(painted, width), width, raw);
24
+ });
25
+ const description = state.rows[state.index]?.description ?? "";
26
+ return [
27
+ rule,
28
+ ...rows,
29
+ "",
30
+ pad(truncateToWidth(theme.fg("dim", ` ${description}`), width), width, ` ${description}`),
31
+ "",
32
+ pad(truncateToWidth(theme.fg("dim", ` ${state.hint}`), width), width, ` ${state.hint}`),
33
+ rule,
34
+ ];
35
+ }
36
+ /** The row a pointer report lands on, or null when it is not on one. */
37
+ export function dialogRowAt(frame, row, column, valueWidth) {
38
+ const at = row - frame.firstRow;
39
+ if (at < 0 || at >= frame.rows)
40
+ return null;
41
+ const start = frame.valueColumn + 1;
42
+ return column >= start && column < start + valueWidth ? at : null;
43
+ }
44
+ function pad(line, width, raw) {
45
+ const visible = displayWidth(raw);
46
+ return visible >= width ? line : line + " ".repeat(width - visible);
47
+ }
@@ -0,0 +1,21 @@
1
+ export interface PaneRect {
2
+ readonly width: number;
3
+ readonly height: number;
4
+ }
5
+ export declare class FrameContractError extends Error {
6
+ readonly pane: string;
7
+ constructor(pane: string, message: string);
8
+ }
9
+ export declare function assertPaneRect(rect: PaneRect, pane?: string): void;
10
+ /**
11
+ * A pane owns its rectangle exactly: one row per row of height, each within
12
+ * width, and never an embedded newline — the host counts rows to place
13
+ * everything below, so a miscount corrupts the surrounding layout.
14
+ */
15
+ export declare function validateFrame(lines: readonly string[], rect: PaneRect, pane?: string): void;
16
+ /**
17
+ * Brings a rendered body up to the contract: truncates overlong rows, pads a
18
+ * short body with blank rows, and drops rows beyond the rectangle. Panes that
19
+ * compose other panes use this so one child cannot break the parent's layout.
20
+ */
21
+ export declare function finalizeFrame(lines: readonly string[], rect: PaneRect, pane?: string): readonly string[];
@@ -0,0 +1,54 @@
1
+ import { displayWidth, truncateToWidth } from "./text.js";
2
+ export class FrameContractError extends Error {
3
+ pane;
4
+ constructor(pane, message) {
5
+ super(`${pane}: ${message}`);
6
+ this.pane = pane;
7
+ this.name = "FrameContractError";
8
+ }
9
+ }
10
+ const MAX_DIMENSION = 10_000;
11
+ export function assertPaneRect(rect, pane = "pane") {
12
+ for (const [name, value] of [["width", rect.width], ["height", rect.height]]) {
13
+ if (!Number.isInteger(value) || value < 0 || value > MAX_DIMENSION) {
14
+ throw new FrameContractError(pane, `${name} must be an integer between 0 and ${MAX_DIMENSION}, received ${value}`);
15
+ }
16
+ }
17
+ }
18
+ /**
19
+ * A pane owns its rectangle exactly: one row per row of height, each within
20
+ * width, and never an embedded newline — the host counts rows to place
21
+ * everything below, so a miscount corrupts the surrounding layout.
22
+ */
23
+ export function validateFrame(lines, rect, pane = "pane") {
24
+ assertPaneRect(rect, pane);
25
+ if (lines.length !== rect.height) {
26
+ throw new FrameContractError(pane, `rendered ${lines.length} rows for a height of ${rect.height}`);
27
+ }
28
+ lines.forEach((line, index) => {
29
+ if (line.includes("\n") || line.includes("\r")) {
30
+ throw new FrameContractError(pane, `row ${index} contains a line break`);
31
+ }
32
+ const width = displayWidth(line);
33
+ if (width > rect.width) {
34
+ throw new FrameContractError(pane, `row ${index} is ${width} columns wide for a width of ${rect.width}`);
35
+ }
36
+ });
37
+ }
38
+ /**
39
+ * Brings a rendered body up to the contract: truncates overlong rows, pads a
40
+ * short body with blank rows, and drops rows beyond the rectangle. Panes that
41
+ * compose other panes use this so one child cannot break the parent's layout.
42
+ */
43
+ export function finalizeFrame(lines, rect, pane = "pane") {
44
+ assertPaneRect(rect, pane);
45
+ const rows = [];
46
+ for (let index = 0; index < rect.height; index++) {
47
+ const line = lines[index] ?? "";
48
+ if (line.includes("\n") || line.includes("\r")) {
49
+ throw new FrameContractError(pane, `row ${index} contains a line break`);
50
+ }
51
+ rows.push(displayWidth(line) > rect.width ? truncateToWidth(line, rect.width) : line);
52
+ }
53
+ return Object.freeze(rows);
54
+ }
@@ -0,0 +1,18 @@
1
+ export * from "./dialog-panel.js";
2
+ export * from "./frame.js";
3
+ export * from "./label.js";
4
+ export * from "./line-input.js";
5
+ export * from "./list-block.js";
6
+ export * from "./list-view.js";
7
+ export * from "./mouse.js";
8
+ export * from "./pane.js";
9
+ export * from "./revision.js";
10
+ export * from "./scrollbar.js";
11
+ export * from "./shortcuts.js";
12
+ export * from "./spans.js";
13
+ export * from "./status-line.js";
14
+ export * from "./stepper.js";
15
+ export * from "./surface.js";
16
+ export * from "./text.js";
17
+ export * from "./theme.js";
18
+ export * from "./value-menu.js";
@@ -0,0 +1,18 @@
1
+ export * from "./dialog-panel.js";
2
+ export * from "./frame.js";
3
+ export * from "./label.js";
4
+ export * from "./line-input.js";
5
+ export * from "./list-block.js";
6
+ export * from "./list-view.js";
7
+ export * from "./mouse.js";
8
+ export * from "./pane.js";
9
+ export * from "./revision.js";
10
+ export * from "./scrollbar.js";
11
+ export * from "./shortcuts.js";
12
+ export * from "./spans.js";
13
+ export * from "./status-line.js";
14
+ export * from "./stepper.js";
15
+ export * from "./surface.js";
16
+ export * from "./text.js";
17
+ export * from "./theme.js";
18
+ export * from "./value-menu.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Setting identifiers are written for code; a screen shows them to a person.
3
+ * `httpIdleTimeoutMs` reads as "Http Idle Timeout Ms", not as itself.
4
+ */
5
+ /** Splits camelCase, snake_case, and kebab-case into display words. */
6
+ export declare function humanizeLabel(identifier: string): string;
7
+ /** Section titles use the same rules, so `owned-ui` reads as "Owned UI". */
8
+ export declare function humanizeTitle(identifier: string): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Setting identifiers are written for code; a screen shows them to a person.
3
+ * `httpIdleTimeoutMs` reads as "Http Idle Timeout Ms", not as itself.
4
+ */
5
+ const ACRONYMS = {
6
+ ui: "UI",
7
+ url: "URL",
8
+ api: "API",
9
+ id: "ID",
10
+ ms: "Ms",
11
+ cli: "CLI",
12
+ tui: "TUI",
13
+ sgr: "SGR",
14
+ ansi: "ANSI",
15
+ };
16
+ /** Splits camelCase, snake_case, and kebab-case into display words. */
17
+ export function humanizeLabel(identifier) {
18
+ const words = identifier
19
+ .replace(/[_-]+/g, " ")
20
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
21
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
22
+ .split(/\s+/)
23
+ .filter(word => word.length > 0);
24
+ if (words.length === 0)
25
+ return identifier;
26
+ return words.map(word => {
27
+ const known = ACRONYMS[word.toLowerCase()];
28
+ if (known !== undefined)
29
+ return known;
30
+ return word.charAt(0).toUpperCase() + word.slice(1);
31
+ }).join(" ");
32
+ }
33
+ /** Section titles use the same rules, so `owned-ui` reads as "Owned UI". */
34
+ export function humanizeTitle(identifier) {
35
+ return humanizeLabel(identifier);
36
+ }
@@ -0,0 +1,74 @@
1
+ export type LineInputOutcome = {
2
+ readonly kind: "editing";
3
+ } | {
4
+ readonly kind: "accepted";
5
+ readonly value: string;
6
+ } | {
7
+ readonly kind: "cancelled";
8
+ };
9
+ export interface LineInputView {
10
+ /** Visible slice of the value, already fitted to the width. */
11
+ readonly text: string;
12
+ /** Zero-based column of the caret within the visible slice. */
13
+ readonly caretColumn: number;
14
+ }
15
+ /**
16
+ * Where a word jump to the left lands: past any whitespace, then past the run of
17
+ * characters of one class, so a word and a run of punctuation each count as one.
18
+ */
19
+ export declare function wordLeft(text: string, from: number): number;
20
+ /** Where a word jump to the right lands, by the same rules mirrored. */
21
+ export declare function wordRight(text: string, from: number): number;
22
+ /**
23
+ * A single-line editable value with a caret and horizontal scrolling. Accept and
24
+ * cancel are reported distinctly so a caller can commit or discard; cancelling
25
+ * never changes the caller's value.
26
+ */
27
+ export declare class LineInput {
28
+ #private;
29
+ constructor(value?: string);
30
+ get value(): string;
31
+ get caret(): number;
32
+ setValue(value: string): void;
33
+ insert(text: string): void;
34
+ backspace(): void;
35
+ deleteForward(): void;
36
+ moveCaret(delta: number): void;
37
+ /** Moves the caret one word, in whichever direction the sign says. */
38
+ moveCaretByWord(direction: -1 | 1): void;
39
+ /** Removes the word before the caret, leaving the caret where it began. */
40
+ deleteWordBefore(): void;
41
+ /** Removes the word after the caret, leaving the caret where it is. */
42
+ deleteWordAfter(): void;
43
+ moveCaretToStart(): void;
44
+ moveCaretToEnd(): void;
45
+ /** Visible slice for a width, scrolled so the caret stays on screen. */
46
+ view(width: number): LineInputView;
47
+ }
48
+ /** Applies one key to the input and reports whether the caller should commit. */
49
+ export declare function handleLineInputKey(input: LineInput, data: string): LineInputOutcome;
50
+ /**
51
+ * The prompt the reference draws at the head of an input row, in the grey it
52
+ * uses for one (#9AA0A6), painted foreground-only so anything drawn behind the
53
+ * row survives.
54
+ */
55
+ /** A rule drawn in the prompt's own grey, as the reference rules an input row. */
56
+ export declare function promptRule(width: number): string;
57
+ export declare const PROMPT_GLYPH = "\u001B[38;2;154;160;166m\u276F\u001B[39m ";
58
+ /**
59
+ * The caret the reference draws: the cell under it is reversed rather than given
60
+ * a colour of its own, so it reads as a block in whatever theme is in use.
61
+ */
62
+ export declare function caretCell(text: string): string;
63
+ export interface InputRowOptions {
64
+ /** Shown quietly while nothing has been typed, with the caret on its first cell. */
65
+ readonly placeholder?: string;
66
+ /** Rules above and below, in the prompt's own grey. Default true. */
67
+ readonly ruled?: boolean;
68
+ }
69
+ export interface InputRow {
70
+ /** The rows to draw, already padded to the width. */
71
+ readonly lines: readonly string[];
72
+ }
73
+ /** The input row as the reference draws one, padded to exactly the width. */
74
+ export declare function renderInputRow(input: LineInput, width: number, options?: InputRowOptions): InputRow;