@jmoyers/harness 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/native/ptyd/Cargo.lock +16 -0
  4. package/native/ptyd/Cargo.toml +7 -0
  5. package/native/ptyd/src/main.rs +257 -0
  6. package/package.json +90 -0
  7. package/scripts/build-ptyd.sh +73 -0
  8. package/scripts/control-plane-daemon.ts +277 -0
  9. package/scripts/cursor-hook-relay.ts +82 -0
  10. package/scripts/harness-animate.ts +469 -0
  11. package/scripts/harness-bin.js +77 -0
  12. package/scripts/harness-core.ts +1 -0
  13. package/scripts/harness-inspector.ts +439 -0
  14. package/scripts/harness.ts +2493 -0
  15. package/src/adapters/agent-session-state.ts +390 -0
  16. package/src/cli/gateway-record.ts +173 -0
  17. package/src/codex/live-session.ts +872 -0
  18. package/src/config/config-core.ts +1359 -0
  19. package/src/config/secrets-core.ts +170 -0
  20. package/src/control-plane/agent-realtime-api.ts +2441 -0
  21. package/src/control-plane/codex-session-stream.ts +392 -0
  22. package/src/control-plane/codex-telemetry.ts +1325 -0
  23. package/src/control-plane/lifecycle-hooks.ts +706 -0
  24. package/src/control-plane/session-summary.ts +380 -0
  25. package/src/control-plane/status/agent-status-reducer.ts +21 -0
  26. package/src/control-plane/status/reducer-base.ts +170 -0
  27. package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
  28. package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
  29. package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
  30. package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
  31. package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
  32. package/src/control-plane/status/session-status-engine.ts +76 -0
  33. package/src/control-plane/stream-client.ts +396 -0
  34. package/src/control-plane/stream-command-parser.ts +1673 -0
  35. package/src/control-plane/stream-protocol.ts +1808 -0
  36. package/src/control-plane/stream-server-background.ts +486 -0
  37. package/src/control-plane/stream-server-command.ts +2557 -0
  38. package/src/control-plane/stream-server-connection.ts +234 -0
  39. package/src/control-plane/stream-server-observed-filter.ts +112 -0
  40. package/src/control-plane/stream-server-session-runtime.ts +566 -0
  41. package/src/control-plane/stream-server-state-store.ts +15 -0
  42. package/src/control-plane/stream-server.ts +3192 -0
  43. package/src/cursor/managed-hooks.ts +282 -0
  44. package/src/domain/conversations.ts +414 -0
  45. package/src/domain/directories.ts +78 -0
  46. package/src/domain/repositories.ts +123 -0
  47. package/src/domain/tasks.ts +148 -0
  48. package/src/domain/workspace.ts +156 -0
  49. package/src/events/normalized-events.ts +124 -0
  50. package/src/mux/ansi-integrity.ts +103 -0
  51. package/src/mux/control-plane-op-queue.ts +212 -0
  52. package/src/mux/conversation-rail.ts +339 -0
  53. package/src/mux/double-click.ts +78 -0
  54. package/src/mux/dual-pane-core.ts +435 -0
  55. package/src/mux/harness-core-ui.ts +817 -0
  56. package/src/mux/input-shortcuts.ts +667 -0
  57. package/src/mux/live-mux/actions-conversation.ts +344 -0
  58. package/src/mux/live-mux/actions-repository.ts +246 -0
  59. package/src/mux/live-mux/actions-task.ts +115 -0
  60. package/src/mux/live-mux/args.ts +142 -0
  61. package/src/mux/live-mux/command-menu.ts +298 -0
  62. package/src/mux/live-mux/control-plane-records.ts +546 -0
  63. package/src/mux/live-mux/conversation-state.ts +188 -0
  64. package/src/mux/live-mux/directory-resolution.ts +34 -0
  65. package/src/mux/live-mux/event-mapping.ts +96 -0
  66. package/src/mux/live-mux/gateway-profiler.ts +152 -0
  67. package/src/mux/live-mux/gateway-render-trace.ts +177 -0
  68. package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
  69. package/src/mux/live-mux/git-parsing.ts +131 -0
  70. package/src/mux/live-mux/git-snapshot.ts +263 -0
  71. package/src/mux/live-mux/git-state.ts +136 -0
  72. package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
  73. package/src/mux/live-mux/home-pane-actions.ts +58 -0
  74. package/src/mux/live-mux/home-pane-drop.ts +44 -0
  75. package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
  76. package/src/mux/live-mux/home-pane-pointer.ts +96 -0
  77. package/src/mux/live-mux/input-forwarding.ts +112 -0
  78. package/src/mux/live-mux/layout.ts +30 -0
  79. package/src/mux/live-mux/left-nav-activation.ts +103 -0
  80. package/src/mux/live-mux/left-nav.ts +85 -0
  81. package/src/mux/live-mux/left-rail-actions.ts +118 -0
  82. package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
  83. package/src/mux/live-mux/left-rail-pointer.ts +74 -0
  84. package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
  85. package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
  86. package/src/mux/live-mux/modal-input-reducers.ts +94 -0
  87. package/src/mux/live-mux/modal-overlays.ts +287 -0
  88. package/src/mux/live-mux/modal-pointer.ts +70 -0
  89. package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
  90. package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
  91. package/src/mux/live-mux/observed-stream.ts +87 -0
  92. package/src/mux/live-mux/palette-parsing.ts +128 -0
  93. package/src/mux/live-mux/pointer-routing.ts +108 -0
  94. package/src/mux/live-mux/process-usage.ts +53 -0
  95. package/src/mux/live-mux/project-pane-pointer.ts +44 -0
  96. package/src/mux/live-mux/rail-layout.ts +244 -0
  97. package/src/mux/live-mux/render-trace-analysis.ts +213 -0
  98. package/src/mux/live-mux/render-trace-state.ts +84 -0
  99. package/src/mux/live-mux/repository-folding.ts +207 -0
  100. package/src/mux/live-mux/runtime-shutdown.ts +51 -0
  101. package/src/mux/live-mux/selection.ts +411 -0
  102. package/src/mux/live-mux/startup-utils.ts +187 -0
  103. package/src/mux/live-mux/status-timeline-state.ts +82 -0
  104. package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
  105. package/src/mux/live-mux/terminal-palette.ts +79 -0
  106. package/src/mux/new-thread-prompt.ts +165 -0
  107. package/src/mux/project-tree.ts +295 -0
  108. package/src/mux/render-frame.ts +113 -0
  109. package/src/mux/runtime-wiring.ts +185 -0
  110. package/src/mux/selector-index.ts +160 -0
  111. package/src/mux/startup-sequencer.ts +238 -0
  112. package/src/mux/task-composer.ts +289 -0
  113. package/src/mux/task-focused-pane.ts +417 -0
  114. package/src/mux/task-screen-keybindings.ts +539 -0
  115. package/src/mux/terminal-input-modes.ts +35 -0
  116. package/src/mux/workspace-path.ts +55 -0
  117. package/src/mux/workspace-rail-model.ts +701 -0
  118. package/src/mux/workspace-rail.ts +247 -0
  119. package/src/perf/perf-core.ts +307 -0
  120. package/src/pty/pty_host.ts +217 -0
  121. package/src/pty/session-broker.ts +158 -0
  122. package/src/recording/terminal-recording.ts +383 -0
  123. package/src/services/control-plane.ts +567 -0
  124. package/src/services/conversation-lifecycle.ts +176 -0
  125. package/src/services/conversation-startup-hydration.ts +47 -0
  126. package/src/services/directory-hydration.ts +49 -0
  127. package/src/services/event-persistence.ts +104 -0
  128. package/src/services/mux-ui-state-persistence.ts +82 -0
  129. package/src/services/output-load-sampler.ts +231 -0
  130. package/src/services/process-usage-refresh.ts +88 -0
  131. package/src/services/recording.ts +75 -0
  132. package/src/services/render-trace-recorder.ts +177 -0
  133. package/src/services/runtime-control-actions.ts +123 -0
  134. package/src/services/runtime-control-plane-ops.ts +131 -0
  135. package/src/services/runtime-conversation-actions.ts +113 -0
  136. package/src/services/runtime-conversation-activation.ts +78 -0
  137. package/src/services/runtime-conversation-starter.ts +171 -0
  138. package/src/services/runtime-conversation-title-edit.ts +149 -0
  139. package/src/services/runtime-directory-actions.ts +164 -0
  140. package/src/services/runtime-envelope-handler.ts +198 -0
  141. package/src/services/runtime-git-state.ts +92 -0
  142. package/src/services/runtime-input-pipeline.ts +50 -0
  143. package/src/services/runtime-input-router.ts +202 -0
  144. package/src/services/runtime-layout-resize.ts +236 -0
  145. package/src/services/runtime-left-rail-render.ts +159 -0
  146. package/src/services/runtime-main-pane-input.ts +230 -0
  147. package/src/services/runtime-modal-input.ts +119 -0
  148. package/src/services/runtime-navigation-input.ts +207 -0
  149. package/src/services/runtime-process-wiring.ts +68 -0
  150. package/src/services/runtime-rail-input.ts +287 -0
  151. package/src/services/runtime-render-flush.ts +146 -0
  152. package/src/services/runtime-render-lifecycle.ts +104 -0
  153. package/src/services/runtime-render-orchestrator.ts +108 -0
  154. package/src/services/runtime-render-pipeline.ts +167 -0
  155. package/src/services/runtime-render-state.ts +72 -0
  156. package/src/services/runtime-repository-actions.ts +197 -0
  157. package/src/services/runtime-right-pane-render.ts +132 -0
  158. package/src/services/runtime-shutdown.ts +79 -0
  159. package/src/services/runtime-stream-subscriptions.ts +56 -0
  160. package/src/services/runtime-task-composer-persistence.ts +139 -0
  161. package/src/services/runtime-task-editor-actions.ts +83 -0
  162. package/src/services/runtime-task-pane-actions.ts +198 -0
  163. package/src/services/runtime-task-pane-shortcuts.ts +189 -0
  164. package/src/services/runtime-task-pane.ts +62 -0
  165. package/src/services/runtime-workspace-actions.ts +153 -0
  166. package/src/services/runtime-workspace-observed-events.ts +190 -0
  167. package/src/services/session-projection-instrumentation.ts +190 -0
  168. package/src/services/startup-background-probe.ts +91 -0
  169. package/src/services/startup-background-resume.ts +65 -0
  170. package/src/services/startup-orchestrator.ts +166 -0
  171. package/src/services/startup-output-tracker.ts +54 -0
  172. package/src/services/startup-paint-tracker.ts +115 -0
  173. package/src/services/startup-persisted-conversation-queue.ts +45 -0
  174. package/src/services/startup-settled-gate.ts +67 -0
  175. package/src/services/startup-shutdown.ts +53 -0
  176. package/src/services/startup-span-tracker.ts +77 -0
  177. package/src/services/startup-state-hydration.ts +94 -0
  178. package/src/services/startup-visibility.ts +35 -0
  179. package/src/services/status-timeline-recorder.ts +144 -0
  180. package/src/services/task-pane-selection-actions.ts +153 -0
  181. package/src/services/task-planning-hydration.ts +58 -0
  182. package/src/services/task-planning-observed-events.ts +89 -0
  183. package/src/services/workspace-observed-events.ts +113 -0
  184. package/src/store/control-plane-store-normalize.ts +760 -0
  185. package/src/store/control-plane-store-types.ts +224 -0
  186. package/src/store/control-plane-store.ts +2951 -0
  187. package/src/store/event-store.ts +253 -0
  188. package/src/store/sqlite.ts +81 -0
  189. package/src/terminal/compat-matrix.ts +345 -0
  190. package/src/terminal/differential-checkpoints.ts +132 -0
  191. package/src/terminal/parity-suite.ts +441 -0
  192. package/src/terminal/snapshot-oracle.ts +1840 -0
  193. package/src/ui/conversation-input-forwarder.ts +114 -0
  194. package/src/ui/conversation-selection-input.ts +103 -0
  195. package/src/ui/debug-footer-notice.ts +39 -0
  196. package/src/ui/global-shortcut-input.ts +126 -0
  197. package/src/ui/input-preflight.ts +68 -0
  198. package/src/ui/input-token-router.ts +312 -0
  199. package/src/ui/input.ts +238 -0
  200. package/src/ui/kit.ts +509 -0
  201. package/src/ui/left-nav-input.ts +80 -0
  202. package/src/ui/left-rail-pointer-input.ts +148 -0
  203. package/src/ui/main-pane-pointer-input.ts +150 -0
  204. package/src/ui/modals/manager.ts +192 -0
  205. package/src/ui/mux-theme.ts +529 -0
  206. package/src/ui/panes/conversation.ts +19 -0
  207. package/src/ui/panes/home-gridfire.ts +302 -0
  208. package/src/ui/panes/home.ts +109 -0
  209. package/src/ui/panes/left-rail.ts +12 -0
  210. package/src/ui/panes/project.ts +44 -0
  211. package/src/ui/pointer-routing-input.ts +158 -0
  212. package/src/ui/repository-fold-input.ts +91 -0
  213. package/src/ui/screen.ts +210 -0
  214. package/src/ui/surface.ts +224 -0
