@jmoyers/harness 0.1.11 → 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 (232) hide show
  1. package/README.md +31 -39
  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/packages/harness-ui/src/modal-manager.ts +222 -0
  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 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  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 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -1,148 +0,0 @@
1
- import { handleLeftRailActionClick } from '../mux/live-mux/left-rail-actions.ts';
2
- import { handleLeftRailConversationClick } from '../mux/live-mux/left-rail-conversation-click.ts';
3
- import {
4
- handleLeftRailPointerClick,
5
- type LeftRailPointerContext,
6
- } from '../mux/live-mux/left-rail-pointer.ts';
7
- import type { buildWorkspaceRailViewRows } from '../mux/workspace-rail-model.ts';
8
-
9
- export interface ConversationTitleClickState {
10
- readonly conversationId: string;
11
- readonly atMs: number;
12
- }
13
-
14
- interface LeftRailPointerInputOptions {
15
- readonly getLatestRailRows: () => ReturnType<typeof buildWorkspaceRailViewRows>;
16
- readonly hasConversationTitleEdit: () => boolean;
17
- readonly conversationTitleEditConversationId: () => string | null;
18
- readonly stopConversationTitleEdit: () => void;
19
- readonly hasSelection: () => boolean;
20
- readonly clearSelection: () => void;
21
- readonly activeConversationId: () => string | null;
22
- readonly repositoriesCollapsed: () => boolean;
23
- readonly clearConversationTitleEditClickState: () => void;
24
- readonly resolveDirectoryForAction: () => string | null;
25
- readonly openNewThreadPrompt: (directoryId: string) => void;
26
- readonly queueArchiveConversation: (conversationId: string) => void;
27
- readonly openAddDirectoryPrompt: () => void;
28
- readonly openRepositoryPromptForCreate: () => void;
29
- readonly repositoryExists: (repositoryId: string) => boolean;
30
- readonly openRepositoryPromptForEdit: (repositoryId: string) => void;
31
- readonly queueArchiveRepository: (repositoryId: string) => void;
32
- readonly toggleRepositoryGroup: (repositoryGroupId: string) => void;
33
- readonly selectLeftNavRepository: (repositoryGroupId: string) => void;
34
- readonly expandAllRepositoryGroups: () => void;
35
- readonly collapseAllRepositoryGroups: () => void;
36
- readonly enterHomePane: () => void;
37
- readonly queueCloseDirectory: (directoryId: string) => void;
38
- readonly toggleShortcutsCollapsed: () => void;
39
- readonly previousConversationClickState: () => ConversationTitleClickState | null;
40
- readonly setConversationClickState: (next: ConversationTitleClickState | null) => void;
41
- readonly nowMs: () => number;
42
- readonly conversationTitleEditDoubleClickWindowMs: number;
43
- readonly isConversationPaneActive: () => boolean;
44
- readonly ensureConversationPaneActive: (conversationId: string) => void;
45
- readonly beginConversationTitleEdit: (conversationId: string) => void;
46
- readonly queueActivateConversation: (conversationId: string) => void;
47
- readonly queueActivateConversationAndEdit: (conversationId: string) => void;
48
- readonly directoriesHas: (directoryId: string) => boolean;
49
- readonly enterProjectPane: (directoryId: string) => void;
50
- readonly markDirty: () => void;
51
- }
52
-
53
- interface LeftRailPointerInputDependencies {
54
- readonly handleLeftRailPointerClick?: typeof handleLeftRailPointerClick;
55
- readonly handleLeftRailActionClick?: typeof handleLeftRailActionClick;
56
- readonly handleLeftRailConversationClick?: typeof handleLeftRailConversationClick;
57
- }
58
-
59
- interface HandlePointerClickInput {
60
- readonly clickEligible: boolean;
61
- readonly paneRows: number;
62
- readonly leftCols: number;
63
- readonly pointerRow: number;
64
- readonly pointerCol: number;
65
- }
66
-
67
- export class LeftRailPointerInput {
68
- private readonly pointerClick: typeof handleLeftRailPointerClick;
69
- private readonly actionClick: typeof handleLeftRailActionClick;
70
- private readonly conversationClick: typeof handleLeftRailConversationClick;
71
-
72
- constructor(
73
- private readonly options: LeftRailPointerInputOptions,
74
- dependencies: LeftRailPointerInputDependencies = {},
75
- ) {
76
- this.pointerClick = dependencies.handleLeftRailPointerClick ?? handleLeftRailPointerClick;
77
- this.actionClick = dependencies.handleLeftRailActionClick ?? handleLeftRailActionClick;
78
- this.conversationClick =
79
- dependencies.handleLeftRailConversationClick ?? handleLeftRailConversationClick;
80
- }
81
-
82
- handlePointerClick(input: HandlePointerClickInput): boolean {
83
- return this.pointerClick({
84
- clickEligible: input.clickEligible,
85
- rows: this.options.getLatestRailRows(),
86
- paneRows: input.paneRows,
87
- leftCols: input.leftCols,
88
- pointerRow: input.pointerRow,
89
- pointerCol: input.pointerCol,
90
- hasConversationTitleEdit: this.options.hasConversationTitleEdit(),
91
- conversationTitleEditConversationId: this.options.conversationTitleEditConversationId(),
92
- stopConversationTitleEdit: this.options.stopConversationTitleEdit,
93
- hasSelection: this.options.hasSelection(),
94
- clearSelection: this.options.clearSelection,
95
- handleAction: (context) => this.handleAction(context),
96
- handleConversation: (context) => this.handleConversation(context),
97
- });
98
- }
99
-
100
- private handleAction(context: LeftRailPointerContext): boolean {
101
- return this.actionClick({
102
- action: context.selectedAction,
103
- selectedProjectId: context.selectedProjectId,
104
- selectedRepositoryId: context.selectedRepositoryId,
105
- activeConversationId: this.options.activeConversationId(),
106
- repositoriesCollapsed: this.options.repositoriesCollapsed(),
107
- clearConversationTitleEditClickState: this.options.clearConversationTitleEditClickState,
108
- resolveDirectoryForAction: this.options.resolveDirectoryForAction,
109
- openNewThreadPrompt: this.options.openNewThreadPrompt,
110
- queueArchiveConversation: this.options.queueArchiveConversation,
111
- openAddDirectoryPrompt: this.options.openAddDirectoryPrompt,
112
- openRepositoryPromptForCreate: this.options.openRepositoryPromptForCreate,
113
- repositoryExists: this.options.repositoryExists,
114
- openRepositoryPromptForEdit: this.options.openRepositoryPromptForEdit,
115
- queueArchiveRepository: this.options.queueArchiveRepository,
116
- toggleRepositoryGroup: this.options.toggleRepositoryGroup,
117
- selectLeftNavRepository: this.options.selectLeftNavRepository,
118
- expandAllRepositoryGroups: this.options.expandAllRepositoryGroups,
119
- collapseAllRepositoryGroups: this.options.collapseAllRepositoryGroups,
120
- enterHomePane: this.options.enterHomePane,
121
- queueCloseDirectory: this.options.queueCloseDirectory,
122
- toggleShortcutsCollapsed: this.options.toggleShortcutsCollapsed,
123
- markDirty: this.options.markDirty,
124
- });
125
- }
126
-
127
- private handleConversation(context: LeftRailPointerContext): void {
128
- this.conversationClick({
129
- selectedConversationId: context.selectedConversationId,
130
- selectedProjectId: context.selectedProjectId,
131
- supportsConversationTitleEditClick: context.supportsConversationTitleEditClick,
132
- previousClickState: this.options.previousConversationClickState(),
133
- nowMs: this.options.nowMs(),
134
- conversationTitleEditDoubleClickWindowMs:
135
- this.options.conversationTitleEditDoubleClickWindowMs,
136
- activeConversationId: this.options.activeConversationId(),
137
- isConversationPaneActive: this.options.isConversationPaneActive(),
138
- setConversationClickState: this.options.setConversationClickState,
139
- ensureConversationPaneActive: this.options.ensureConversationPaneActive,
140
- beginConversationTitleEdit: this.options.beginConversationTitleEdit,
141
- queueActivateConversation: this.options.queueActivateConversation,
142
- queueActivateConversationAndEdit: this.options.queueActivateConversationAndEdit,
143
- directoriesHas: this.options.directoriesHas,
144
- enterProjectPane: this.options.enterProjectPane,
145
- markDirty: this.options.markDirty,
146
- });
147
- }
148
- }
@@ -1,218 +0,0 @@
1
- import {
2
- buildCommandMenuModalOverlay as buildCommandMenuModalOverlayFrame,
3
- buildAddDirectoryModalOverlay as buildAddDirectoryModalOverlayFrame,
4
- buildApiKeyModalOverlay as buildApiKeyModalOverlayFrame,
5
- buildConversationTitleModalOverlay as buildConversationTitleModalOverlayFrame,
6
- buildNewThreadModalOverlay as buildNewThreadModalOverlayFrame,
7
- buildRepositoryModalOverlay as buildRepositoryModalOverlayFrame,
8
- buildTaskEditorModalOverlay as buildTaskEditorModalOverlayFrame,
9
- } from '../../mux/live-mux/modal-overlays.ts';
10
- import { dismissModalOnOutsideClick as dismissModalOnOutsideClickFrame } from '../../mux/live-mux/modal-pointer.ts';
11
- import type {
12
- CommandMenuActionDescriptor,
13
- CommandMenuState,
14
- } from '../../mux/live-mux/command-menu.ts';
15
- import type { createNewThreadPromptState } from '../../mux/new-thread-prompt.ts';
16
- import type {
17
- ConversationTitleEditState,
18
- RepositoryPromptState,
19
- TaskEditorPromptState,
20
- } from '../../domain/workspace.ts';
21
- import { isUiModalOverlayHit } from '../kit.ts';
22
-
23
- type NewThreadPromptState = ReturnType<typeof createNewThreadPromptState>;
24
- type AddDirectoryPromptState = { value: string; error: string | null };
25
- type ApiKeyPromptState = {
26
- keyName: string;
27
- displayName: string;
28
- value: string;
29
- error: string | null;
30
- hasExistingValue: boolean;
31
- };
32
- type ModalOverlay = Exclude<ReturnType<typeof buildNewThreadModalOverlayFrame>, null>;
33
- type ModalTheme = Parameters<typeof buildNewThreadModalOverlayFrame>[3];
34
- type DismissModalOnOutsideClickInput = Parameters<typeof dismissModalOnOutsideClickFrame>[0];
35
-
36
- interface ModalManagerOptions {
37
- readonly theme: ModalTheme;
38
- readonly resolveRepositoryName: (repositoryId: string) => string | null;
39
- readonly getCommandMenu: () => CommandMenuState | null;
40
- readonly resolveCommandMenuActions: () => readonly CommandMenuActionDescriptor[];
41
- readonly getNewThreadPrompt: () => NewThreadPromptState | null;
42
- readonly getAddDirectoryPrompt: () => AddDirectoryPromptState | null;
43
- readonly getApiKeyPrompt?: () => ApiKeyPromptState | null;
44
- readonly getTaskEditorPrompt: () => TaskEditorPromptState | null;
45
- readonly getRepositoryPrompt: () => RepositoryPromptState | null;
46
- readonly getConversationTitleEdit: () => ConversationTitleEditState | null;
47
- }
48
-
49
- interface ModalManagerDependencies {
50
- readonly buildCommandMenuModalOverlay?: typeof buildCommandMenuModalOverlayFrame;
51
- readonly buildNewThreadModalOverlay?: typeof buildNewThreadModalOverlayFrame;
52
- readonly buildAddDirectoryModalOverlay?: typeof buildAddDirectoryModalOverlayFrame;
53
- readonly buildTaskEditorModalOverlay?: typeof buildTaskEditorModalOverlayFrame;
54
- readonly buildApiKeyModalOverlay?: typeof buildApiKeyModalOverlayFrame;
55
- readonly buildRepositoryModalOverlay?: typeof buildRepositoryModalOverlayFrame;
56
- readonly buildConversationTitleModalOverlay?: typeof buildConversationTitleModalOverlayFrame;
57
- readonly dismissModalOnOutsideClick?: typeof dismissModalOnOutsideClickFrame;
58
- readonly isOverlayHit?: typeof isUiModalOverlayHit;
59
- }
60
-
61
- interface ModalDismissInput {
62
- readonly input: Buffer;
63
- readonly inputRemainder: string;
64
- readonly layoutCols: number;
65
- readonly viewportRows: number;
66
- readonly dismiss: () => void;
67
- readonly onInsidePointerPress?: (col: number, row: number) => boolean;
68
- }
69
-
70
- interface ModalDismissResult {
71
- readonly handled: boolean;
72
- readonly inputRemainder: string;
73
- }
74
-
75
- export class ModalManager {
76
- private readonly buildCommandMenuModalOverlay: typeof buildCommandMenuModalOverlayFrame;
77
- private readonly buildNewThreadModalOverlay: typeof buildNewThreadModalOverlayFrame;
78
- private readonly buildAddDirectoryModalOverlay: typeof buildAddDirectoryModalOverlayFrame;
79
- private readonly buildTaskEditorModalOverlay: typeof buildTaskEditorModalOverlayFrame;
80
- private readonly buildApiKeyModalOverlay: typeof buildApiKeyModalOverlayFrame;
81
- private readonly buildRepositoryModalOverlay: typeof buildRepositoryModalOverlayFrame;
82
- private readonly buildConversationTitleModalOverlay: typeof buildConversationTitleModalOverlayFrame;
83
- private readonly dismissModalOnOutsideClick: typeof dismissModalOnOutsideClickFrame;
84
- private readonly isOverlayHit: typeof isUiModalOverlayHit;
85
-
86
- constructor(
87
- private readonly options: ModalManagerOptions,
88
- dependencies: ModalManagerDependencies = {},
89
- ) {
90
- this.buildCommandMenuModalOverlay =
91
- dependencies.buildCommandMenuModalOverlay ?? buildCommandMenuModalOverlayFrame;
92
- this.buildNewThreadModalOverlay =
93
- dependencies.buildNewThreadModalOverlay ?? buildNewThreadModalOverlayFrame;
94
- this.buildAddDirectoryModalOverlay =
95
- dependencies.buildAddDirectoryModalOverlay ?? buildAddDirectoryModalOverlayFrame;
96
- this.buildTaskEditorModalOverlay =
97
- dependencies.buildTaskEditorModalOverlay ?? buildTaskEditorModalOverlayFrame;
98
- this.buildApiKeyModalOverlay =
99
- dependencies.buildApiKeyModalOverlay ?? buildApiKeyModalOverlayFrame;
100
- this.buildRepositoryModalOverlay =
101
- dependencies.buildRepositoryModalOverlay ?? buildRepositoryModalOverlayFrame;
102
- this.buildConversationTitleModalOverlay =
103
- dependencies.buildConversationTitleModalOverlay ?? buildConversationTitleModalOverlayFrame;
104
- this.dismissModalOnOutsideClick =
105
- dependencies.dismissModalOnOutsideClick ?? dismissModalOnOutsideClickFrame;
106
- this.isOverlayHit = dependencies.isOverlayHit ?? isUiModalOverlayHit;
107
- }
108
-
109
- buildCommandMenuOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
110
- return this.buildCommandMenuModalOverlay(
111
- layoutCols,
112
- viewportRows,
113
- this.options.getCommandMenu(),
114
- this.options.resolveCommandMenuActions(),
115
- this.options.theme,
116
- );
117
- }
118
-
119
- buildNewThreadOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
120
- return this.buildNewThreadModalOverlay(
121
- layoutCols,
122
- viewportRows,
123
- this.options.getNewThreadPrompt(),
124
- this.options.theme,
125
- );
126
- }
127
-
128
- buildAddDirectoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
129
- return this.buildAddDirectoryModalOverlay(
130
- layoutCols,
131
- viewportRows,
132
- this.options.getAddDirectoryPrompt(),
133
- this.options.theme,
134
- );
135
- }
136
-
137
- buildTaskEditorOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
138
- return this.buildTaskEditorModalOverlay(
139
- layoutCols,
140
- viewportRows,
141
- this.options.getTaskEditorPrompt(),
142
- this.options.resolveRepositoryName,
143
- this.options.theme,
144
- );
145
- }
146
-
147
- buildApiKeyOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
148
- return this.buildApiKeyModalOverlay(
149
- layoutCols,
150
- viewportRows,
151
- this.options.getApiKeyPrompt?.() ?? null,
152
- this.options.theme,
153
- );
154
- }
155
-
156
- buildRepositoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
157
- return this.buildRepositoryModalOverlay(
158
- layoutCols,
159
- viewportRows,
160
- this.options.getRepositoryPrompt(),
161
- this.options.theme,
162
- );
163
- }
164
-
165
- buildConversationTitleOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
166
- return this.buildConversationTitleModalOverlay(
167
- layoutCols,
168
- viewportRows,
169
- this.options.getConversationTitleEdit(),
170
- this.options.theme,
171
- );
172
- }
173
-
174
- buildCurrentOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
175
- const commandMenuOverlay = this.buildCommandMenuOverlay(layoutCols, viewportRows);
176
- if (commandMenuOverlay !== null) {
177
- return commandMenuOverlay;
178
- }
179
- const newThreadOverlay = this.buildNewThreadOverlay(layoutCols, viewportRows);
180
- if (newThreadOverlay !== null) {
181
- return newThreadOverlay;
182
- }
183
- const addDirectoryOverlay = this.buildAddDirectoryOverlay(layoutCols, viewportRows);
184
- if (addDirectoryOverlay !== null) {
185
- return addDirectoryOverlay;
186
- }
187
- const taskEditorOverlay = this.buildTaskEditorOverlay(layoutCols, viewportRows);
188
- if (taskEditorOverlay !== null) {
189
- return taskEditorOverlay;
190
- }
191
- const apiKeyOverlay = this.buildApiKeyOverlay(layoutCols, viewportRows);
192
- if (apiKeyOverlay !== null) {
193
- return apiKeyOverlay;
194
- }
195
- const repositoryOverlay = this.buildRepositoryOverlay(layoutCols, viewportRows);
196
- if (repositoryOverlay !== null) {
197
- return repositoryOverlay;
198
- }
199
- return this.buildConversationTitleOverlay(layoutCols, viewportRows);
200
- }
201
-
202
- dismissOnOutsideClick(input: ModalDismissInput): ModalDismissResult {
203
- const dismissInput: DismissModalOnOutsideClickInput = {
204
- input: input.input,
205
- inputRemainder: input.inputRemainder,
206
- dismiss: input.dismiss,
207
- buildCurrentModalOverlay: () =>
208
- this.buildCurrentOverlay(input.layoutCols, input.viewportRows),
209
- isOverlayHit: this.isOverlayHit,
210
- ...(input.onInsidePointerPress === undefined
211
- ? {}
212
- : {
213
- onInsidePointerPress: input.onInsidePointerPress,
214
- }),
215
- };
216
- return this.dismissModalOnOutsideClick(dismissInput);
217
- }
218
- }
@@ -1,91 +0,0 @@
1
- import {
2
- reduceRepositoryFoldChordInput,
3
- repositoryTreeArrowAction,
4
- } from '../mux/live-mux/repository-folding.ts';
5
- import type { LeftNavSelection } from '../mux/live-mux/left-nav.ts';
6
-
7
- interface RepositoryFoldInputOptions {
8
- readonly getLeftNavSelection: () => LeftNavSelection;
9
- readonly getRepositoryToggleChordPrefixAtMs: () => number | null;
10
- readonly setRepositoryToggleChordPrefixAtMs: (value: number | null) => void;
11
- readonly conversations: ReadonlyMap<string, { directoryId: string | null }>;
12
- readonly repositoryGroupIdForDirectory: (directoryId: string) => string;
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
- readonly chordTimeoutMs: number;
20
- readonly collapseAllChordPrefix: Buffer;
21
- readonly nowMs: () => number;
22
- }
23
-
24
- export class RepositoryFoldInput {
25
- constructor(private readonly options: RepositoryFoldInputOptions) {}
26
-
27
- private selectedRepositoryGroupId(): string | null {
28
- const leftNavSelection = this.options.getLeftNavSelection();
29
- if (leftNavSelection.kind === 'repository') {
30
- return leftNavSelection.repositoryId;
31
- }
32
- if (leftNavSelection.kind === 'project') {
33
- return this.options.repositoryGroupIdForDirectory(leftNavSelection.directoryId);
34
- }
35
- if (leftNavSelection.kind === 'conversation') {
36
- const conversation = this.options.conversations.get(leftNavSelection.sessionId);
37
- if (conversation?.directoryId !== null && conversation?.directoryId !== undefined) {
38
- return this.options.repositoryGroupIdForDirectory(conversation.directoryId);
39
- }
40
- }
41
- return null;
42
- }
43
-
44
- handleRepositoryTreeArrow(input: Buffer): boolean {
45
- const repositoryId = this.selectedRepositoryGroupId();
46
- const action = repositoryTreeArrowAction(
47
- input,
48
- this.options.getLeftNavSelection(),
49
- repositoryId,
50
- );
51
- if (repositoryId === null || action === null) {
52
- return false;
53
- }
54
- if (action === 'expand') {
55
- this.options.expandRepositoryGroup(repositoryId);
56
- this.options.selectLeftNavRepository(repositoryId);
57
- this.options.markDirty();
58
- return true;
59
- }
60
- if (action === 'collapse') {
61
- this.options.collapseRepositoryGroup(repositoryId);
62
- this.options.selectLeftNavRepository(repositoryId);
63
- this.options.markDirty();
64
- return true;
65
- }
66
- return false;
67
- }
68
-
69
- handleRepositoryFoldChords(input: Buffer): boolean {
70
- const reduced = reduceRepositoryFoldChordInput({
71
- input,
72
- leftNavSelection: this.options.getLeftNavSelection(),
73
- nowMs: this.options.nowMs(),
74
- prefixAtMs: this.options.getRepositoryToggleChordPrefixAtMs(),
75
- chordTimeoutMs: this.options.chordTimeoutMs,
76
- collapseAllChordPrefix: this.options.collapseAllChordPrefix,
77
- });
78
- this.options.setRepositoryToggleChordPrefixAtMs(reduced.nextPrefixAtMs);
79
- if (reduced.action === 'expand-all') {
80
- this.options.expandAllRepositoryGroups();
81
- this.options.markDirty();
82
- return true;
83
- }
84
- if (reduced.action === 'collapse-all') {
85
- this.options.collapseAllRepositoryGroups();
86
- this.options.markDirty();
87
- return true;
88
- }
89
- return reduced.consumed;
90
- }
91
- }
package/src/ui/surface.ts DELETED
@@ -1,224 +0,0 @@
1
- import { measureDisplayWidth } from '../terminal/snapshot-oracle.ts';
2
-
3
- export type UiColor =
4
- | { kind: 'default' }
5
- | { kind: 'indexed'; index: number }
6
- | { kind: 'rgb'; r: number; g: number; b: number };
7
-
8
- export interface UiStyle {
9
- readonly fg: UiColor;
10
- readonly bg: UiColor;
11
- readonly bold: boolean;
12
- }
13
-
14
- interface UiCell {
15
- glyph: string;
16
- continued: boolean;
17
- style: UiStyle;
18
- }
19
-
20
- export interface UiSurface {
21
- readonly cols: number;
22
- readonly rows: number;
23
- readonly baseStyle: UiStyle;
24
- readonly cells: UiCell[];
25
- }
26
-
27
- const DEFAULT_COLOR: UiColor = {
28
- kind: 'default',
29
- };
30
-
31
- export const DEFAULT_UI_STYLE: UiStyle = {
32
- fg: DEFAULT_COLOR,
33
- bg: DEFAULT_COLOR,
34
- bold: false,
35
- };
36
-
37
- function cloneColor(color: UiColor): UiColor {
38
- if (color.kind === 'default') {
39
- return DEFAULT_COLOR;
40
- }
41
- if (color.kind === 'indexed') {
42
- return {
43
- kind: 'indexed',
44
- index: color.index,
45
- };
46
- }
47
- return {
48
- kind: 'rgb',
49
- r: color.r,
50
- g: color.g,
51
- b: color.b,
52
- };
53
- }
54
-
55
- function cloneStyle(style: UiStyle): UiStyle {
56
- return {
57
- fg: cloneColor(style.fg),
58
- bg: cloneColor(style.bg),
59
- bold: style.bold,
60
- };
61
- }
62
-
63
- function styleEqual(left: UiStyle, right: UiStyle): boolean {
64
- if (left.bold !== right.bold) {
65
- return false;
66
- }
67
- if (left.fg.kind !== right.fg.kind || left.bg.kind !== right.bg.kind) {
68
- return false;
69
- }
70
-
71
- if (left.fg.kind === 'indexed') {
72
- if (left.fg.index !== (right.fg as Extract<UiColor, { kind: 'indexed' }>).index) {
73
- return false;
74
- }
75
- } else if (left.fg.kind === 'rgb') {
76
- const typedRight = right.fg as Extract<UiColor, { kind: 'rgb' }>;
77
- if (left.fg.r !== typedRight.r || left.fg.g !== typedRight.g || left.fg.b !== typedRight.b) {
78
- return false;
79
- }
80
- }
81
-
82
- if (left.bg.kind === 'indexed') {
83
- if (left.bg.index !== (right.bg as Extract<UiColor, { kind: 'indexed' }>).index) {
84
- return false;
85
- }
86
- } else if (left.bg.kind === 'rgb') {
87
- const typedRight = right.bg as Extract<UiColor, { kind: 'rgb' }>;
88
- if (left.bg.r !== typedRight.r || left.bg.g !== typedRight.g || left.bg.b !== typedRight.b) {
89
- return false;
90
- }
91
- }
92
-
93
- return true;
94
- }
95
-
96
- function createCell(style: UiStyle): UiCell {
97
- return {
98
- glyph: ' ',
99
- continued: false,
100
- style: cloneStyle(style),
101
- };
102
- }
103
-
104
- function cellOffset(surface: UiSurface, col: number, row: number): number {
105
- return row * surface.cols + col;
106
- }
107
-
108
- function colorSgrCodes(color: UiColor, target: 'fg' | 'bg'): readonly string[] {
109
- const prefix = target === 'fg' ? '38' : '48';
110
- if (color.kind === 'default') {
111
- return [target === 'fg' ? '39' : '49'];
112
- }
113
- if (color.kind === 'indexed') {
114
- return [prefix, '5', String(color.index)];
115
- }
116
- return [prefix, '2', String(color.r), String(color.g), String(color.b)];
117
- }
118
-
119
- function styleToSgr(style: UiStyle): string {
120
- const codes: string[] = ['0'];
121
- if (style.bold) {
122
- codes.push('1');
123
- }
124
- codes.push(...colorSgrCodes(style.fg, 'fg'));
125
- codes.push(...colorSgrCodes(style.bg, 'bg'));
126
- return `\u001b[${codes.join(';')}m`;
127
- }
128
-
129
- export function createUiSurface(
130
- cols: number,
131
- rows: number,
132
- baseStyle: UiStyle = DEFAULT_UI_STYLE,
133
- ): UiSurface {
134
- const safeCols = Math.max(1, cols);
135
- const safeRows = Math.max(1, rows);
136
- const typedBaseStyle = cloneStyle(baseStyle);
137
- return {
138
- cols: safeCols,
139
- rows: safeRows,
140
- baseStyle: typedBaseStyle,
141
- cells: Array.from({ length: safeCols * safeRows }, () => createCell(typedBaseStyle)),
142
- };
143
- }
144
-
145
- export function fillUiRow(surface: UiSurface, row: number, style: UiStyle): void {
146
- if (row < 0 || row >= surface.rows) {
147
- return;
148
- }
149
- const typedStyle = cloneStyle(style);
150
- for (let col = 0; col < surface.cols; col += 1) {
151
- const cell = surface.cells[cellOffset(surface, col, row)]!;
152
- cell.glyph = ' ';
153
- cell.continued = false;
154
- cell.style = typedStyle;
155
- }
156
- }
157
-
158
- export function drawUiText(
159
- surface: UiSurface,
160
- colStart: number,
161
- row: number,
162
- text: string,
163
- style: UiStyle = surface.baseStyle,
164
- ): void {
165
- if (row < 0 || row >= surface.rows || colStart >= surface.cols) {
166
- return;
167
- }
168
- let col = Math.max(0, colStart);
169
- const typedStyle = cloneStyle(style);
170
-
171
- for (const glyph of text) {
172
- const width = Math.max(0, measureDisplayWidth(glyph));
173
- if (width === 0) {
174
- continue;
175
- }
176
- if (col >= surface.cols) {
177
- break;
178
- }
179
-
180
- if (width === 1) {
181
- const cell = surface.cells[cellOffset(surface, col, row)]!;
182
- cell.glyph = glyph;
183
- cell.continued = false;
184
- cell.style = typedStyle;
185
- col += 1;
186
- continue;
187
- }
188
-
189
- if (col + width > surface.cols) {
190
- break;
191
- }
192
-
193
- const first = surface.cells[cellOffset(surface, col, row)]!;
194
- first.glyph = glyph;
195
- first.continued = false;
196
- first.style = typedStyle;
197
- for (let offset = 1; offset < width && col + offset < surface.cols; offset += 1) {
198
- const cell = surface.cells[cellOffset(surface, col + offset, row)]!;
199
- cell.glyph = '';
200
- cell.continued = true;
201
- cell.style = typedStyle;
202
- }
203
- col += width;
204
- }
205
- }
206
-
207
- export function renderUiSurfaceAnsiRows(surface: UiSurface): readonly string[] {
208
- const rows: string[] = [];
209
- for (let row = 0; row < surface.rows; row += 1) {
210
- let output = '';
211
- let lastStyle: UiStyle | null = null;
212
- for (let col = 0; col < surface.cols; col += 1) {
213
- const cell = surface.cells[cellOffset(surface, col, row)]!;
214
- if (lastStyle === null || !styleEqual(lastStyle, cell.style)) {
215
- output += styleToSgr(cell.style);
216
- lastStyle = cell.style;
217
- }
218
- output += cell.continued ? '' : cell.glyph.length > 0 ? cell.glyph : ' ';
219
- }
220
- output += '\u001b[0m';
221
- rows.push(output);
222
- }
223
- return rows;
224
- }