@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
@@ -0,0 +1,166 @@
1
+ export type LeftNavSelection =
2
+ | {
3
+ readonly kind: 'home';
4
+ }
5
+ | {
6
+ readonly kind: 'nim';
7
+ }
8
+ | {
9
+ readonly kind: 'tasks';
10
+ }
11
+ | {
12
+ readonly kind: 'repository';
13
+ readonly repositoryId: string;
14
+ }
15
+ | {
16
+ readonly kind: 'project';
17
+ readonly directoryId: string;
18
+ }
19
+ | {
20
+ readonly kind: 'github';
21
+ readonly directoryId: string;
22
+ }
23
+ | {
24
+ readonly kind: 'conversation';
25
+ readonly sessionId: string;
26
+ };
27
+
28
+ export interface LeftNavState {
29
+ readonly railViewState: {
30
+ readLatestRows(): unknown;
31
+ };
32
+ readonly currentSelection: () => LeftNavSelection;
33
+ }
34
+
35
+ export interface LeftNavActions {
36
+ readonly enterHomePane: () => void;
37
+ readonly enterNimPane?: () => void;
38
+ readonly enterTasksPane?: () => void;
39
+ readonly firstDirectoryForRepositoryGroup: (repositoryGroupId: string) => string | null;
40
+ readonly enterProjectPane: (directoryId: string) => void;
41
+ readonly enterGitHubPane?: (directoryId: string) => void;
42
+ readonly setMainPaneProjectMode: () => void;
43
+ readonly selectLeftNavRepository: (repositoryGroupId: string) => void;
44
+ readonly selectLeftNavConversation?: (sessionId: string) => void;
45
+ readonly markDirty: () => void;
46
+ readonly directoriesHas: (directoryId: string) => boolean;
47
+ readonly conversationDirectoryId: (sessionId: string) => string | null;
48
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
49
+ readonly queueLatestControlPlaneOp?: (
50
+ key: string,
51
+ task: (options: { readonly signal: AbortSignal }) => Promise<void>,
52
+ label: string,
53
+ ) => void;
54
+ readonly activateConversation: (
55
+ sessionId: string,
56
+ options?: { readonly signal?: AbortSignal },
57
+ ) => Promise<void>;
58
+ readonly conversationsHas: (sessionId: string) => boolean;
59
+ }
60
+
61
+ export interface ActivateLeftNavTargetInput {
62
+ readonly target: LeftNavSelection;
63
+ readonly direction: 'next' | 'previous';
64
+ readonly enterHomePane: () => void;
65
+ readonly enterNimPane?: () => void;
66
+ readonly enterTasksPane?: () => void;
67
+ readonly firstDirectoryForRepositoryGroup: (repositoryGroupId: string) => string | null;
68
+ readonly enterProjectPane: (directoryId: string) => void;
69
+ readonly enterGitHubPane?: (directoryId: string) => void;
70
+ readonly setMainPaneProjectMode: () => void;
71
+ readonly selectLeftNavRepository: (repositoryGroupId: string) => void;
72
+ readonly selectLeftNavConversation?: (sessionId: string) => void;
73
+ readonly markDirty: () => void;
74
+ readonly directoriesHas: (directoryId: string) => boolean;
75
+ readonly visibleTargetsForState: () => readonly LeftNavSelection[];
76
+ readonly conversationDirectoryId: (sessionId: string) => string | null;
77
+ readonly queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
78
+ readonly queueLatestControlPlaneOp?: (
79
+ key: string,
80
+ task: (options: { readonly signal: AbortSignal }) => Promise<void>,
81
+ label: string,
82
+ ) => void;
83
+ readonly activateConversation: (
84
+ sessionId: string,
85
+ options?: { readonly signal?: AbortSignal },
86
+ ) => Promise<void>;
87
+ readonly conversationsHas: (sessionId: string) => boolean;
88
+ }
89
+
90
+ export interface CycleLeftNavSelectionInput {
91
+ readonly visibleTargets: readonly LeftNavSelection[];
92
+ readonly currentSelection: LeftNavSelection;
93
+ readonly direction: 'next' | 'previous';
94
+ readonly activateTarget: (target: LeftNavSelection, direction: 'next' | 'previous') => void;
95
+ }
96
+
97
+ export interface LeftNavStrategies {
98
+ visibleTargets(rows: unknown): readonly LeftNavSelection[];
99
+ activateTarget(input: ActivateLeftNavTargetInput): void;
100
+ cycleSelection(input: CycleLeftNavSelectionInput): boolean;
101
+ }
102
+
103
+ export class LeftNavInput {
104
+ constructor(
105
+ private readonly state: LeftNavState,
106
+ private readonly actions: LeftNavActions,
107
+ private readonly strategies: LeftNavStrategies,
108
+ ) {}
109
+
110
+ visibleTargets(): readonly LeftNavSelection[] {
111
+ return this.strategies.visibleTargets(this.state.railViewState.readLatestRows());
112
+ }
113
+
114
+ activateTarget(target: LeftNavSelection, direction: 'next' | 'previous'): void {
115
+ this.strategies.activateTarget({
116
+ target,
117
+ direction,
118
+ enterHomePane: this.actions.enterHomePane,
119
+ ...(this.actions.enterNimPane === undefined
120
+ ? {}
121
+ : {
122
+ enterNimPane: this.actions.enterNimPane,
123
+ }),
124
+ firstDirectoryForRepositoryGroup: this.actions.firstDirectoryForRepositoryGroup,
125
+ enterProjectPane: this.actions.enterProjectPane,
126
+ ...(this.actions.enterGitHubPane === undefined
127
+ ? {}
128
+ : {
129
+ enterGitHubPane: this.actions.enterGitHubPane,
130
+ }),
131
+ setMainPaneProjectMode: this.actions.setMainPaneProjectMode,
132
+ selectLeftNavRepository: this.actions.selectLeftNavRepository,
133
+ ...(this.actions.selectLeftNavConversation === undefined
134
+ ? {}
135
+ : {
136
+ selectLeftNavConversation: this.actions.selectLeftNavConversation,
137
+ }),
138
+ markDirty: this.actions.markDirty,
139
+ directoriesHas: this.actions.directoriesHas,
140
+ visibleTargetsForState: () => this.visibleTargets(),
141
+ conversationDirectoryId: this.actions.conversationDirectoryId,
142
+ queueControlPlaneOp: this.actions.queueControlPlaneOp,
143
+ ...(this.actions.queueLatestControlPlaneOp === undefined
144
+ ? {}
145
+ : {
146
+ queueLatestControlPlaneOp: this.actions.queueLatestControlPlaneOp,
147
+ }),
148
+ activateConversation: this.actions.activateConversation,
149
+ conversationsHas: this.actions.conversationsHas,
150
+ ...(this.actions.enterTasksPane === undefined
151
+ ? {}
152
+ : {
153
+ enterTasksPane: this.actions.enterTasksPane,
154
+ }),
155
+ });
156
+ }
157
+
158
+ cycleSelection(direction: 'next' | 'previous'): boolean {
159
+ return this.strategies.cycleSelection({
160
+ visibleTargets: this.visibleTargets(),
161
+ currentSelection: this.state.currentSelection(),
162
+ direction,
163
+ activateTarget: (target, nextDirection) => this.activateTarget(target, nextDirection),
164
+ });
165
+ }
166
+ }
@@ -1,7 +1,3 @@
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
1
  interface EntityDoubleClickState {
6
2
  readonly entityId: string;
7
3
  readonly atMs: number;
@@ -15,10 +11,32 @@ interface HomePaneDragState {
15
11
  readonly hasDragged: boolean;
16
12
  }
17
13
 
18
- type MainPaneMode = 'conversation' | 'project' | 'home';
14
+ type MainPaneMode = 'conversation' | 'project' | 'home' | 'nim';
19
15
  type PointerTarget = 'left' | 'right' | 'separator' | 'status' | 'outside';
20
16
 
21
- interface MainPanePointerInputOptions<TProjectSnapshot extends { directoryId: string }> {
17
+ function isWheelMouseCode(code: number): boolean {
18
+ return (code & 0b0100_0000) !== 0;
19
+ }
20
+
21
+ function hasAltModifier(code: number): boolean {
22
+ return (code & 0b0000_1000) !== 0;
23
+ }
24
+
25
+ function isMotionMouseCode(code: number): boolean {
26
+ return (code & 0b0010_0000) !== 0;
27
+ }
28
+
29
+ function isLeftButtonPress(code: number, final: 'M' | 'm'): boolean {
30
+ if (final !== 'M') {
31
+ return false;
32
+ }
33
+ if (isWheelMouseCode(code) || isMotionMouseCode(code)) {
34
+ return false;
35
+ }
36
+ return (code & 0b0000_0011) === 0;
37
+ }
38
+
39
+ export interface MainPanePointerInputOptions<TProjectSnapshot extends { directoryId: string }> {
22
40
  readonly getMainPaneMode: () => MainPaneMode;
23
41
  readonly getProjectPaneSnapshot: () => TProjectSnapshot | null;
24
42
  readonly getProjectPaneScrollTop: () => number;
@@ -40,6 +58,7 @@ interface MainPanePointerInputOptions<TProjectSnapshot extends { directoryId: st
40
58
  readonly setTaskRepositoryDropdownOpen: (open: boolean) => void;
41
59
  readonly taskIdAtRow: (rowIndex: number) => string | null;
42
60
  readonly repositoryIdAtRow: (rowIndex: number) => string | null;
61
+ readonly rowTextAtRow?: (rowIndex: number) => string | null;
43
62
  readonly selectTaskById: (taskId: string) => void;
44
63
  readonly selectRepositoryById: (repositoryId: string) => void;
45
64
  readonly runTaskPaneAction: (action: 'task.ready' | 'task.draft' | 'task.complete') => void;
@@ -56,11 +75,6 @@ interface MainPanePointerInputOptions<TProjectSnapshot extends { directoryId: st
56
75
  readonly markDirty: () => void;
57
76
  }
58
77
 
59
- interface MainPanePointerInputDependencies {
60
- readonly handleProjectPaneActionClick?: typeof handleProjectPaneActionClick;
61
- readonly handleHomePanePointerClick?: typeof handleHomePanePointerClick;
62
- }
63
-
64
78
  interface PointerEventInput {
65
79
  readonly target: PointerTarget;
66
80
  readonly code: number;
@@ -72,19 +86,68 @@ interface PointerEventInput {
72
86
  readonly rightStartCol: number;
73
87
  }
74
88
 
75
- export class MainPanePointerInput<TProjectSnapshot extends { directoryId: string }> {
76
- private readonly projectPaneActionClick: typeof handleProjectPaneActionClick<TProjectSnapshot>;
77
- private readonly homePanePointerClick: typeof handleHomePanePointerClick;
89
+ export interface HandleProjectPaneActionClickInput<TSnapshot extends { directoryId: string }> {
90
+ readonly clickEligible: boolean;
91
+ readonly snapshot: TSnapshot | null;
92
+ readonly rightCols: number;
93
+ readonly paneRows: number;
94
+ readonly projectPaneScrollTop: number;
95
+ readonly rowIndex: number;
96
+ readonly projectPaneActionAtRow: (
97
+ snapshot: TSnapshot,
98
+ rightCols: number,
99
+ paneRows: number,
100
+ projectPaneScrollTop: number,
101
+ rowIndex: number,
102
+ ) => string | null;
103
+ readonly openNewThreadPrompt: (directoryId: string) => void;
104
+ readonly queueCloseDirectory: (directoryId: string) => void;
105
+ readonly markDirty: () => void;
106
+ }
78
107
 
108
+ export interface HandleHomePanePointerClickInput {
109
+ readonly clickEligible: boolean;
110
+ readonly paneRows: number;
111
+ readonly rightCols: number;
112
+ readonly rightStartCol: number;
113
+ readonly pointerRow: number;
114
+ readonly pointerCol: number;
115
+ readonly actionAtCell: (rowIndex: number, colIndex: number) => string | null;
116
+ readonly actionAtRow: (rowIndex: number) => string | null;
117
+ readonly clearTaskEditClickState: () => void;
118
+ readonly clearRepositoryEditClickState: () => void;
119
+ readonly clearHomePaneDragState: () => void;
120
+ readonly getTaskRepositoryDropdownOpen: () => boolean;
121
+ readonly setTaskRepositoryDropdownOpen: (open: boolean) => void;
122
+ readonly taskIdAtRow: (rowIndex: number) => string | null;
123
+ readonly repositoryIdAtRow: (rowIndex: number) => string | null;
124
+ readonly rowTextAtRow?: (rowIndex: number) => string | null;
125
+ readonly selectTaskById: (taskId: string) => void;
126
+ readonly selectRepositoryById: (repositoryId: string) => void;
127
+ readonly runTaskPaneAction: (action: 'task.ready' | 'task.draft' | 'task.complete') => void;
128
+ readonly nowMs: number;
129
+ readonly homePaneEditDoubleClickWindowMs: number;
130
+ readonly taskEditClickState: EntityDoubleClickState | null;
131
+ readonly repositoryEditClickState: EntityDoubleClickState | null;
132
+ readonly clearTaskPaneNotice: () => void;
133
+ readonly setTaskEditClickState: (next: EntityDoubleClickState | null) => void;
134
+ readonly setRepositoryEditClickState: (next: EntityDoubleClickState | null) => void;
135
+ readonly setHomePaneDragState: (next: HomePaneDragState | null) => void;
136
+ readonly openTaskEditPrompt: (taskId: string) => void;
137
+ readonly openRepositoryPromptForEdit: (repositoryId: string) => void;
138
+ readonly markDirty: () => void;
139
+ }
140
+
141
+ export interface MainPanePointerStrategies<TProjectSnapshot extends { directoryId: string }> {
142
+ handleProjectPaneActionClick(input: HandleProjectPaneActionClickInput<TProjectSnapshot>): boolean;
143
+ handleHomePanePointerClick(input: HandleHomePanePointerClickInput): boolean;
144
+ }
145
+
146
+ export class MainPanePointerInput<TProjectSnapshot extends { directoryId: string }> {
79
147
  constructor(
80
148
  private readonly options: MainPanePointerInputOptions<TProjectSnapshot>,
81
- dependencies: MainPanePointerInputDependencies = {},
82
- ) {
83
- this.projectPaneActionClick =
84
- dependencies.handleProjectPaneActionClick ?? handleProjectPaneActionClick;
85
- this.homePanePointerClick =
86
- dependencies.handleHomePanePointerClick ?? handleHomePanePointerClick;
87
- }
149
+ private readonly strategies: MainPanePointerStrategies<TProjectSnapshot>,
150
+ ) {}
88
151
 
89
152
  handleProjectPanePointerClick(input: PointerEventInput): boolean {
90
153
  const clickEligible =
@@ -94,7 +157,7 @@ export class MainPanePointerInput<TProjectSnapshot extends { directoryId: string
94
157
  !hasAltModifier(input.code) &&
95
158
  !isMotionMouseCode(input.code);
96
159
  const rowIndex = Math.max(0, Math.min(input.paneRows - 1, input.row - 1));
97
- return this.projectPaneActionClick({
160
+ return this.strategies.handleProjectPaneActionClick({
98
161
  clickEligible,
99
162
  snapshot: this.options.getProjectPaneSnapshot(),
100
163
  rightCols: input.rightCols,
@@ -115,7 +178,7 @@ export class MainPanePointerInput<TProjectSnapshot extends { directoryId: string
115
178
  isLeftButtonPress(input.code, input.final) &&
116
179
  !hasAltModifier(input.code) &&
117
180
  !isMotionMouseCode(input.code);
118
- return this.homePanePointerClick({
181
+ return this.strategies.handleHomePanePointerClick({
119
182
  clickEligible,
120
183
  paneRows: input.paneRows,
121
184
  rightCols: input.rightCols,
@@ -145,6 +208,11 @@ export class MainPanePointerInput<TProjectSnapshot extends { directoryId: string
145
208
  openTaskEditPrompt: this.options.openTaskEditPrompt,
146
209
  openRepositoryPromptForEdit: this.options.openRepositoryPromptForEdit,
147
210
  markDirty: this.options.markDirty,
211
+ ...(this.options.rowTextAtRow === undefined
212
+ ? {}
213
+ : {
214
+ rowTextAtRow: this.options.rowTextAtRow,
215
+ }),
148
216
  });
149
217
  }
150
218
  }
@@ -1,20 +1,4 @@
1
- import { wheelDeltaRowsFromCode } from '../mux/dual-pane-core.ts';
2
- import { handleHomePaneDragRelease as handleHomePaneDragReleaseFrame } from '../mux/live-mux/home-pane-drop.ts';
3
- import {
4
- handleHomePaneDragMove as handleHomePaneDragMoveFrame,
5
- handleMainPaneWheelInput as handleMainPaneWheelInputFrame,
6
- handlePaneDividerDragInput as handlePaneDividerDragInputFrame,
7
- handleSeparatorPointerPress as handleSeparatorPointerPressFrame,
8
- } from '../mux/live-mux/pointer-routing.ts';
9
- import {
10
- hasAltModifier,
11
- isLeftButtonPress,
12
- isMouseRelease,
13
- isSelectionDrag,
14
- isWheelMouseCode,
15
- } from '../mux/live-mux/selection.ts';
16
-
17
- type MainPaneMode = 'conversation' | 'project' | 'home';
1
+ type MainPaneMode = 'conversation' | 'project' | 'home' | 'nim';
18
2
  type PointerTarget = 'left' | 'right' | 'separator' | 'status' | 'outside';
19
3
 
20
4
  interface HomePaneDragState {
@@ -25,7 +9,44 @@ interface HomePaneDragState {
25
9
  readonly hasDragged: boolean;
26
10
  }
27
11
 
28
- interface PointerRoutingInputOptions {
12
+ function isWheelMouseCode(code: number): boolean {
13
+ return (code & 0b0100_0000) !== 0;
14
+ }
15
+
16
+ function hasAltModifier(code: number): boolean {
17
+ return (code & 0b0000_1000) !== 0;
18
+ }
19
+
20
+ function isMotionMouseCode(code: number): boolean {
21
+ return (code & 0b0010_0000) !== 0;
22
+ }
23
+
24
+ function isLeftButtonPress(code: number, final: 'M' | 'm'): boolean {
25
+ if (final !== 'M') {
26
+ return false;
27
+ }
28
+ if (isWheelMouseCode(code) || isMotionMouseCode(code)) {
29
+ return false;
30
+ }
31
+ return (code & 0b0000_0011) === 0;
32
+ }
33
+
34
+ function isMouseRelease(final: 'M' | 'm'): boolean {
35
+ return final === 'm';
36
+ }
37
+
38
+ function isSelectionDrag(code: number, final: 'M' | 'm'): boolean {
39
+ return final === 'M' && isMotionMouseCode(code);
40
+ }
41
+
42
+ function wheelDeltaRowsFromCode(code: number): number | null {
43
+ if (!isWheelMouseCode(code)) {
44
+ return null;
45
+ }
46
+ return (code & 0b0000_0001) === 0 ? -1 : 1;
47
+ }
48
+
49
+ export interface PointerRoutingInputOptions {
29
50
  readonly getPaneDividerDragActive: () => boolean;
30
51
  readonly setPaneDividerDragActive: (active: boolean) => void;
31
52
  readonly applyPaneDividerAtCol: (col: number) => void;
@@ -41,15 +62,74 @@ interface PointerRoutingInputOptions {
41
62
  ) => void;
42
63
  readonly onProjectWheel: (delta: number) => void;
43
64
  readonly onHomeWheel: (delta: number) => void;
65
+ readonly onNimWheel: (delta: number) => void;
44
66
  readonly markDirty: () => void;
45
67
  }
46
68
 
47
- interface PointerRoutingInputDependencies {
48
- readonly handlePaneDividerDragInput?: typeof handlePaneDividerDragInputFrame;
49
- readonly handleHomePaneDragRelease?: typeof handleHomePaneDragReleaseFrame;
50
- readonly handleSeparatorPointerPress?: typeof handleSeparatorPointerPressFrame;
51
- readonly handleMainPaneWheelInput?: typeof handleMainPaneWheelInputFrame;
52
- readonly handleHomePaneDragMove?: typeof handleHomePaneDragMoveFrame;
69
+ export interface HandlePaneDividerDragInput {
70
+ readonly paneDividerDragActive: boolean;
71
+ readonly isMouseRelease: boolean;
72
+ readonly isWheelMouseCode: boolean;
73
+ readonly mouseCol: number;
74
+ readonly setPaneDividerDragActive: (active: boolean) => void;
75
+ readonly applyPaneDividerAtCol: (col: number) => void;
76
+ readonly markDirty: () => void;
77
+ }
78
+
79
+ export interface HandleHomePaneDragReleaseInput {
80
+ readonly homePaneDragState: HomePaneDragState | null;
81
+ readonly isMouseRelease: boolean;
82
+ readonly mainPaneMode: MainPaneMode;
83
+ readonly target: PointerTarget;
84
+ readonly rowIndex: number;
85
+ readonly taskIdAtRow: (rowIndex: number) => string | null;
86
+ readonly repositoryIdAtRow: (rowIndex: number) => string | null;
87
+ readonly reorderTaskByDrop: (draggedTaskId: string, targetTaskId: string) => void;
88
+ readonly reorderRepositoryByDrop: (
89
+ draggedRepositoryId: string,
90
+ targetRepositoryId: string,
91
+ ) => void;
92
+ readonly setHomePaneDragState: (next: HomePaneDragState | null) => void;
93
+ readonly markDirty: () => void;
94
+ }
95
+
96
+ export interface HandleSeparatorPointerPressInput {
97
+ readonly target: PointerTarget;
98
+ readonly isLeftButtonPress: boolean;
99
+ readonly hasAltModifier: boolean;
100
+ readonly mouseCol: number;
101
+ readonly setPaneDividerDragActive: (active: boolean) => void;
102
+ readonly applyPaneDividerAtCol: (col: number) => void;
103
+ }
104
+
105
+ export interface HandleMainPaneWheelInput {
106
+ readonly target: PointerTarget;
107
+ readonly wheelDelta: number | null;
108
+ readonly mainPaneMode: MainPaneMode;
109
+ readonly onProjectWheel: (delta: number) => void;
110
+ readonly onHomeWheel: (delta: number) => void;
111
+ readonly onNimWheel: (delta: number) => void;
112
+ readonly onConversationWheel: (delta: number) => void;
113
+ readonly markDirty: () => void;
114
+ }
115
+
116
+ export interface HandleHomePaneDragMoveInput {
117
+ readonly homePaneDragState: HomePaneDragState | null;
118
+ readonly mainPaneMode: MainPaneMode;
119
+ readonly target: PointerTarget;
120
+ readonly isSelectionDrag: boolean;
121
+ readonly hasAltModifier: boolean;
122
+ readonly rowIndex: number;
123
+ readonly setHomePaneDragState: (next: HomePaneDragState) => void;
124
+ readonly markDirty: () => void;
125
+ }
126
+
127
+ export interface PointerRoutingStrategies {
128
+ handlePaneDividerDragInput(options: HandlePaneDividerDragInput): boolean;
129
+ handleHomePaneDragRelease(options: HandleHomePaneDragReleaseInput): boolean;
130
+ handleSeparatorPointerPress(options: HandleSeparatorPointerPressInput): boolean;
131
+ handleMainPaneWheelInput(options: HandleMainPaneWheelInput): boolean;
132
+ handleHomePaneDragMove(options: HandleHomePaneDragMoveInput): boolean;
53
133
  }
54
134
 
55
135
  interface PointerEventInput {
@@ -61,30 +141,13 @@ interface PointerEventInput {
61
141
  }
62
142
 
63
143
  export class PointerRoutingInput {
64
- private readonly handlePaneDividerDragInput: typeof handlePaneDividerDragInputFrame;
65
- private readonly handleHomePaneDragReleaseInput: typeof handleHomePaneDragReleaseFrame;
66
- private readonly handleSeparatorPointerPressInput: typeof handleSeparatorPointerPressFrame;
67
- private readonly handleMainPaneWheelInput: typeof handleMainPaneWheelInputFrame;
68
- private readonly handleHomePaneDragMoveInput: typeof handleHomePaneDragMoveFrame;
69
-
70
144
  constructor(
71
145
  private readonly options: PointerRoutingInputOptions,
72
- dependencies: PointerRoutingInputDependencies = {},
73
- ) {
74
- this.handlePaneDividerDragInput =
75
- dependencies.handlePaneDividerDragInput ?? handlePaneDividerDragInputFrame;
76
- this.handleHomePaneDragReleaseInput =
77
- dependencies.handleHomePaneDragRelease ?? handleHomePaneDragReleaseFrame;
78
- this.handleSeparatorPointerPressInput =
79
- dependencies.handleSeparatorPointerPress ?? handleSeparatorPointerPressFrame;
80
- this.handleMainPaneWheelInput =
81
- dependencies.handleMainPaneWheelInput ?? handleMainPaneWheelInputFrame;
82
- this.handleHomePaneDragMoveInput =
83
- dependencies.handleHomePaneDragMove ?? handleHomePaneDragMoveFrame;
84
- }
146
+ private readonly strategies: PointerRoutingStrategies,
147
+ ) {}
85
148
 
86
149
  handlePaneDividerDrag(event: Pick<PointerEventInput, 'code' | 'final' | 'col'>): boolean {
87
- return this.handlePaneDividerDragInput({
150
+ return this.strategies.handlePaneDividerDragInput({
88
151
  paneDividerDragActive: this.options.getPaneDividerDragActive(),
89
152
  isMouseRelease: isMouseRelease(event.final),
90
153
  isWheelMouseCode: isWheelMouseCode(event.code),
@@ -98,7 +161,7 @@ export class PointerRoutingInput {
98
161
  handleHomePaneDragRelease(
99
162
  event: Pick<PointerEventInput, 'final' | 'target' | 'rowIndex'>,
100
163
  ): boolean {
101
- return this.handleHomePaneDragReleaseInput({
164
+ return this.strategies.handleHomePaneDragRelease({
102
165
  homePaneDragState: this.options.getHomePaneDragState(),
103
166
  isMouseRelease: isMouseRelease(event.final),
104
167
  mainPaneMode: this.options.getMainPaneMode(),
@@ -116,7 +179,7 @@ export class PointerRoutingInput {
116
179
  handleSeparatorPointerPress(
117
180
  event: Pick<PointerEventInput, 'target' | 'code' | 'final' | 'col'>,
118
181
  ): boolean {
119
- return this.handleSeparatorPointerPressInput({
182
+ return this.strategies.handleSeparatorPointerPress({
120
183
  target: event.target,
121
184
  isLeftButtonPress: isLeftButtonPress(event.code, event.final),
122
185
  hasAltModifier: hasAltModifier(event.code),
@@ -130,12 +193,13 @@ export class PointerRoutingInput {
130
193
  event: Pick<PointerEventInput, 'target' | 'code'>,
131
194
  onConversationWheel: (delta: number) => void,
132
195
  ): boolean {
133
- return this.handleMainPaneWheelInput({
196
+ return this.strategies.handleMainPaneWheelInput({
134
197
  target: event.target,
135
198
  wheelDelta: wheelDeltaRowsFromCode(event.code),
136
199
  mainPaneMode: this.options.getMainPaneMode(),
137
200
  onProjectWheel: this.options.onProjectWheel,
138
201
  onHomeWheel: this.options.onHomeWheel,
202
+ onNimWheel: this.options.onNimWheel,
139
203
  onConversationWheel,
140
204
  markDirty: this.options.markDirty,
141
205
  });
@@ -144,7 +208,7 @@ export class PointerRoutingInput {
144
208
  handleHomePaneDragMove(
145
209
  event: Pick<PointerEventInput, 'target' | 'code' | 'final' | 'rowIndex'>,
146
210
  ): boolean {
147
- return this.handleHomePaneDragMoveInput({
211
+ return this.strategies.handleHomePaneDragMove({
148
212
  homePaneDragState: this.options.getHomePaneDragState(),
149
213
  mainPaneMode: this.options.getMainPaneMode(),
150
214
  target: event.target,
@@ -0,0 +1,62 @@
1
+ export interface HandlePointerClickInput {
2
+ readonly clickEligible: boolean;
3
+ readonly paneRows: number;
4
+ readonly leftCols: number;
5
+ readonly pointerRow: number;
6
+ readonly pointerCol: number;
7
+ }
8
+
9
+ export interface RailPointerHitResolver<THit> {
10
+ resolveHit(rowIndex: number, colIndex: number, railCols: number): THit | null;
11
+ }
12
+
13
+ export interface RailPointerHitDispatcher<THit> {
14
+ dispatchHit(hit: THit): boolean;
15
+ }
16
+
17
+ export interface RailPointerEditController<THit> {
18
+ hasActiveEdit(): boolean;
19
+ shouldKeepActiveEdit(hit: THit): boolean;
20
+ stopActiveEdit(): void;
21
+ }
22
+
23
+ export interface RailPointerSelectionController {
24
+ hasSelection(): boolean;
25
+ clearSelection(): void;
26
+ }
27
+
28
+ export class RailPointerInput<THit> {
29
+ constructor(
30
+ private readonly hitResolver: RailPointerHitResolver<THit>,
31
+ private readonly hitDispatcher: RailPointerHitDispatcher<THit>,
32
+ private readonly editController: RailPointerEditController<THit> | null = null,
33
+ private readonly selectionController: RailPointerSelectionController | null = null,
34
+ ) {}
35
+
36
+ handlePointerClick(input: HandlePointerClickInput): boolean {
37
+ if (!input.clickEligible) {
38
+ return false;
39
+ }
40
+
41
+ const rowIndex = Math.max(0, Math.min(input.paneRows - 1, input.pointerRow - 1));
42
+ const colIndex = Math.max(0, Math.min(input.leftCols - 1, input.pointerCol - 1));
43
+ const hit = this.hitResolver.resolveHit(rowIndex, colIndex, input.leftCols);
44
+ if (hit === null) {
45
+ return false;
46
+ }
47
+
48
+ if (
49
+ this.editController !== null &&
50
+ this.editController.hasActiveEdit() &&
51
+ !this.editController.shouldKeepActiveEdit(hit)
52
+ ) {
53
+ this.editController.stopActiveEdit();
54
+ }
55
+
56
+ if (this.selectionController !== null && this.selectionController.hasSelection()) {
57
+ this.selectionController.clearSelection();
58
+ }
59
+
60
+ return this.hitDispatcher.dispatchHit(hit);
61
+ }
62
+ }