@@ -0,0 +1,148 @@
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
+ }
@@ -0,0 +1,150 @@
1
+ import { hasAltModifier, isLeftButtonPress, isMotionMouseCode } from '../mux/live-mux/selection.ts';
2
+ import { handleHomePanePointerClick } from '../mux/live-mux/home-pane-pointer.ts';
3
+ import { handleProjectPaneActionClick } from '../mux/live-mux/project-pane-pointer.ts';
4
+
5
+ interface EntityDoubleClickState {
6
+ readonly entityId: string;
7
+ readonly atMs: number;
8
+ }
9
+
10
+ interface HomePaneDragState {
11
+ readonly kind: 'task' | 'repository';
12
+ readonly itemId: string;
13
+ readonly startedRowIndex: number;
14
+ readonly latestRowIndex: number;
15
+ readonly hasDragged: boolean;
16
+ }
17
+
18
+ type MainPaneMode = 'conversation' | 'project' | 'home';
19
+ type PointerTarget = 'left' | 'right' | 'separator' | 'status' | 'outside';
20
+
21
+ interface MainPanePointerInputOptions<TProjectSnapshot extends { directoryId: string }> {
22
+ readonly getMainPaneMode: () => MainPaneMode;
23
+ readonly getProjectPaneSnapshot: () => TProjectSnapshot | null;
24
+ readonly getProjectPaneScrollTop: () => number;
25
+ readonly projectPaneActionAtRow: (
26
+ snapshot: TProjectSnapshot,
27
+ rightCols: number,
28
+ paneRows: number,
29
+ projectPaneScrollTop: number,
30
+ rowIndex: number,
31
+ ) => string | null;
32
+ readonly openNewThreadPrompt: (directoryId: string) => void;
33
+ readonly queueCloseDirectory: (directoryId: string) => void;
34
+ readonly actionAtCell: (rowIndex: number, colIndex: number) => string | null;
35
+ readonly actionAtRow: (rowIndex: number) => string | null;
36
+ readonly clearTaskEditClickState: () => void;
37
+ readonly clearRepositoryEditClickState: () => void;
38
+ readonly clearHomePaneDragState: () => void;
39
+ readonly getTaskRepositoryDropdownOpen: () => boolean;
40
+ readonly setTaskRepositoryDropdownOpen: (open: boolean) => void;
41
+ readonly taskIdAtRow: (rowIndex: number) => string | null;
42
+ readonly repositoryIdAtRow: (rowIndex: number) => string | null;
43
+ readonly selectTaskById: (taskId: string) => void;
44
+ readonly selectRepositoryById: (repositoryId: string) => void;
45
+ readonly runTaskPaneAction: (action: 'task.ready' | 'task.draft' | 'task.complete') => void;
46
+ readonly nowMs: () => number;
47
+ readonly homePaneEditDoubleClickWindowMs: number;
48
+ readonly getTaskEditClickState: () => EntityDoubleClickState | null;
49
+ readonly getRepositoryEditClickState: () => EntityDoubleClickState | null;
50
+ readonly clearTaskPaneNotice: () => void;
51
+ readonly setTaskEditClickState: (next: EntityDoubleClickState | null) => void;
52
+ readonly setRepositoryEditClickState: (next: EntityDoubleClickState | null) => void;
53
+ readonly setHomePaneDragState: (next: HomePaneDragState | null) => void;
54
+ readonly openTaskEditPrompt: (taskId: string) => void;
55
+ readonly openRepositoryPromptForEdit: (repositoryId: string) => void;
56
+ readonly markDirty: () => void;
57
+ }
58
+
59
+ interface MainPanePointerInputDependencies {
60
+ readonly handleProjectPaneActionClick?: typeof handleProjectPaneActionClick;
61
+ readonly handleHomePanePointerClick?: typeof handleHomePanePointerClick;
62
+ }
63
+
64
+ interface PointerEventInput {
65
+ readonly target: PointerTarget;
66
+ readonly code: number;
67
+ readonly final: 'M' | 'm';
68
+ readonly row: number;
69
+ readonly col: number;
70
+ readonly paneRows: number;
71
+ readonly rightCols: number;
72
+ readonly rightStartCol: number;
73
+ }
74
+
75
+ export class MainPanePointerInput<TProjectSnapshot extends { directoryId: string }> {
76
+ private readonly projectPaneActionClick: typeof handleProjectPaneActionClick<TProjectSnapshot>;
77
+ private readonly homePanePointerClick: typeof handleHomePanePointerClick;
78
+
79
+ constructor(
80
+ private readonly options: MainPanePointerInputOptions<TProjectSnapshot>,
81
+ dependencies: MainPanePointerInputDependencies = {},
82
+ ) {
83
+ this.projectPaneActionClick =
84
+ dependencies.handleProjectPaneActionClick ?? handleProjectPaneActionClick;
85
+ this.homePanePointerClick =
86
+ dependencies.handleHomePanePointerClick ?? handleHomePanePointerClick;
87
+ }
88
+
89
+ handleProjectPanePointerClick(input: PointerEventInput): boolean {
90
+ const clickEligible =
91
+ input.target === 'right' &&
92
+ this.options.getMainPaneMode() === 'project' &&
93
+ isLeftButtonPress(input.code, input.final) &&
94
+ !hasAltModifier(input.code) &&
95
+ !isMotionMouseCode(input.code);
96
+ const rowIndex = Math.max(0, Math.min(input.paneRows - 1, input.row - 1));
97
+ return this.projectPaneActionClick({
98
+ clickEligible,
99
+ snapshot: this.options.getProjectPaneSnapshot(),
100
+ rightCols: input.rightCols,
101
+ paneRows: input.paneRows,
102
+ projectPaneScrollTop: this.options.getProjectPaneScrollTop(),
103
+ rowIndex,
104
+ projectPaneActionAtRow: this.options.projectPaneActionAtRow,
105
+ openNewThreadPrompt: this.options.openNewThreadPrompt,
106
+ queueCloseDirectory: this.options.queueCloseDirectory,
107
+ markDirty: this.options.markDirty,
108
+ });
109
+ }
110
+
111
+ handleHomePanePointerClick(input: PointerEventInput): boolean {
112
+ const clickEligible =
113
+ input.target === 'right' &&
114
+ this.options.getMainPaneMode() === 'home' &&
115
+ isLeftButtonPress(input.code, input.final) &&
116
+ !hasAltModifier(input.code) &&
117
+ !isMotionMouseCode(input.code);
118
+ return this.homePanePointerClick({
119
+ clickEligible,
120
+ paneRows: input.paneRows,
121
+ rightCols: input.rightCols,
122
+ rightStartCol: input.rightStartCol,
123
+ pointerRow: input.row,
124
+ pointerCol: input.col,
125
+ actionAtCell: this.options.actionAtCell,
126
+ actionAtRow: this.options.actionAtRow,
127
+ clearTaskEditClickState: this.options.clearTaskEditClickState,
128
+ clearRepositoryEditClickState: this.options.clearRepositoryEditClickState,
129
+ clearHomePaneDragState: this.options.clearHomePaneDragState,
130
+ getTaskRepositoryDropdownOpen: this.options.getTaskRepositoryDropdownOpen,
131
+ setTaskRepositoryDropdownOpen: this.options.setTaskRepositoryDropdownOpen,
132
+ taskIdAtRow: this.options.taskIdAtRow,
133
+ repositoryIdAtRow: this.options.repositoryIdAtRow,
134
+ selectTaskById: this.options.selectTaskById,
135
+ selectRepositoryById: this.options.selectRepositoryById,
136
+ runTaskPaneAction: this.options.runTaskPaneAction,
137
+ nowMs: this.options.nowMs(),
138
+ homePaneEditDoubleClickWindowMs: this.options.homePaneEditDoubleClickWindowMs,
139
+ taskEditClickState: this.options.getTaskEditClickState(),
140
+ repositoryEditClickState: this.options.getRepositoryEditClickState(),
141
+ clearTaskPaneNotice: this.options.clearTaskPaneNotice,
142
+ setTaskEditClickState: this.options.setTaskEditClickState,
143
+ setRepositoryEditClickState: this.options.setRepositoryEditClickState,
144
+ setHomePaneDragState: this.options.setHomePaneDragState,
145
+ openTaskEditPrompt: this.options.openTaskEditPrompt,
146
+ openRepositoryPromptForEdit: this.options.openRepositoryPromptForEdit,
147
+ markDirty: this.options.markDirty,
148
+ });
149
+ }
150
+ }
@@ -0,0 +1,192 @@
1
+ import {
2
+ buildCommandMenuModalOverlay as buildCommandMenuModalOverlayFrame,
3
+ buildAddDirectoryModalOverlay as buildAddDirectoryModalOverlayFrame,
4
+ buildConversationTitleModalOverlay as buildConversationTitleModalOverlayFrame,
5
+ buildNewThreadModalOverlay as buildNewThreadModalOverlayFrame,
6
+ buildRepositoryModalOverlay as buildRepositoryModalOverlayFrame,
7
+ buildTaskEditorModalOverlay as buildTaskEditorModalOverlayFrame,
8
+ } from '../../mux/live-mux/modal-overlays.ts';
9
+ import { dismissModalOnOutsideClick as dismissModalOnOutsideClickFrame } from '../../mux/live-mux/modal-pointer.ts';
10
+ import type {
11
+ CommandMenuActionDescriptor,
12
+ CommandMenuState,
13
+ } from '../../mux/live-mux/command-menu.ts';
14
+ import type { createNewThreadPromptState } from '../../mux/new-thread-prompt.ts';
15
+ import type {
16
+ ConversationTitleEditState,
17
+ RepositoryPromptState,
18
+ TaskEditorPromptState,
19
+ } from '../../domain/workspace.ts';
20
+ import { isUiModalOverlayHit } from '../kit.ts';
21
+
22
+ type NewThreadPromptState = ReturnType<typeof createNewThreadPromptState>;
23
+ type AddDirectoryPromptState = { value: string; error: string | null };
24
+ type ModalOverlay = Exclude<ReturnType<typeof buildNewThreadModalOverlayFrame>, null>;
25
+ type ModalTheme = Parameters<typeof buildNewThreadModalOverlayFrame>[3];
26
+ type DismissModalOnOutsideClickInput = Parameters<typeof dismissModalOnOutsideClickFrame>[0];
27
+
28
+ interface ModalManagerOptions {
29
+ readonly theme: ModalTheme;
30
+ readonly resolveRepositoryName: (repositoryId: string) => string | null;
31
+ readonly getCommandMenu: () => CommandMenuState | null;
32
+ readonly resolveCommandMenuActions: () => readonly CommandMenuActionDescriptor[];
33
+ readonly getNewThreadPrompt: () => NewThreadPromptState | null;
34
+ readonly getAddDirectoryPrompt: () => AddDirectoryPromptState | null;
35
+ readonly getTaskEditorPrompt: () => TaskEditorPromptState | null;
36
+ readonly getRepositoryPrompt: () => RepositoryPromptState | null;
37
+ readonly getConversationTitleEdit: () => ConversationTitleEditState | null;
38
+ }
39
+
40
+ interface ModalManagerDependencies {
41
+ readonly buildCommandMenuModalOverlay?: typeof buildCommandMenuModalOverlayFrame;
42
+ readonly buildNewThreadModalOverlay?: typeof buildNewThreadModalOverlayFrame;
43
+ readonly buildAddDirectoryModalOverlay?: typeof buildAddDirectoryModalOverlayFrame;
44
+ readonly buildTaskEditorModalOverlay?: typeof buildTaskEditorModalOverlayFrame;
45
+ readonly buildRepositoryModalOverlay?: typeof buildRepositoryModalOverlayFrame;
46
+ readonly buildConversationTitleModalOverlay?: typeof buildConversationTitleModalOverlayFrame;
47
+ readonly dismissModalOnOutsideClick?: typeof dismissModalOnOutsideClickFrame;
48
+ readonly isOverlayHit?: typeof isUiModalOverlayHit;
49
+ }
50
+
51
+ interface ModalDismissInput {
52
+ readonly input: Buffer;
53
+ readonly inputRemainder: string;
54
+ readonly layoutCols: number;
55
+ readonly viewportRows: number;
56
+ readonly dismiss: () => void;
57
+ readonly onInsidePointerPress?: (col: number, row: number) => boolean;
58
+ }
59
+
60
+ interface ModalDismissResult {
61
+ readonly handled: boolean;
62
+ readonly inputRemainder: string;
63
+ }
64
+
65
+ export class ModalManager {
66
+ private readonly buildCommandMenuModalOverlay: typeof buildCommandMenuModalOverlayFrame;
67
+ private readonly buildNewThreadModalOverlay: typeof buildNewThreadModalOverlayFrame;
68
+ private readonly buildAddDirectoryModalOverlay: typeof buildAddDirectoryModalOverlayFrame;
69
+ private readonly buildTaskEditorModalOverlay: typeof buildTaskEditorModalOverlayFrame;
70
+ private readonly buildRepositoryModalOverlay: typeof buildRepositoryModalOverlayFrame;
71
+ private readonly buildConversationTitleModalOverlay: typeof buildConversationTitleModalOverlayFrame;
72
+ private readonly dismissModalOnOutsideClick: typeof dismissModalOnOutsideClickFrame;
73
+ private readonly isOverlayHit: typeof isUiModalOverlayHit;
74
+
75
+ constructor(
76
+ private readonly options: ModalManagerOptions,
77
+ dependencies: ModalManagerDependencies = {},
78
+ ) {
79
+ this.buildCommandMenuModalOverlay =
80
+ dependencies.buildCommandMenuModalOverlay ?? buildCommandMenuModalOverlayFrame;
81
+ this.buildNewThreadModalOverlay =
82
+ dependencies.buildNewThreadModalOverlay ?? buildNewThreadModalOverlayFrame;
83
+ this.buildAddDirectoryModalOverlay =
84
+ dependencies.buildAddDirectoryModalOverlay ?? buildAddDirectoryModalOverlayFrame;
85
+ this.buildTaskEditorModalOverlay =
86
+ dependencies.buildTaskEditorModalOverlay ?? buildTaskEditorModalOverlayFrame;
87
+ this.buildRepositoryModalOverlay =
88
+ dependencies.buildRepositoryModalOverlay ?? buildRepositoryModalOverlayFrame;
89
+ this.buildConversationTitleModalOverlay =
90
+ dependencies.buildConversationTitleModalOverlay ?? buildConversationTitleModalOverlayFrame;
91
+ this.dismissModalOnOutsideClick =
92
+ dependencies.dismissModalOnOutsideClick ?? dismissModalOnOutsideClickFrame;
93
+ this.isOverlayHit = dependencies.isOverlayHit ?? isUiModalOverlayHit;
94
+ }
95
+
96
+ buildCommandMenuOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
97
+ return this.buildCommandMenuModalOverlay(
98
+ layoutCols,
99
+ viewportRows,
100
+ this.options.getCommandMenu(),
101
+ this.options.resolveCommandMenuActions(),
102
+ this.options.theme,
103
+ );
104
+ }
105
+
106
+ buildNewThreadOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
107
+ return this.buildNewThreadModalOverlay(
108
+ layoutCols,
109
+ viewportRows,
110
+ this.options.getNewThreadPrompt(),
111
+ this.options.theme,
112
+ );
113
+ }
114
+
115
+ buildAddDirectoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
116
+ return this.buildAddDirectoryModalOverlay(
117
+ layoutCols,
118
+ viewportRows,
119
+ this.options.getAddDirectoryPrompt(),
120
+ this.options.theme,
121
+ );
122
+ }
123
+
124
+ buildTaskEditorOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
125
+ return this.buildTaskEditorModalOverlay(
126
+ layoutCols,
127
+ viewportRows,
128
+ this.options.getTaskEditorPrompt(),
129
+ this.options.resolveRepositoryName,
130
+ this.options.theme,
131
+ );
132
+ }
133
+
134
+ buildRepositoryOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
135
+ return this.buildRepositoryModalOverlay(
136
+ layoutCols,
137
+ viewportRows,
138
+ this.options.getRepositoryPrompt(),
139
+ this.options.theme,
140
+ );
141
+ }
142
+
143
+ buildConversationTitleOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
144
+ return this.buildConversationTitleModalOverlay(
145
+ layoutCols,
146
+ viewportRows,
147
+ this.options.getConversationTitleEdit(),
148
+ this.options.theme,
149
+ );
150
+ }
151
+
152
+ buildCurrentOverlay(layoutCols: number, viewportRows: number): ModalOverlay | null {
153
+ const commandMenuOverlay = this.buildCommandMenuOverlay(layoutCols, viewportRows);
154
+ if (commandMenuOverlay !== null) {
155
+ return commandMenuOverlay;
156
+ }
157
+ const newThreadOverlay = this.buildNewThreadOverlay(layoutCols, viewportRows);
158
+ if (newThreadOverlay !== null) {
159
+ return newThreadOverlay;
160
+ }
161
+ const addDirectoryOverlay = this.buildAddDirectoryOverlay(layoutCols, viewportRows);
162
+ if (addDirectoryOverlay !== null) {
163
+ return addDirectoryOverlay;
164
+ }
165
+ const taskEditorOverlay = this.buildTaskEditorOverlay(layoutCols, viewportRows);
166
+ if (taskEditorOverlay !== null) {
167
+ return taskEditorOverlay;
168
+ }
169
+ const repositoryOverlay = this.buildRepositoryOverlay(layoutCols, viewportRows);
170
+ if (repositoryOverlay !== null) {
171
+ return repositoryOverlay;
172
+ }
173
+ return this.buildConversationTitleOverlay(layoutCols, viewportRows);
174
+ }
175
+
176
+ dismissOnOutsideClick(input: ModalDismissInput): ModalDismissResult {
177
+ const dismissInput: DismissModalOnOutsideClickInput = {
178
+ input: input.input,
179
+ inputRemainder: input.inputRemainder,
180
+ dismiss: input.dismiss,
181
+ buildCurrentModalOverlay: () =>
182
+ this.buildCurrentOverlay(input.layoutCols, input.viewportRows),
183
+ isOverlayHit: this.isOverlayHit,
184
+ ...(input.onInsidePointerPress === undefined
185
+ ? {}
186
+ : {
187
+ onInsidePointerPress: input.onInsidePointerPress,
188
+ }),
189
+ };
190
+ return this.dismissModalOnOutsideClick(dismissInput);
191
+ }
192
+ }