@jmoyers/harness 0.1.10 → 0.1.20

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 (239) hide show
  1. package/README.md +31 -35
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. package/src/ui/surface.ts +0 -224
@@ -0,0 +1,118 @@
1
+ import type { LeftNavSelection } from './left-nav-input.ts';
2
+
3
+ export interface RepositoryFoldState {
4
+ readonly leftNavSelection: () => LeftNavSelection;
5
+ readonly repositoryToggleChordPrefixAtMs: () => number | null;
6
+ readonly setRepositoryToggleChordPrefixAtMs: (value: number | null) => void;
7
+ readonly conversations: () => ReadonlyMap<string, { directoryId: string | null }>;
8
+ readonly repositoryGroupIdForDirectory: (directoryId: string) => string;
9
+ readonly nowMs: () => number;
10
+ }
11
+
12
+ export interface RepositoryFoldActions {
13
+ readonly collapseRepositoryGroup: (repositoryGroupId: string) => void;
14
+ readonly expandRepositoryGroup: (repositoryGroupId: string) => void;
15
+ readonly collapseAllRepositoryGroups: () => void;
16
+ readonly expandAllRepositoryGroups: () => void;
17
+ readonly selectLeftNavRepository: (repositoryGroupId: string) => void;
18
+ readonly markDirty: () => void;
19
+ }
20
+
21
+ export interface RepositoryFoldChordConfig {
22
+ readonly chordTimeoutMs: number;
23
+ readonly collapseAllChordPrefix: Buffer;
24
+ }
25
+
26
+ export interface RepositoryFoldStrategies {
27
+ reduceRepositoryFoldChordInput(input: {
28
+ readonly input: Buffer;
29
+ readonly leftNavSelection: LeftNavSelection;
30
+ readonly nowMs: number;
31
+ readonly prefixAtMs: number | null;
32
+ readonly chordTimeoutMs: number;
33
+ readonly collapseAllChordPrefix: Buffer;
34
+ }): {
35
+ readonly consumed: boolean;
36
+ readonly nextPrefixAtMs: number | null;
37
+ readonly action: 'expand-all' | 'collapse-all' | null;
38
+ };
39
+ repositoryTreeArrowAction(
40
+ input: Buffer,
41
+ selection: LeftNavSelection,
42
+ repositoryId: string | null,
43
+ ): 'expand' | 'collapse' | null;
44
+ }
45
+
46
+ export class RepositoryFoldInput {
47
+ constructor(
48
+ private readonly state: RepositoryFoldState,
49
+ private readonly actions: RepositoryFoldActions,
50
+ private readonly chordConfig: RepositoryFoldChordConfig,
51
+ private readonly strategies: RepositoryFoldStrategies,
52
+ ) {}
53
+
54
+ private selectedRepositoryGroupId(): string | null {
55
+ const leftNavSelection = this.state.leftNavSelection();
56
+ if (leftNavSelection.kind === 'repository') {
57
+ return leftNavSelection.repositoryId;
58
+ }
59
+ if (leftNavSelection.kind === 'project') {
60
+ return this.state.repositoryGroupIdForDirectory(leftNavSelection.directoryId);
61
+ }
62
+ if (leftNavSelection.kind === 'conversation') {
63
+ const conversation = this.state.conversations().get(leftNavSelection.sessionId);
64
+ if (conversation?.directoryId !== null && conversation?.directoryId !== undefined) {
65
+ return this.state.repositoryGroupIdForDirectory(conversation.directoryId);
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ handleRepositoryTreeArrow(input: Buffer): boolean {
72
+ const repositoryId = this.selectedRepositoryGroupId();
73
+ const action = this.strategies.repositoryTreeArrowAction(
74
+ input,
75
+ this.state.leftNavSelection(),
76
+ repositoryId,
77
+ );
78
+ if (repositoryId === null || action === null) {
79
+ return false;
80
+ }
81
+ if (action === 'expand') {
82
+ this.actions.expandRepositoryGroup(repositoryId);
83
+ this.actions.selectLeftNavRepository(repositoryId);
84
+ this.actions.markDirty();
85
+ return true;
86
+ }
87
+ if (action === 'collapse') {
88
+ this.actions.collapseRepositoryGroup(repositoryId);
89
+ this.actions.selectLeftNavRepository(repositoryId);
90
+ this.actions.markDirty();
91
+ return true;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ handleRepositoryFoldChords(input: Buffer): boolean {
97
+ const reduced = this.strategies.reduceRepositoryFoldChordInput({
98
+ input,
99
+ leftNavSelection: this.state.leftNavSelection(),
100
+ nowMs: this.state.nowMs(),
101
+ prefixAtMs: this.state.repositoryToggleChordPrefixAtMs(),
102
+ chordTimeoutMs: this.chordConfig.chordTimeoutMs,
103
+ collapseAllChordPrefix: this.chordConfig.collapseAllChordPrefix,
104
+ });
105
+ this.state.setRepositoryToggleChordPrefixAtMs(reduced.nextPrefixAtMs);
106
+ if (reduced.action === 'expand-all') {
107
+ this.actions.expandAllRepositoryGroups();
108
+ this.actions.markDirty();
109
+ return true;
110
+ }
111
+ if (reduced.action === 'collapse-all') {
112
+ this.actions.collapseAllRepositoryGroups();
113
+ this.actions.markDirty();
114
+ return true;
115
+ }
116
+ return reduced.consumed;
117
+ }
118
+ }
@@ -0,0 +1,476 @@
1
+ import { TextLayoutEngine } from './text-layout.ts';
2
+ import { DEFAULT_UI_STYLE, SurfaceBuffer, type UiColor, type UiStyle } from './surface.ts';
3
+
4
+ export interface UiRect {
5
+ readonly col: number;
6
+ readonly row: number;
7
+ readonly width: number;
8
+ readonly height: number;
9
+ }
10
+
11
+ export type UiTextAlign = 'left' | 'center' | 'right';
12
+
13
+ export interface UiBoxGlyphs {
14
+ readonly topLeft: string;
15
+ readonly topRight: string;
16
+ readonly bottomLeft: string;
17
+ readonly bottomRight: string;
18
+ readonly horizontal: string;
19
+ readonly vertical: string;
20
+ }
21
+
22
+ export const SINGLE_LINE_UI_BOX_GLYPHS: UiBoxGlyphs = {
23
+ topLeft: '┌',
24
+ topRight: '┐',
25
+ bottomLeft: '└',
26
+ bottomRight: '┘',
27
+ horizontal: '─',
28
+ vertical: '│',
29
+ };
30
+
31
+ export interface UiModalTheme {
32
+ readonly frameStyle: UiStyle;
33
+ readonly titleStyle: UiStyle;
34
+ readonly bodyStyle: UiStyle;
35
+ readonly footerStyle: UiStyle;
36
+ }
37
+
38
+ export interface UiModalContent {
39
+ readonly title?: string;
40
+ readonly bodyLines?: readonly string[];
41
+ readonly footer?: string;
42
+ readonly paddingX?: number;
43
+ }
44
+
45
+ export type UiModalAnchor = 'center' | 'bottom';
46
+
47
+ export interface UiModalOverlayOptions extends UiModalContent {
48
+ readonly viewportCols: number;
49
+ readonly viewportRows: number;
50
+ readonly width: number;
51
+ readonly height: number;
52
+ readonly anchor?: UiModalAnchor;
53
+ readonly marginRows?: number;
54
+ readonly theme?: Partial<UiModalTheme>;
55
+ }
56
+
57
+ export interface UiModalOverlay {
58
+ readonly left: number;
59
+ readonly top: number;
60
+ readonly width: number;
61
+ readonly height: number;
62
+ readonly rows: readonly string[];
63
+ }
64
+
65
+ export interface UiButtonContent {
66
+ readonly label: string;
67
+ readonly prefixIcon?: string;
68
+ readonly suffixIcon?: string;
69
+ readonly paddingX?: number;
70
+ }
71
+
72
+ export interface UiTrailingLabelRowOptions {
73
+ readonly col?: number;
74
+ readonly width?: number;
75
+ readonly gap?: number;
76
+ }
77
+
78
+ const MODAL_FRAME_FG: UiColor = { kind: 'indexed', index: 252 };
79
+ const MODAL_TITLE_FG: UiColor = { kind: 'indexed', index: 231 };
80
+ const MODAL_BODY_FG: UiColor = { kind: 'indexed', index: 253 };
81
+ const MODAL_FOOTER_FG: UiColor = { kind: 'indexed', index: 247 };
82
+ const MODAL_BG: UiColor = { kind: 'indexed', index: 236 };
83
+
84
+ export const DEFAULT_UI_MODAL_THEME: UiModalTheme = {
85
+ frameStyle: {
86
+ fg: MODAL_FRAME_FG,
87
+ bg: MODAL_BG,
88
+ bold: true,
89
+ },
90
+ titleStyle: {
91
+ fg: MODAL_TITLE_FG,
92
+ bg: MODAL_BG,
93
+ bold: true,
94
+ },
95
+ bodyStyle: {
96
+ fg: MODAL_BODY_FG,
97
+ bg: MODAL_BG,
98
+ bold: false,
99
+ },
100
+ footerStyle: {
101
+ fg: MODAL_FOOTER_FG,
102
+ bg: MODAL_BG,
103
+ bold: false,
104
+ },
105
+ };
106
+
107
+ function clamp(value: number, min: number, max: number): number {
108
+ if (value < min) {
109
+ return min;
110
+ }
111
+ if (value > max) {
112
+ return max;
113
+ }
114
+ return value;
115
+ }
116
+
117
+ function normalizeRect(surface: SurfaceBuffer, rect: UiRect): UiRect | null {
118
+ const colStart = Math.max(0, Math.floor(rect.col));
119
+ const rowStart = Math.max(0, Math.floor(rect.row));
120
+ const colEnd = Math.min(surface.cols, Math.ceil(rect.col + rect.width));
121
+ const rowEnd = Math.min(surface.rows, Math.ceil(rect.row + rect.height));
122
+ const width = colEnd - colStart;
123
+ const height = rowEnd - rowStart;
124
+ if (width <= 0 || height <= 0) {
125
+ return null;
126
+ }
127
+ return {
128
+ col: colStart,
129
+ row: rowStart,
130
+ width,
131
+ height,
132
+ };
133
+ }
134
+
135
+ function mergeModalTheme(theme: Partial<UiModalTheme> | undefined): UiModalTheme {
136
+ if (theme === undefined) {
137
+ return DEFAULT_UI_MODAL_THEME;
138
+ }
139
+ return {
140
+ frameStyle: theme.frameStyle ?? DEFAULT_UI_MODAL_THEME.frameStyle,
141
+ titleStyle: theme.titleStyle ?? DEFAULT_UI_MODAL_THEME.titleStyle,
142
+ bodyStyle: theme.bodyStyle ?? DEFAULT_UI_MODAL_THEME.bodyStyle,
143
+ footerStyle: theme.footerStyle ?? DEFAULT_UI_MODAL_THEME.footerStyle,
144
+ };
145
+ }
146
+
147
+ export class UiKit {
148
+ constructor(private readonly layout: TextLayoutEngine = new TextLayoutEngine()) {}
149
+
150
+ public truncateText(text: string, width: number): string {
151
+ return this.layout.truncate(text, width);
152
+ }
153
+
154
+ public formatButton(content: UiButtonContent): string {
155
+ const label = content.label.trim();
156
+ const prefixIcon = content.prefixIcon?.trim();
157
+ const suffixIcon = content.suffixIcon?.trim();
158
+ const paddingX = clamp(Math.floor(content.paddingX ?? 1), 0, 8);
159
+
160
+ const segments: string[] = [];
161
+ if (prefixIcon !== undefined && prefixIcon.length > 0) {
162
+ segments.push(prefixIcon);
163
+ }
164
+ segments.push(label.length > 0 ? label : 'button');
165
+ if (suffixIcon !== undefined && suffixIcon.length > 0) {
166
+ segments.push(suffixIcon);
167
+ }
168
+
169
+ const padded = `${' '.repeat(paddingX)}${segments.join(' ')}${' '.repeat(paddingX)}`;
170
+ return `[${padded}]`;
171
+ }
172
+
173
+ public drawAlignedText(
174
+ surface: SurfaceBuffer,
175
+ col: number,
176
+ row: number,
177
+ width: number,
178
+ text: string,
179
+ style: UiStyle,
180
+ align: UiTextAlign = 'left',
181
+ ): void {
182
+ const safeWidth = Math.max(0, Math.floor(width));
183
+ if (safeWidth === 0) {
184
+ return;
185
+ }
186
+
187
+ const clipped = this.truncateText(text, safeWidth);
188
+ const clippedWidth = Math.max(0, this.layout.measure(clipped));
189
+ let offset = 0;
190
+ if (align === 'center') {
191
+ offset = Math.max(0, Math.floor((safeWidth - clippedWidth) / 2));
192
+ } else if (align === 'right') {
193
+ offset = Math.max(0, safeWidth - clippedWidth);
194
+ }
195
+ surface.drawText(col + offset, row, clipped, style);
196
+ }
197
+
198
+ public paintRow(
199
+ surface: SurfaceBuffer,
200
+ row: number,
201
+ text: string,
202
+ textStyle: UiStyle,
203
+ fillStyle: UiStyle = textStyle,
204
+ col = 0,
205
+ ): void {
206
+ surface.fillRow(row, fillStyle);
207
+ surface.drawText(col, row, text, textStyle);
208
+ }
209
+
210
+ public paintRowWithTrailingLabel(
211
+ surface: SurfaceBuffer,
212
+ row: number,
213
+ leftText: string,
214
+ trailingLabel: string,
215
+ leftStyle: UiStyle,
216
+ trailingStyle: UiStyle,
217
+ fillStyle: UiStyle = leftStyle,
218
+ options: UiTrailingLabelRowOptions = {},
219
+ ): void {
220
+ const col = Math.max(0, Math.floor(options.col ?? 0));
221
+ const width = Math.max(0, Math.floor(options.width ?? surface.cols - col));
222
+ const gap = clamp(Math.floor(options.gap ?? 1), 0, width);
223
+ surface.fillRow(row, fillStyle);
224
+ if (width === 0) {
225
+ return;
226
+ }
227
+
228
+ const clippedTrailing = this.truncateText(trailingLabel, width);
229
+ const trailingWidth = Math.max(0, this.layout.measure(clippedTrailing));
230
+ const reservedGap = trailingWidth > 0 ? gap : 0;
231
+ const leftWidth = Math.max(0, width - trailingWidth - reservedGap);
232
+ const clippedLeft = this.truncateText(leftText, leftWidth);
233
+ if (clippedLeft.length > 0) {
234
+ surface.drawText(col, row, clippedLeft, leftStyle);
235
+ }
236
+ if (clippedTrailing.length > 0) {
237
+ surface.drawText(col + width - trailingWidth, row, clippedTrailing, trailingStyle);
238
+ }
239
+ }
240
+
241
+ public fillRect(surface: SurfaceBuffer, rect: UiRect, style: UiStyle): void {
242
+ const normalized = normalizeRect(surface, rect);
243
+ if (normalized === null) {
244
+ return;
245
+ }
246
+ const blank = ' '.repeat(normalized.width);
247
+ for (let row = normalized.row; row < normalized.row + normalized.height; row += 1) {
248
+ surface.drawText(normalized.col, row, blank, style);
249
+ }
250
+ }
251
+
252
+ public strokeRect(
253
+ surface: SurfaceBuffer,
254
+ rect: UiRect,
255
+ style: UiStyle,
256
+ glyphs: UiBoxGlyphs = SINGLE_LINE_UI_BOX_GLYPHS,
257
+ ): void {
258
+ const normalized = normalizeRect(surface, rect);
259
+ if (normalized === null) {
260
+ return;
261
+ }
262
+
263
+ if (normalized.width === 1 && normalized.height === 1) {
264
+ surface.drawText(normalized.col, normalized.row, glyphs.topLeft, style);
265
+ return;
266
+ }
267
+
268
+ if (normalized.height === 1) {
269
+ surface.drawText(
270
+ normalized.col,
271
+ normalized.row,
272
+ glyphs.horizontal.repeat(normalized.width),
273
+ style,
274
+ );
275
+ return;
276
+ }
277
+
278
+ if (normalized.width === 1) {
279
+ for (let row = normalized.row; row < normalized.row + normalized.height; row += 1) {
280
+ surface.drawText(normalized.col, row, glyphs.vertical, style);
281
+ }
282
+ return;
283
+ }
284
+
285
+ const horizontal = glyphs.horizontal.repeat(Math.max(0, normalized.width - 2));
286
+ surface.drawText(
287
+ normalized.col,
288
+ normalized.row,
289
+ `${glyphs.topLeft}${horizontal}${glyphs.topRight}`,
290
+ style,
291
+ );
292
+
293
+ const bottomRow = normalized.row + normalized.height - 1;
294
+ surface.drawText(
295
+ normalized.col,
296
+ bottomRow,
297
+ `${glyphs.bottomLeft}${horizontal}${glyphs.bottomRight}`,
298
+ style,
299
+ );
300
+
301
+ for (let row = normalized.row + 1; row < bottomRow; row += 1) {
302
+ surface.drawText(normalized.col, row, glyphs.vertical, style);
303
+ surface.drawText(normalized.col + normalized.width - 1, row, glyphs.vertical, style);
304
+ }
305
+ }
306
+
307
+ public layoutModalRect(
308
+ viewportCols: number,
309
+ viewportRows: number,
310
+ width: number,
311
+ height: number,
312
+ anchor: UiModalAnchor = 'center',
313
+ marginRows = 1,
314
+ ): UiRect {
315
+ const safeViewportCols = Math.max(1, Math.floor(viewportCols));
316
+ const safeViewportRows = Math.max(1, Math.floor(viewportRows));
317
+ const safeWidth = clamp(Math.floor(width), 1, safeViewportCols);
318
+ const safeHeight = clamp(Math.floor(height), 1, safeViewportRows);
319
+
320
+ const left = Math.floor((safeViewportCols - safeWidth) / 2);
321
+ const top =
322
+ anchor === 'bottom'
323
+ ? Math.max(0, safeViewportRows - safeHeight - Math.max(0, Math.floor(marginRows)))
324
+ : Math.floor((safeViewportRows - safeHeight) / 2);
325
+
326
+ return {
327
+ col: left,
328
+ row: top,
329
+ width: safeWidth,
330
+ height: safeHeight,
331
+ };
332
+ }
333
+
334
+ public drawModal(
335
+ surface: SurfaceBuffer,
336
+ rect: UiRect,
337
+ content: UiModalContent,
338
+ theme: Partial<UiModalTheme> | undefined = undefined,
339
+ ): UiRect | null {
340
+ const normalized = normalizeRect(surface, rect);
341
+ if (normalized === null) {
342
+ return null;
343
+ }
344
+
345
+ const resolvedTheme = mergeModalTheme(theme);
346
+ this.fillRect(surface, normalized, resolvedTheme.bodyStyle);
347
+ this.strokeRect(surface, normalized, resolvedTheme.frameStyle);
348
+
349
+ const inner: UiRect = {
350
+ col: normalized.col + 1,
351
+ row: normalized.row + 1,
352
+ width: Math.max(0, normalized.width - 2),
353
+ height: Math.max(0, normalized.height - 2),
354
+ };
355
+ const normalizedInner = normalizeRect(surface, inner);
356
+ if (normalizedInner === null) {
357
+ return normalized;
358
+ }
359
+
360
+ const paddingX = clamp(
361
+ Math.floor(content.paddingX ?? 1),
362
+ 0,
363
+ Math.floor(normalizedInner.width / 2),
364
+ );
365
+ const textCol = normalizedInner.col + paddingX;
366
+ const textWidth = Math.max(0, normalizedInner.width - paddingX * 2);
367
+ if (textWidth === 0) {
368
+ return normalized;
369
+ }
370
+
371
+ let nextRow = normalizedInner.row;
372
+ const innerBottom = normalizedInner.row + normalizedInner.height - 1;
373
+
374
+ if (content.title !== undefined && content.title.length > 0 && nextRow <= innerBottom) {
375
+ this.drawAlignedText(
376
+ surface,
377
+ textCol,
378
+ nextRow,
379
+ textWidth,
380
+ content.title,
381
+ resolvedTheme.titleStyle,
382
+ 'center',
383
+ );
384
+ nextRow += 1;
385
+ }
386
+
387
+ const footerRow =
388
+ content.footer !== undefined && content.footer.length > 0 && nextRow <= innerBottom
389
+ ? innerBottom
390
+ : null;
391
+ const footerText = content.footer;
392
+ const bodyBottom = footerRow === null ? innerBottom : footerRow - 1;
393
+
394
+ const bodyLines = content.bodyLines ?? [];
395
+ for (const line of bodyLines) {
396
+ if (nextRow > bodyBottom) {
397
+ break;
398
+ }
399
+ this.drawAlignedText(
400
+ surface,
401
+ textCol,
402
+ nextRow,
403
+ textWidth,
404
+ line,
405
+ resolvedTheme.bodyStyle,
406
+ 'left',
407
+ );
408
+ nextRow += 1;
409
+ }
410
+
411
+ if (footerRow !== null && footerText !== undefined) {
412
+ this.drawAlignedText(
413
+ surface,
414
+ textCol,
415
+ footerRow,
416
+ textWidth,
417
+ footerText,
418
+ resolvedTheme.footerStyle,
419
+ 'right',
420
+ );
421
+ }
422
+ return normalized;
423
+ }
424
+
425
+ public buildModalOverlay(options: UiModalOverlayOptions): UiModalOverlay {
426
+ const rect = this.layoutModalRect(
427
+ options.viewportCols,
428
+ options.viewportRows,
429
+ options.width,
430
+ options.height,
431
+ options.anchor ?? 'center',
432
+ options.marginRows ?? 1,
433
+ );
434
+
435
+ const surface = new SurfaceBuffer(rect.width, rect.height, DEFAULT_UI_STYLE);
436
+ const content: UiModalContent = {
437
+ bodyLines: options.bodyLines ?? [],
438
+ ...(options.title !== undefined ? { title: options.title } : {}),
439
+ ...(options.footer !== undefined ? { footer: options.footer } : {}),
440
+ ...(options.paddingX !== undefined ? { paddingX: options.paddingX } : {}),
441
+ };
442
+ this.drawModal(
443
+ surface,
444
+ {
445
+ col: 0,
446
+ row: 0,
447
+ width: rect.width,
448
+ height: rect.height,
449
+ },
450
+ content,
451
+ options.theme,
452
+ );
453
+
454
+ return {
455
+ left: rect.col,
456
+ top: rect.row,
457
+ width: rect.width,
458
+ height: rect.height,
459
+ rows: surface.renderAnsiRows(),
460
+ };
461
+ }
462
+
463
+ public isModalOverlayHit(overlay: UiModalOverlay, col: number, row: number): boolean {
464
+ if (col < 1 || row < 1) {
465
+ return false;
466
+ }
467
+ const colZero = col - 1;
468
+ const rowZero = row - 1;
469
+ return (
470
+ colZero >= overlay.left &&
471
+ colZero < overlay.left + overlay.width &&
472
+ rowZero >= overlay.top &&
473
+ rowZero < overlay.top + overlay.height
474
+ );
475
+ }
476
+ }