@jmoyers/harness 0.1.10 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +31 -35
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. package/src/ui/surface.ts +0 -224
@@ -0,0 +1,509 @@
1
+ import {
2
+ classifyPaneAt,
3
+ parseMuxInputChunk,
4
+ type computeDualPaneLayout,
5
+ } from '../../mux/dual-pane-core.ts';
6
+ import { routeInputTokensForConversation } from '../../mux/live-mux/input-forwarding.ts';
7
+ import { normalizeMuxKeyboardInputForPty } from '../../mux/input-shortcuts.ts';
8
+ import { handleHomePaneDragRelease } from '../../mux/live-mux/home-pane-drop.ts';
9
+ import { handleHomePanePointerClick } from '../../mux/live-mux/home-pane-pointer.ts';
10
+ import {
11
+ handleHomePaneDragMove,
12
+ handleMainPaneWheelInput,
13
+ handlePaneDividerDragInput,
14
+ handleSeparatorPointerPress,
15
+ } from '../../mux/live-mux/pointer-routing.ts';
16
+ import { handleProjectPaneActionClick } from '../../mux/live-mux/project-pane-pointer.ts';
17
+ import { extractFocusEvents } from '../../mux/live-mux/startup-utils.ts';
18
+ import {
19
+ compareSelectionPoints,
20
+ hasAltModifier,
21
+ isCopyShortcutInput,
22
+ isLeftButtonPress,
23
+ isMotionMouseCode,
24
+ pointFromMouseEvent,
25
+ reduceConversationMouseSelection,
26
+ selectionText,
27
+ writeTextToClipboard,
28
+ type PaneSelection,
29
+ } from '../../mux/live-mux/selection.ts';
30
+ import {
31
+ taskFocusedPaneActionAtCell,
32
+ taskFocusedPaneActionAtRow,
33
+ taskFocusedPaneRepositoryIdAtRow,
34
+ taskFocusedPaneTaskIdAtRow,
35
+ } from '../../mux/task-focused-pane.ts';
36
+ import type { WorkspaceModel } from '../../domain/workspace.ts';
37
+ import type { TerminalSnapshotFrameCore } from '../../terminal/snapshot-oracle.ts';
38
+ import { ConversationInputForwarder } from '../../../packages/harness-ui/src/interaction/conversation-input-forwarder.ts';
39
+ import { ConversationSelectionInput } from '../../../packages/harness-ui/src/interaction/conversation-selection-input.ts';
40
+ import { InputPreflight } from '../../../packages/harness-ui/src/interaction/input-preflight.ts';
41
+ import { InputTokenRouter } from '../../../packages/harness-ui/src/interaction/input-token-router.ts';
42
+ import { MainPanePointerInput } from '../../../packages/harness-ui/src/interaction/main-pane-pointer-input.ts';
43
+ import { PointerRoutingInput } from '../../../packages/harness-ui/src/interaction/pointer-routing-input.ts';
44
+ import type { HandlePointerClickInput } from '../../../packages/harness-ui/src/interaction/rail-pointer-input.ts';
45
+
46
+ interface ActiveConversationLike {
47
+ readonly sessionId: string;
48
+ readonly directoryId: string | null;
49
+ readonly controller: unknown | null;
50
+ readonly oracle: {
51
+ snapshotWithoutHash(): TerminalSnapshotFrameCore;
52
+ isMouseTrackingEnabled(): boolean;
53
+ scrollViewport(deltaRows: number): void;
54
+ selectionText(anchor: PaneSelection['anchor'], focus: PaneSelection['focus']): string;
55
+ };
56
+ }
57
+
58
+ interface TuiMainPaneTaskActions {
59
+ selectTaskById(taskId: string): void;
60
+ selectRepositoryById(repositoryId: string): void;
61
+ runTaskPaneAction(action: 'task.ready' | 'task.draft' | 'task.complete'): void;
62
+ openTaskEditPrompt(taskId: string): void;
63
+ reorderTaskByDrop(draggedTaskId: string, targetTaskId: string): void;
64
+ reorderRepositoryByDrop(draggedRepositoryId: string, targetRepositoryId: string): void;
65
+ handleShortcutInput(input: Buffer): boolean;
66
+ }
67
+
68
+ type WorkspaceProjectPaneSnapshot = NonNullable<WorkspaceModel['projectPaneSnapshot']>;
69
+
70
+ interface TuiMainPaneProjectActions {
71
+ projectPaneActionAtRow: (
72
+ snapshot: WorkspaceProjectPaneSnapshot,
73
+ rightCols: number,
74
+ paneRows: number,
75
+ projectPaneScrollTop: number,
76
+ rowIndex: number,
77
+ ) => string | null;
78
+ refreshGitHubReview(directoryId: string): void;
79
+ toggleGitHubNode(directoryId: string, nodeId: string): boolean;
80
+ openNewThreadPrompt(directoryId: string): void;
81
+ queueCloseDirectory(directoryId: string): void;
82
+ }
83
+
84
+ interface TuiMainPaneRepositoryActions {
85
+ openRepositoryPromptForEdit(repositoryId: string): void;
86
+ }
87
+
88
+ interface TuiMainPaneSelectionControls {
89
+ pinViewportForSelection(): void;
90
+ releaseViewportPinForSelection(): void;
91
+ }
92
+
93
+ interface TuiMainPaneRuntimeControls<TConversation extends ActiveConversationLike> {
94
+ isShuttingDown(): boolean;
95
+ getActiveConversation(): TConversation | null;
96
+ sendInputToSession(sessionId: string, input: Buffer): void;
97
+ isControlledByLocalHuman(input: {
98
+ readonly conversation: TConversation;
99
+ readonly controllerId: string;
100
+ }): boolean;
101
+ enableInputMode(): void;
102
+ }
103
+
104
+ interface TuiMainPaneModalRouting {
105
+ routeModalInput(input: Buffer): boolean;
106
+ }
107
+
108
+ interface TuiMainPaneShortcutHandlers {
109
+ handleRepositoryFoldInput(input: Buffer): boolean;
110
+ handleGlobalShortcutInput(input: Buffer): boolean;
111
+ }
112
+
113
+ interface TuiMainPaneLayoutActions {
114
+ applyPaneDividerAtCol(col: number): void;
115
+ }
116
+
117
+ interface TuiLeftRailPointerInput {
118
+ handlePointerClick(input: HandlePointerClickInput): boolean;
119
+ }
120
+
121
+ type RuntimeLayout = ReturnType<typeof computeDualPaneLayout>;
122
+ type NonConversationMainPaneMode = Exclude<WorkspaceModel['mainPaneMode'], 'conversation'>;
123
+
124
+ export interface TuiMainPaneInteractionsOptions<TConversation extends ActiveConversationLike> {
125
+ readonly workspace: WorkspaceModel;
126
+ readonly controllerId: string;
127
+ readonly getLayout: () => RuntimeLayout;
128
+ readonly noteGitActivity: (directoryId: string | null) => void;
129
+ readonly getInputRemainder: () => string;
130
+ readonly setInputRemainder: (next: string) => void;
131
+ readonly leftRailPointerInput: TuiLeftRailPointerInput;
132
+ readonly project: TuiMainPaneProjectActions;
133
+ readonly task: TuiMainPaneTaskActions;
134
+ readonly repository: TuiMainPaneRepositoryActions;
135
+ readonly selection: TuiMainPaneSelectionControls;
136
+ readonly runtime: TuiMainPaneRuntimeControls<TConversation>;
137
+ readonly modal: TuiMainPaneModalRouting;
138
+ readonly shortcuts: TuiMainPaneShortcutHandlers;
139
+ readonly layout: TuiMainPaneLayoutActions;
140
+ readonly markDirty: () => void;
141
+ readonly handlePassthroughTextInMainPaneMode?: (input: {
142
+ readonly mainPaneMode: NonConversationMainPaneMode;
143
+ readonly text: string;
144
+ }) => void;
145
+ readonly handleEscapeInMainPaneMode?: (mainPaneMode: NonConversationMainPaneMode) => void;
146
+ readonly nowMs?: () => number;
147
+ readonly homePaneEditDoubleClickWindowMs?: number;
148
+ readonly writeTextToClipboard?: (text: string) => boolean;
149
+ }
150
+
151
+ export interface TuiMainPaneInteractions {
152
+ readonly handleInput: (input: Buffer) => void;
153
+ readonly mainPaneInputTokenRouter: InputTokenRouter;
154
+ readonly inputPreflight: InputPreflight;
155
+ }
156
+
157
+ const DEFAULT_HOME_PANE_EDIT_DOUBLE_CLICK_WINDOW_MS = 350;
158
+
159
+ function stripAnsiSgr(value: string): string {
160
+ let output = '';
161
+ let index = 0;
162
+ while (index < value.length) {
163
+ const char = value[index]!;
164
+ if (char === '\u001b' && value[index + 1] === '[') {
165
+ index += 2;
166
+ while (index < value.length) {
167
+ const nextChar = value[index]!;
168
+ if (nextChar >= '@' && nextChar <= '~') {
169
+ index += 1;
170
+ break;
171
+ }
172
+ index += 1;
173
+ }
174
+ continue;
175
+ }
176
+ output += char;
177
+ index += 1;
178
+ }
179
+ return output;
180
+ }
181
+
182
+ function selectionTextFromHomePaneRows(
183
+ rows: readonly string[],
184
+ viewportTop: number,
185
+ selection: PaneSelection,
186
+ ): string {
187
+ const normalized =
188
+ compareSelectionPoints(selection.anchor, selection.focus) <= 0
189
+ ? {
190
+ start: selection.anchor,
191
+ end: selection.focus,
192
+ }
193
+ : {
194
+ start: selection.focus,
195
+ end: selection.anchor,
196
+ };
197
+ const selectedRows: string[] = [];
198
+ for (let rowAbs = normalized.start.rowAbs; rowAbs <= normalized.end.rowAbs; rowAbs += 1) {
199
+ const rowIndex = rowAbs - viewportTop;
200
+ const rowText = rows[rowIndex] ?? '';
201
+ const rowStart = rowAbs === normalized.start.rowAbs ? normalized.start.col : 0;
202
+ const rowEnd = rowAbs === normalized.end.rowAbs ? normalized.end.col : rowText.length - 1;
203
+ if (rowEnd < rowStart || rowStart >= rowText.length) {
204
+ selectedRows.push('');
205
+ continue;
206
+ }
207
+ const start = Math.max(0, rowStart);
208
+ const endExclusive = Math.min(rowText.length, rowEnd + 1);
209
+ selectedRows.push(rowText.slice(start, endExclusive));
210
+ }
211
+ return selectedRows.join('\n');
212
+ }
213
+
214
+ export function createTuiMainPaneInteractions<TConversation extends ActiveConversationLike>(
215
+ options: TuiMainPaneInteractionsOptions<TConversation>,
216
+ ): TuiMainPaneInteractions {
217
+ const nowMs = options.nowMs ?? (() => Date.now());
218
+ const homePaneEditDoubleClickWindowMs =
219
+ options.homePaneEditDoubleClickWindowMs ?? DEFAULT_HOME_PANE_EDIT_DOUBLE_CLICK_WINDOW_MS;
220
+ const writeClipboard = options.writeTextToClipboard ?? writeTextToClipboard;
221
+
222
+ const mainPanePointerInput = new MainPanePointerInput<WorkspaceProjectPaneSnapshot>(
223
+ {
224
+ getMainPaneMode: () => options.workspace.mainPaneMode,
225
+ getProjectPaneSnapshot: () => options.workspace.projectPaneSnapshot,
226
+ getProjectPaneScrollTop: () => options.workspace.projectPaneScrollTop,
227
+ projectPaneActionAtRow: options.project.projectPaneActionAtRow,
228
+ openNewThreadPrompt: options.project.openNewThreadPrompt,
229
+ queueCloseDirectory: options.project.queueCloseDirectory,
230
+ actionAtCell: (rowIndex, colIndex) =>
231
+ taskFocusedPaneActionAtCell(options.workspace.latestTaskPaneView, rowIndex, colIndex),
232
+ actionAtRow: (rowIndex) =>
233
+ taskFocusedPaneActionAtRow(options.workspace.latestTaskPaneView, rowIndex),
234
+ clearTaskEditClickState: () => {
235
+ options.workspace.taskPaneTaskEditClickState = null;
236
+ },
237
+ clearRepositoryEditClickState: () => {
238
+ options.workspace.taskPaneRepositoryEditClickState = null;
239
+ },
240
+ clearHomePaneDragState: () => {
241
+ options.workspace.homePaneDragState = null;
242
+ },
243
+ getTaskRepositoryDropdownOpen: () => options.workspace.taskRepositoryDropdownOpen,
244
+ setTaskRepositoryDropdownOpen: (open) => {
245
+ options.workspace.taskRepositoryDropdownOpen = open;
246
+ },
247
+ taskIdAtRow: (rowIndex) =>
248
+ taskFocusedPaneTaskIdAtRow(options.workspace.latestTaskPaneView, rowIndex),
249
+ repositoryIdAtRow: (rowIndex) =>
250
+ taskFocusedPaneRepositoryIdAtRow(options.workspace.latestTaskPaneView, rowIndex),
251
+ rowTextAtRow: (rowIndex) =>
252
+ options.workspace.latestTaskPaneView.plainRows?.[rowIndex] ?? null,
253
+ selectTaskById: options.task.selectTaskById,
254
+ selectRepositoryById: options.task.selectRepositoryById,
255
+ runTaskPaneAction: options.task.runTaskPaneAction,
256
+ nowMs,
257
+ homePaneEditDoubleClickWindowMs,
258
+ getTaskEditClickState: () => options.workspace.taskPaneTaskEditClickState,
259
+ getRepositoryEditClickState: () => options.workspace.taskPaneRepositoryEditClickState,
260
+ clearTaskPaneNotice: () => {
261
+ options.workspace.taskPaneNotice = null;
262
+ },
263
+ setTaskEditClickState: (next) => {
264
+ options.workspace.taskPaneTaskEditClickState = next;
265
+ },
266
+ setRepositoryEditClickState: (next) => {
267
+ options.workspace.taskPaneRepositoryEditClickState = next;
268
+ },
269
+ setHomePaneDragState: (next) => {
270
+ options.workspace.homePaneDragState = next;
271
+ },
272
+ openTaskEditPrompt: options.task.openTaskEditPrompt,
273
+ openRepositoryPromptForEdit: options.repository.openRepositoryPromptForEdit,
274
+ markDirty: options.markDirty,
275
+ },
276
+ {
277
+ handleProjectPaneActionClick: (input) =>
278
+ handleProjectPaneActionClick({
279
+ ...input,
280
+ handleProjectPaneAction: (action, directoryId) => {
281
+ if (action === 'project.github.refresh') {
282
+ options.project.refreshGitHubReview(directoryId);
283
+ return true;
284
+ }
285
+ const togglePrefix = 'project.github.toggle:';
286
+ if (!action.startsWith(togglePrefix)) {
287
+ return false;
288
+ }
289
+ const nodeId = action.slice(togglePrefix.length).trim();
290
+ if (nodeId.length === 0) {
291
+ return false;
292
+ }
293
+ return options.project.toggleGitHubNode(directoryId, nodeId);
294
+ },
295
+ }),
296
+ handleHomePanePointerClick,
297
+ },
298
+ );
299
+
300
+ const pointerRoutingInput = new PointerRoutingInput(
301
+ {
302
+ getPaneDividerDragActive: () => options.workspace.paneDividerDragActive,
303
+ setPaneDividerDragActive: (active) => {
304
+ options.workspace.paneDividerDragActive = active;
305
+ },
306
+ applyPaneDividerAtCol: options.layout.applyPaneDividerAtCol,
307
+ getHomePaneDragState: () => options.workspace.homePaneDragState,
308
+ setHomePaneDragState: (next) => {
309
+ options.workspace.homePaneDragState = next;
310
+ },
311
+ getMainPaneMode: () => options.workspace.mainPaneMode,
312
+ taskIdAtRow: (index) =>
313
+ taskFocusedPaneTaskIdAtRow(options.workspace.latestTaskPaneView, index),
314
+ repositoryIdAtRow: (index) =>
315
+ taskFocusedPaneRepositoryIdAtRow(options.workspace.latestTaskPaneView, index),
316
+ reorderTaskByDrop: options.task.reorderTaskByDrop,
317
+ reorderRepositoryByDrop: options.task.reorderRepositoryByDrop,
318
+ onProjectWheel: (delta) => {
319
+ options.workspace.projectPaneScrollTop = Math.max(
320
+ 0,
321
+ options.workspace.projectPaneScrollTop + delta,
322
+ );
323
+ },
324
+ onHomeWheel: (delta) => {
325
+ options.workspace.taskPaneScrollTop = Math.max(
326
+ 0,
327
+ options.workspace.taskPaneScrollTop + delta,
328
+ );
329
+ },
330
+ onNimWheel: (_delta) => {
331
+ // Nim pane currently has no independent scroll state in the TUI adapter.
332
+ },
333
+ markDirty: options.markDirty,
334
+ },
335
+ {
336
+ handlePaneDividerDragInput,
337
+ handleHomePaneDragRelease,
338
+ handleSeparatorPointerPress,
339
+ handleMainPaneWheelInput,
340
+ handleHomePaneDragMove,
341
+ },
342
+ );
343
+
344
+ const conversationSelectionInput = new ConversationSelectionInput(
345
+ {
346
+ getSelection: () => options.workspace.selection,
347
+ setSelection: (next) => {
348
+ options.workspace.selection = next;
349
+ },
350
+ getSelectionDrag: () => options.workspace.selectionDrag,
351
+ setSelectionDrag: (next) => {
352
+ options.workspace.selectionDrag = next;
353
+ },
354
+ pinViewportForSelection: options.selection.pinViewportForSelection,
355
+ releaseViewportPinForSelection: options.selection.releaseViewportPinForSelection,
356
+ markDirty: options.markDirty,
357
+ },
358
+ {
359
+ pointFromMouseEvent,
360
+ reduceConversationMouseSelection,
361
+ selectionText,
362
+ },
363
+ );
364
+
365
+ const mainPaneInputTokenRouter = new InputTokenRouter(
366
+ {
367
+ getMainPaneMode: () => options.workspace.mainPaneMode,
368
+ getHomePaneSelectionContext: () => {
369
+ const plainRows =
370
+ options.workspace.latestTaskPaneView.plainRows ??
371
+ options.workspace.latestTaskPaneView.rows;
372
+ const rows = plainRows.map((row) => stripAnsiSgr(row));
373
+ const viewportTop = Math.max(0, options.workspace.latestTaskPaneView.top);
374
+ return {
375
+ viewportTop,
376
+ totalRows: Math.max(1, viewportTop + rows.length),
377
+ resolveSelectionText: (selection) =>
378
+ selectionTextFromHomePaneRows(rows, viewportTop, selection),
379
+ };
380
+ },
381
+ pointerRoutingInput,
382
+ mainPanePointerInput,
383
+ leftRailPointerInput: options.leftRailPointerInput,
384
+ conversationSelectionInput,
385
+ },
386
+ {
387
+ classifyPaneAt: (layout, col, row) =>
388
+ classifyPaneAt(layout as Parameters<typeof classifyPaneAt>[0], col, row),
389
+ isLeftButtonPress,
390
+ hasAltModifier,
391
+ hasShiftModifier: (code) => (code & 0b0000_0100) !== 0,
392
+ isMotionMouseCode,
393
+ },
394
+ );
395
+
396
+ const inputPreflight = new InputPreflight(
397
+ {
398
+ isShuttingDown: options.runtime.isShuttingDown,
399
+ routeModalInput: options.modal.routeModalInput,
400
+ handleEscapeInput: (input) => {
401
+ if (options.workspace.selection !== null || options.workspace.selectionDrag !== null) {
402
+ options.workspace.selection = null;
403
+ options.workspace.selectionDrag = null;
404
+ options.selection.releaseViewportPinForSelection();
405
+ options.markDirty();
406
+ }
407
+ if (options.workspace.mainPaneMode === 'conversation') {
408
+ const escapeTarget = options.runtime.getActiveConversation();
409
+ if (escapeTarget !== null) {
410
+ options.runtime.sendInputToSession(escapeTarget.sessionId, input);
411
+ }
412
+ return;
413
+ }
414
+ if (options.handleEscapeInMainPaneMode !== undefined) {
415
+ options.handleEscapeInMainPaneMode(
416
+ options.workspace.mainPaneMode as NonConversationMainPaneMode,
417
+ );
418
+ }
419
+ },
420
+ onFocusIn: () => {
421
+ options.runtime.enableInputMode();
422
+ options.markDirty();
423
+ },
424
+ onFocusOut: () => {
425
+ options.markDirty();
426
+ },
427
+ handleRepositoryFoldInput: options.shortcuts.handleRepositoryFoldInput,
428
+ handleGlobalShortcutInput: options.shortcuts.handleGlobalShortcutInput,
429
+ handleTaskPaneShortcutInput: (input) => {
430
+ const handled = options.task.handleShortcutInput(input);
431
+ if (
432
+ handled &&
433
+ (options.workspace.selection !== null || options.workspace.selectionDrag !== null)
434
+ ) {
435
+ options.workspace.selection = null;
436
+ options.workspace.selectionDrag = null;
437
+ options.selection.releaseViewportPinForSelection();
438
+ options.markDirty();
439
+ }
440
+ return handled;
441
+ },
442
+ handleCopyShortcutInput: (input) => {
443
+ if (options.workspace.selection === null || !isCopyShortcutInput(input)) {
444
+ return false;
445
+ }
446
+ let textToCopy = options.workspace.selection.text;
447
+ if (options.workspace.mainPaneMode === 'conversation') {
448
+ const active = options.runtime.getActiveConversation();
449
+ if (active === null) {
450
+ return true;
451
+ }
452
+ const selectedFrame = active.oracle.snapshotWithoutHash();
453
+ textToCopy = selectionText(selectedFrame, options.workspace.selection);
454
+ }
455
+ if (textToCopy.length === 0) {
456
+ return true;
457
+ }
458
+ const copied = writeClipboard(textToCopy);
459
+ if (copied) {
460
+ options.markDirty();
461
+ }
462
+ return true;
463
+ },
464
+ },
465
+ {
466
+ extractFocusEvents,
467
+ },
468
+ );
469
+
470
+ const conversationInputForwarder = new ConversationInputForwarder<
471
+ TerminalSnapshotFrameCore,
472
+ TConversation
473
+ >({
474
+ getInputRemainder: options.getInputRemainder,
475
+ setInputRemainder: options.setInputRemainder,
476
+ getMainPaneMode: () => options.workspace.mainPaneMode,
477
+ getLayout: options.getLayout,
478
+ inputTokenRouter: mainPaneInputTokenRouter,
479
+ getActiveConversation: options.runtime.getActiveConversation,
480
+ markDirty: options.markDirty,
481
+ isControlledByLocalHuman: options.runtime.isControlledByLocalHuman,
482
+ controllerId: options.controllerId,
483
+ sendInputToSession: options.runtime.sendInputToSession,
484
+ noteGitActivity: options.noteGitActivity,
485
+ parseMuxInputChunk,
486
+ routeInputTokensForConversation,
487
+ classifyPaneAt,
488
+ normalizeMuxKeyboardInputForPty,
489
+ ...(options.handlePassthroughTextInMainPaneMode === undefined
490
+ ? {}
491
+ : {
492
+ handlePassthroughTextInMainPaneMode: options.handlePassthroughTextInMainPaneMode,
493
+ }),
494
+ });
495
+
496
+ const handleInput = (input: Buffer): void => {
497
+ const sanitized = inputPreflight.nextInput(input);
498
+ if (sanitized === null) {
499
+ return;
500
+ }
501
+ conversationInputForwarder.handleInput(sanitized);
502
+ };
503
+
504
+ return {
505
+ handleInput,
506
+ mainPaneInputTokenRouter,
507
+ inputPreflight,
508
+ };
509
+ }
@@ -0,0 +1,71 @@
1
+ interface ModalDismissResult {
2
+ readonly handled: boolean;
3
+ readonly inputRemainder: string;
4
+ }
5
+
6
+ interface ModalDismissManager {
7
+ dismissOnOutsideClick(input: {
8
+ readonly input: Buffer;
9
+ readonly inputRemainder: string;
10
+ readonly layoutCols: number;
11
+ readonly viewportRows: number;
12
+ readonly dismiss: () => void;
13
+ readonly onInsidePointerPress?: (col: number, row: number) => boolean;
14
+ }): ModalDismissResult;
15
+ }
16
+
17
+ export interface DismissModalOnOutsideClickInput {
18
+ readonly modalManager: ModalDismissManager;
19
+ readonly layoutCols: number;
20
+ readonly viewportRows: number;
21
+ readonly input: Buffer;
22
+ readonly dismiss: () => void;
23
+ readonly onInsidePointerPress?: (col: number, row: number) => boolean;
24
+ }
25
+
26
+ export interface TuiModalInputRemainderState {
27
+ getInputRemainder(): string;
28
+ setInputRemainder(next: string): void;
29
+ dismissModalOnOutsideClick(input: DismissModalOnOutsideClickInput): boolean;
30
+ }
31
+
32
+ export function createTuiModalInputRemainderState(
33
+ initialInputRemainder = '',
34
+ ): TuiModalInputRemainderState {
35
+ let inputRemainder = initialInputRemainder;
36
+ return {
37
+ getInputRemainder: (): string => inputRemainder,
38
+ setInputRemainder: (next: string): void => {
39
+ inputRemainder = next;
40
+ },
41
+ dismissModalOnOutsideClick: (input: DismissModalOnOutsideClickInput): boolean => {
42
+ const result = input.modalManager.dismissOnOutsideClick({
43
+ input: input.input,
44
+ inputRemainder,
45
+ layoutCols: input.layoutCols,
46
+ viewportRows: input.viewportRows,
47
+ dismiss: input.dismiss,
48
+ ...(input.onInsidePointerPress === undefined
49
+ ? {}
50
+ : {
51
+ onInsidePointerPress: input.onInsidePointerPress,
52
+ }),
53
+ });
54
+ inputRemainder = result.inputRemainder;
55
+ return result.handled;
56
+ },
57
+ };
58
+ }
59
+
60
+ export interface RouteTuiModalInputOptions {
61
+ readonly input: Buffer;
62
+ readonly routeReleaseNotesModalInput: (input: Buffer) => boolean;
63
+ readonly routeModalInput: (input: Buffer) => boolean;
64
+ }
65
+
66
+ export function routeTuiModalInput(options: RouteTuiModalInputOptions): boolean {
67
+ if (options.routeReleaseNotesModalInput(options.input)) {
68
+ return true;
69
+ }
70
+ return options.routeModalInput(options.input);
71
+ }
@@ -0,0 +1,88 @@
1
+ import type { TaskComposerBuffer } from '../../mux/task-composer.ts';
2
+ import type {
3
+ TaskFocusedPaneRepositoryRecord,
4
+ TaskFocusedPaneTaskRecord,
5
+ } from '../../mux/task-focused-pane.ts';
6
+ import type { RuntimeRenderPipelineSnapshot } from '../../services/runtime-render-pipeline.ts';
7
+ import { snapshotTaskComposerBuffers } from '../../services/runtime-task-composer-snapshot.ts';
8
+
9
+ interface TuiRenderSnapshotConversationLookup<TConversation> {
10
+ readonlyConversations(): ReadonlyMap<string, TConversation>;
11
+ orderedIds(): readonly string[];
12
+ readonly activeConversationId: string | null;
13
+ }
14
+
15
+ interface TuiRenderSnapshotDirectoryLookup<TDirectoryRecord> {
16
+ readonlyDirectories(): ReadonlyMap<string, TDirectoryRecord>;
17
+ }
18
+
19
+ interface TuiRenderSnapshotRepositoryLookup<
20
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
21
+ > {
22
+ readonlyRepositories(): ReadonlyMap<string, TRepositoryRecord>;
23
+ }
24
+
25
+ interface TuiRenderSnapshotTaskLookup<TTaskRecord extends TaskFocusedPaneTaskRecord> {
26
+ readonlyTasks(): ReadonlyMap<string, TTaskRecord>;
27
+ readonlyTaskComposers(): ReadonlyMap<string, TaskComposerBuffer>;
28
+ }
29
+
30
+ interface TuiRenderSnapshotProcessUsageLookup<TProcessUsage> {
31
+ readonlyUsage(): ReadonlyMap<string, TProcessUsage>;
32
+ }
33
+
34
+ export interface TuiRenderSnapshotAdapterOptions<
35
+ TDirectoryRecord,
36
+ TConversation,
37
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
38
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
39
+ TProcessUsage,
40
+ > {
41
+ readonly directories: TuiRenderSnapshotDirectoryLookup<TDirectoryRecord>;
42
+ readonly conversations: TuiRenderSnapshotConversationLookup<TConversation>;
43
+ readonly repositories: TuiRenderSnapshotRepositoryLookup<TRepositoryRecord>;
44
+ readonly tasks: TuiRenderSnapshotTaskLookup<TTaskRecord>;
45
+ readonly processUsage: TuiRenderSnapshotProcessUsageLookup<TProcessUsage>;
46
+ readonly snapshotTaskComposers?: (
47
+ taskComposers: ReadonlyMap<string, TaskComposerBuffer>,
48
+ ) => ReadonlyMap<string, TaskComposerBuffer>;
49
+ }
50
+
51
+ export function readTuiRenderSnapshot<
52
+ TDirectoryRecord,
53
+ TConversation,
54
+ TRepositoryRecord extends TaskFocusedPaneRepositoryRecord,
55
+ TTaskRecord extends TaskFocusedPaneTaskRecord,
56
+ TProcessUsage,
57
+ >(
58
+ options: TuiRenderSnapshotAdapterOptions<
59
+ TDirectoryRecord,
60
+ TConversation,
61
+ TRepositoryRecord,
62
+ TTaskRecord,
63
+ TProcessUsage
64
+ >,
65
+ ): RuntimeRenderPipelineSnapshot<
66
+ TDirectoryRecord,
67
+ TConversation,
68
+ TRepositoryRecord,
69
+ TTaskRecord,
70
+ TProcessUsage
71
+ > {
72
+ const snapshotTaskComposers = options.snapshotTaskComposers ?? snapshotTaskComposerBuffers;
73
+ return {
74
+ leftRail: {
75
+ repositories: options.repositories.readonlyRepositories(),
76
+ directories: options.directories.readonlyDirectories(),
77
+ conversations: options.conversations.readonlyConversations(),
78
+ orderedConversationIds: options.conversations.orderedIds(),
79
+ processUsageBySessionId: options.processUsage.readonlyUsage(),
80
+ activeConversationId: options.conversations.activeConversationId,
81
+ },
82
+ rightPane: {
83
+ repositories: options.repositories.readonlyRepositories(),
84
+ tasks: options.tasks.readonlyTasks(),
85
+ taskComposers: snapshotTaskComposers(options.tasks.readonlyTaskComposers()),
86
+ },
87
+ };
88
+ }