@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,94 @@
1
+ export interface TaskEditorPromptInputState {
2
+ title: string;
3
+ description: string;
4
+ repositoryIds: readonly string[];
5
+ repositoryIndex: number;
6
+ fieldIndex: 0 | 1 | 2;
7
+ }
8
+
9
+ interface TaskEditorPromptReduction {
10
+ title: string;
11
+ description: string;
12
+ repositoryIndex: number;
13
+ fieldIndex: 0 | 1 | 2;
14
+ submit: boolean;
15
+ }
16
+
17
+ interface LinePromptReduction {
18
+ value: string;
19
+ submit: boolean;
20
+ }
21
+
22
+ export function reduceLinePromptInput(value: string, input: Buffer): LinePromptReduction {
23
+ let nextValue = value;
24
+ let submit = false;
25
+ for (const byte of input) {
26
+ if (byte === 0x0d || byte === 0x0a) {
27
+ submit = true;
28
+ break;
29
+ }
30
+ if (byte === 0x7f || byte === 0x08) {
31
+ nextValue = nextValue.slice(0, -1);
32
+ continue;
33
+ }
34
+ if (byte >= 32 && byte <= 126) {
35
+ nextValue += String.fromCharCode(byte);
36
+ }
37
+ }
38
+ return {
39
+ value: nextValue,
40
+ submit,
41
+ };
42
+ }
43
+
44
+ export function reduceTaskEditorPromptInput(
45
+ prompt: TaskEditorPromptInputState,
46
+ input: Buffer,
47
+ ): TaskEditorPromptReduction {
48
+ let nextTitle = prompt.title;
49
+ let nextDescription = prompt.description;
50
+ let nextFieldIndex = prompt.fieldIndex;
51
+ let nextRepositoryIndex = prompt.repositoryIndex;
52
+ let submit = false;
53
+ const text = input.toString('utf8');
54
+ if (text === '\u001b[C') {
55
+ nextFieldIndex = 1;
56
+ nextRepositoryIndex = Math.min(prompt.repositoryIds.length - 1, prompt.repositoryIndex + 1);
57
+ } else if (text === '\u001b[D') {
58
+ nextFieldIndex = 1;
59
+ nextRepositoryIndex = Math.max(0, prompt.repositoryIndex - 1);
60
+ } else {
61
+ for (const byte of input) {
62
+ if (byte === 0x0d || byte === 0x0a) {
63
+ submit = true;
64
+ break;
65
+ }
66
+ if (byte === 0x09) {
67
+ nextFieldIndex = ((nextFieldIndex + 1) % 3) as 0 | 1 | 2;
68
+ continue;
69
+ }
70
+ if (byte === 0x7f || byte === 0x08) {
71
+ if (nextFieldIndex === 0) {
72
+ nextTitle = nextTitle.slice(0, -1);
73
+ } else if (nextFieldIndex === 2) {
74
+ nextDescription = nextDescription.slice(0, -1);
75
+ }
76
+ continue;
77
+ }
78
+ if (byte >= 32 && byte <= 126) {
79
+ if (nextFieldIndex === 0) {
80
+ nextTitle += String.fromCharCode(byte);
81
+ } else if (nextFieldIndex === 2) {
82
+ nextDescription += String.fromCharCode(byte);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ return {
88
+ title: nextTitle,
89
+ description: nextDescription,
90
+ repositoryIndex: nextRepositoryIndex,
91
+ fieldIndex: nextFieldIndex,
92
+ submit,
93
+ };
94
+ }
@@ -0,0 +1,287 @@
1
+ import {
2
+ CONVERSATION_EDIT_ARCHIVE_BUTTON_LABEL,
3
+ NEW_THREAD_MODAL_CLAUDE_BUTTON,
4
+ NEW_THREAD_MODAL_CODEX_BUTTON,
5
+ NEW_THREAD_MODAL_CRITIQUE_BUTTON,
6
+ NEW_THREAD_MODAL_CURSOR_BUTTON,
7
+ NEW_THREAD_MODAL_TERMINAL_BUTTON,
8
+ resolveGoldenModalSize,
9
+ } from '../harness-core-ui.ts';
10
+ import {
11
+ COMMAND_MENU_MAX_RESULTS,
12
+ resolveCommandMenuMatches,
13
+ type CommandMenuActionDescriptor,
14
+ type CommandMenuState,
15
+ } from './command-menu.ts';
16
+ import type { createNewThreadPromptState } from '../new-thread-prompt.ts';
17
+ import { newThreadPromptBodyLines } from '../new-thread-prompt.ts';
18
+ import { buildUiModalOverlay } from '../../ui/kit.ts';
19
+
20
+ type NewThreadPromptState = ReturnType<typeof createNewThreadPromptState>;
21
+ type UiModalThemeInput = NonNullable<Parameters<typeof buildUiModalOverlay>[0]['theme']>;
22
+
23
+ interface TaskEditorPromptOverlayState {
24
+ mode: 'create' | 'edit';
25
+ title: string;
26
+ description: string;
27
+ repositoryIds: readonly string[];
28
+ repositoryIndex: number;
29
+ fieldIndex: 0 | 1 | 2;
30
+ error: string | null;
31
+ }
32
+
33
+ interface RepositoryPromptOverlayState {
34
+ readonly mode: 'add' | 'edit';
35
+ readonly value: string;
36
+ readonly error: string | null;
37
+ }
38
+
39
+ interface ConversationTitleOverlayState {
40
+ value: string;
41
+ lastSavedValue: string;
42
+ error: string | null;
43
+ persistInFlight: boolean;
44
+ }
45
+
46
+ export function buildNewThreadModalOverlay(
47
+ layoutCols: number,
48
+ viewportRows: number,
49
+ prompt: NewThreadPromptState | null,
50
+ theme: UiModalThemeInput,
51
+ ): ReturnType<typeof buildUiModalOverlay> | null {
52
+ if (prompt === null) {
53
+ return null;
54
+ }
55
+ const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
56
+ preferredHeight: 15,
57
+ minWidth: 22,
58
+ maxWidth: 36,
59
+ });
60
+ return buildUiModalOverlay({
61
+ viewportCols: layoutCols,
62
+ viewportRows,
63
+ width: modalSize.width,
64
+ height: modalSize.height,
65
+ anchor: 'center',
66
+ marginRows: 1,
67
+ title: 'New Thread',
68
+ bodyLines: newThreadPromptBodyLines(prompt, {
69
+ codexButtonLabel: NEW_THREAD_MODAL_CODEX_BUTTON,
70
+ claudeButtonLabel: NEW_THREAD_MODAL_CLAUDE_BUTTON,
71
+ cursorButtonLabel: NEW_THREAD_MODAL_CURSOR_BUTTON,
72
+ terminalButtonLabel: NEW_THREAD_MODAL_TERMINAL_BUTTON,
73
+ critiqueButtonLabel: NEW_THREAD_MODAL_CRITIQUE_BUTTON,
74
+ }),
75
+ footer: 'enter create esc',
76
+ theme,
77
+ });
78
+ }
79
+
80
+ export function buildCommandMenuModalOverlay(
81
+ layoutCols: number,
82
+ viewportRows: number,
83
+ menu: CommandMenuState | null,
84
+ actions: readonly CommandMenuActionDescriptor[],
85
+ theme: UiModalThemeInput,
86
+ ): ReturnType<typeof buildUiModalOverlay> | null {
87
+ if (menu === null) {
88
+ return null;
89
+ }
90
+ const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
91
+ preferredHeight: 18,
92
+ minWidth: 34,
93
+ maxWidth: 72,
94
+ });
95
+ const matches = resolveCommandMenuMatches(actions, menu.query, COMMAND_MENU_MAX_RESULTS);
96
+ const selectedIndex =
97
+ matches.length === 0 ? 0 : Math.max(0, Math.min(matches.length - 1, menu.selectedIndex));
98
+ const bodyLines: string[] = [`search: ${menu.query}_`, ''];
99
+ if (matches.length === 0) {
100
+ bodyLines.push('no actions match');
101
+ } else {
102
+ for (let index = 0; index < matches.length; index += 1) {
103
+ const match = matches[index]!;
104
+ const prefix = index === selectedIndex ? '>' : ' ';
105
+ const detail = match.action.detail?.trim() ?? '';
106
+ bodyLines.push(
107
+ detail.length > 0
108
+ ? `${prefix} ${match.action.title} - ${detail}`
109
+ : `${prefix} ${match.action.title}`,
110
+ );
111
+ }
112
+ }
113
+ bodyLines.push('', 'type to filter');
114
+ return buildUiModalOverlay({
115
+ viewportCols: layoutCols,
116
+ viewportRows,
117
+ width: modalSize.width,
118
+ height: modalSize.height,
119
+ anchor: 'center',
120
+ marginRows: 1,
121
+ title: 'Command Menu',
122
+ bodyLines,
123
+ footer: 'enter run esc',
124
+ theme,
125
+ });
126
+ }
127
+
128
+ export function buildAddDirectoryModalOverlay(
129
+ layoutCols: number,
130
+ viewportRows: number,
131
+ prompt: { value: string; error: string | null } | null,
132
+ theme: UiModalThemeInput,
133
+ ): ReturnType<typeof buildUiModalOverlay> | null {
134
+ if (prompt === null) {
135
+ return null;
136
+ }
137
+ const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
138
+ preferredHeight: 15,
139
+ minWidth: 24,
140
+ maxWidth: 40,
141
+ });
142
+ const promptValue = prompt.value.length > 0 ? prompt.value : '.';
143
+ const addDirectoryBody = [`path: ${promptValue}_`];
144
+ if (prompt.error !== null && prompt.error.length > 0) {
145
+ addDirectoryBody.push(`error: ${prompt.error}`);
146
+ } else {
147
+ addDirectoryBody.push('add a workspace project for new threads');
148
+ }
149
+ return buildUiModalOverlay({
150
+ viewportCols: layoutCols,
151
+ viewportRows,
152
+ width: modalSize.width,
153
+ height: modalSize.height,
154
+ anchor: 'center',
155
+ marginRows: 1,
156
+ title: 'Add Project',
157
+ bodyLines: addDirectoryBody,
158
+ footer: 'enter save esc',
159
+ theme,
160
+ });
161
+ }
162
+
163
+ export function buildTaskEditorModalOverlay(
164
+ layoutCols: number,
165
+ viewportRows: number,
166
+ prompt: TaskEditorPromptOverlayState | null,
167
+ resolveRepositoryName: (repositoryId: string) => string | null,
168
+ theme: UiModalThemeInput,
169
+ ): ReturnType<typeof buildUiModalOverlay> | null {
170
+ if (prompt === null) {
171
+ return null;
172
+ }
173
+ const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
174
+ preferredHeight: 18,
175
+ minWidth: 30,
176
+ maxWidth: 56,
177
+ });
178
+ const selectedRepositoryId = prompt.repositoryIds[prompt.repositoryIndex] ?? null;
179
+ const selectedRepositoryName =
180
+ selectedRepositoryId === null
181
+ ? '(none)'
182
+ : (resolveRepositoryName(selectedRepositoryId) ?? '(missing)');
183
+ const taskBody = [
184
+ `${prompt.fieldIndex === 0 ? '>' : ' '} title: ${prompt.title}${prompt.fieldIndex === 0 ? '_' : ''}`,
185
+ `${prompt.fieldIndex === 1 ? '>' : ' '} repository: ${selectedRepositoryName}`,
186
+ `${prompt.fieldIndex === 2 ? '>' : ' '} description: ${prompt.description}${
187
+ prompt.fieldIndex === 2 ? '_' : ''
188
+ }`,
189
+ '',
190
+ 'tab next field',
191
+ 'left/right change repository',
192
+ ];
193
+ if (prompt.error !== null && prompt.error.length > 0) {
194
+ taskBody.push(`error: ${prompt.error}`);
195
+ }
196
+ return buildUiModalOverlay({
197
+ viewportCols: layoutCols,
198
+ viewportRows,
199
+ width: modalSize.width,
200
+ height: modalSize.height,
201
+ anchor: 'center',
202
+ marginRows: 1,
203
+ title: prompt.mode === 'create' ? 'New Task' : 'Edit Task',
204
+ bodyLines: taskBody,
205
+ footer: 'enter save esc',
206
+ theme,
207
+ });
208
+ }
209
+
210
+ export function buildRepositoryModalOverlay(
211
+ layoutCols: number,
212
+ viewportRows: number,
213
+ prompt: RepositoryPromptOverlayState | null,
214
+ theme: UiModalThemeInput,
215
+ ): ReturnType<typeof buildUiModalOverlay> | null {
216
+ if (prompt === null) {
217
+ return null;
218
+ }
219
+ const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
220
+ preferredHeight: 15,
221
+ minWidth: 28,
222
+ maxWidth: 56,
223
+ });
224
+ const promptValue = prompt.value.length > 0 ? prompt.value : 'https://github.com/org/repo';
225
+ const bodyLines = [`github url: ${promptValue}_`];
226
+ if (prompt.error !== null && prompt.error.length > 0) {
227
+ bodyLines.push(`error: ${prompt.error}`);
228
+ } else if (prompt.mode === 'add') {
229
+ bodyLines.push('add a repository and link matching projects');
230
+ } else {
231
+ bodyLines.push('update repository github url');
232
+ }
233
+ return buildUiModalOverlay({
234
+ viewportCols: layoutCols,
235
+ viewportRows,
236
+ width: modalSize.width,
237
+ height: modalSize.height,
238
+ anchor: 'center',
239
+ marginRows: 1,
240
+ title: prompt.mode === 'add' ? 'Add Repository' : 'Edit Repository',
241
+ bodyLines,
242
+ footer: 'enter save esc',
243
+ theme,
244
+ });
245
+ }
246
+
247
+ export function buildConversationTitleModalOverlay(
248
+ layoutCols: number,
249
+ viewportRows: number,
250
+ edit: ConversationTitleOverlayState | null,
251
+ theme: UiModalThemeInput,
252
+ ): ReturnType<typeof buildUiModalOverlay> | null {
253
+ if (edit === null) {
254
+ return null;
255
+ }
256
+ const modalSize = resolveGoldenModalSize(layoutCols, viewportRows, {
257
+ preferredHeight: 18,
258
+ minWidth: 26,
259
+ maxWidth: 44,
260
+ });
261
+ const editState = edit.persistInFlight
262
+ ? 'saving'
263
+ : edit.value === edit.lastSavedValue
264
+ ? 'saved'
265
+ : 'pending';
266
+ const editBody = [
267
+ `title: ${edit.value}_`,
268
+ `state: ${editState}`,
269
+ '',
270
+ CONVERSATION_EDIT_ARCHIVE_BUTTON_LABEL,
271
+ ];
272
+ if (edit.error !== null && edit.error.length > 0) {
273
+ editBody.push(`error: ${edit.error}`);
274
+ }
275
+ return buildUiModalOverlay({
276
+ viewportCols: layoutCols,
277
+ viewportRows,
278
+ width: modalSize.width,
279
+ height: modalSize.height,
280
+ anchor: 'center',
281
+ marginRows: 1,
282
+ title: 'Edit Thread Title',
283
+ bodyLines: editBody,
284
+ footer: 'type to save enter done',
285
+ theme,
286
+ });
287
+ }
@@ -0,0 +1,70 @@
1
+ import { parseMuxInputChunk } from '../dual-pane-core.ts';
2
+ import { type buildUiModalOverlay } from '../../ui/kit.ts';
3
+ import { isMotionMouseCode, isWheelMouseCode } from './selection.ts';
4
+
5
+ interface DismissModalOnOutsideClickOptions {
6
+ input: Buffer;
7
+ inputRemainder: string;
8
+ dismiss: () => void;
9
+ buildCurrentModalOverlay: () => ReturnType<typeof buildUiModalOverlay> | null;
10
+ onInsidePointerPress?: (col: number, row: number) => boolean;
11
+ isOverlayHit: (
12
+ overlay: ReturnType<typeof buildUiModalOverlay>,
13
+ col: number,
14
+ row: number,
15
+ ) => boolean;
16
+ }
17
+
18
+ interface DismissModalOnOutsideClickResult {
19
+ handled: boolean;
20
+ inputRemainder: string;
21
+ }
22
+
23
+ export function dismissModalOnOutsideClick(
24
+ options: DismissModalOnOutsideClickOptions,
25
+ ): DismissModalOnOutsideClickResult {
26
+ const { input, dismiss, buildCurrentModalOverlay, onInsidePointerPress, isOverlayHit } = options;
27
+ if (!input.includes(0x1b)) {
28
+ return {
29
+ handled: false,
30
+ inputRemainder: options.inputRemainder,
31
+ };
32
+ }
33
+ const parsed = parseMuxInputChunk(options.inputRemainder, input);
34
+ const modalOverlay = buildCurrentModalOverlay();
35
+ if (modalOverlay === null) {
36
+ return {
37
+ handled: true,
38
+ inputRemainder: parsed.remainder,
39
+ };
40
+ }
41
+ for (const token of parsed.tokens) {
42
+ if (token.kind !== 'mouse') {
43
+ continue;
44
+ }
45
+ const pointerPress =
46
+ token.event.final === 'M' &&
47
+ !isWheelMouseCode(token.event.code) &&
48
+ !isMotionMouseCode(token.event.code);
49
+ if (!pointerPress) {
50
+ continue;
51
+ }
52
+ if (!isOverlayHit(modalOverlay, token.event.col, token.event.row)) {
53
+ dismiss();
54
+ return {
55
+ handled: true,
56
+ inputRemainder: parsed.remainder,
57
+ };
58
+ }
59
+ if (onInsidePointerPress?.(token.event.col, token.event.row) === true) {
60
+ return {
61
+ handled: true,
62
+ inputRemainder: parsed.remainder,
63
+ };
64
+ }
65
+ }
66
+ return {
67
+ handled: true,
68
+ inputRemainder: parsed.remainder,
69
+ };
70
+ }
@@ -0,0 +1,187 @@
1
+ import { reduceLinePromptInput } from './modal-input-reducers.ts';
2
+
3
+ interface AddDirectoryPromptState {
4
+ value: string;
5
+ error: string | null;
6
+ }
7
+
8
+ interface RepositoryPromptState {
9
+ readonly mode: 'add' | 'edit';
10
+ readonly repositoryId: string | null;
11
+ readonly value: string;
12
+ readonly error: string | null;
13
+ }
14
+
15
+ interface HandleAddDirectoryPromptInputOptions {
16
+ input: Buffer;
17
+ prompt: AddDirectoryPromptState | null;
18
+ isQuitShortcut: (input: Buffer) => boolean;
19
+ dismissOnOutsideClick: (input: Buffer, dismiss: () => void) => boolean;
20
+ setPrompt: (next: AddDirectoryPromptState | null) => void;
21
+ markDirty: () => void;
22
+ queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
23
+ addDirectoryByPath: (path: string) => Promise<void>;
24
+ }
25
+
26
+ interface HandleRepositoryPromptInputOptions {
27
+ input: Buffer;
28
+ prompt: RepositoryPromptState | null;
29
+ isQuitShortcut: (input: Buffer) => boolean;
30
+ dismissOnOutsideClick: (input: Buffer, dismiss: () => void) => boolean;
31
+ setPrompt: (next: RepositoryPromptState | null) => void;
32
+ markDirty: () => void;
33
+ repositoriesHas: (repositoryId: string) => boolean;
34
+ normalizeGitHubRemoteUrl: (remoteUrl: string) => string | null;
35
+ queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
36
+ upsertRepositoryByRemoteUrl: (remoteUrl: string, existingRepositoryId?: string) => Promise<void>;
37
+ }
38
+
39
+ export function handleAddDirectoryPromptInput(
40
+ options: HandleAddDirectoryPromptInputOptions,
41
+ ): boolean {
42
+ const {
43
+ input,
44
+ prompt,
45
+ isQuitShortcut,
46
+ dismissOnOutsideClick,
47
+ setPrompt,
48
+ markDirty,
49
+ queueControlPlaneOp,
50
+ addDirectoryByPath,
51
+ } = options;
52
+ if (prompt === null) {
53
+ return false;
54
+ }
55
+ if (input.length === 1 && input[0] === 0x03) {
56
+ return false;
57
+ }
58
+ if (isQuitShortcut(input)) {
59
+ setPrompt(null);
60
+ markDirty();
61
+ return true;
62
+ }
63
+ if (
64
+ dismissOnOutsideClick(input, () => {
65
+ setPrompt(null);
66
+ markDirty();
67
+ })
68
+ ) {
69
+ return true;
70
+ }
71
+
72
+ const reduced = reduceLinePromptInput(prompt.value, input);
73
+ const value = reduced.value;
74
+ const submit = reduced.submit;
75
+
76
+ if (!submit) {
77
+ setPrompt({
78
+ value,
79
+ error: null,
80
+ });
81
+ markDirty();
82
+ return true;
83
+ }
84
+
85
+ const trimmed = value.trim();
86
+ if (trimmed.length === 0) {
87
+ setPrompt({
88
+ value,
89
+ error: 'path required',
90
+ });
91
+ markDirty();
92
+ return true;
93
+ }
94
+ setPrompt(null);
95
+ queueControlPlaneOp(async () => {
96
+ await addDirectoryByPath(trimmed);
97
+ }, 'prompt-add-directory');
98
+ markDirty();
99
+ return true;
100
+ }
101
+
102
+ export function handleRepositoryPromptInput(options: HandleRepositoryPromptInputOptions): boolean {
103
+ const {
104
+ input,
105
+ prompt,
106
+ isQuitShortcut,
107
+ dismissOnOutsideClick,
108
+ setPrompt,
109
+ markDirty,
110
+ repositoriesHas,
111
+ normalizeGitHubRemoteUrl,
112
+ queueControlPlaneOp,
113
+ upsertRepositoryByRemoteUrl,
114
+ } = options;
115
+ if (prompt === null) {
116
+ return false;
117
+ }
118
+ if (input.length === 1 && input[0] === 0x03) {
119
+ return false;
120
+ }
121
+ if (isQuitShortcut(input)) {
122
+ setPrompt(null);
123
+ markDirty();
124
+ return true;
125
+ }
126
+ if (
127
+ dismissOnOutsideClick(input, () => {
128
+ setPrompt(null);
129
+ markDirty();
130
+ })
131
+ ) {
132
+ return true;
133
+ }
134
+
135
+ const reduced = reduceLinePromptInput(prompt.value, input);
136
+ const value = reduced.value;
137
+ const submit = reduced.submit;
138
+
139
+ if (!submit) {
140
+ setPrompt({
141
+ ...prompt,
142
+ value,
143
+ error: null,
144
+ });
145
+ markDirty();
146
+ return true;
147
+ }
148
+
149
+ const trimmed = value.trim();
150
+ if (trimmed.length === 0) {
151
+ setPrompt({
152
+ ...prompt,
153
+ value,
154
+ error: 'github url required',
155
+ });
156
+ markDirty();
157
+ return true;
158
+ }
159
+ if (normalizeGitHubRemoteUrl(trimmed) === null) {
160
+ setPrompt({
161
+ ...prompt,
162
+ value,
163
+ error: 'github url required',
164
+ });
165
+ markDirty();
166
+ return true;
167
+ }
168
+
169
+ const mode = prompt.mode;
170
+ const repositoryId = prompt.repositoryId;
171
+ setPrompt(null);
172
+ if (mode === 'edit' && (repositoryId === null || !repositoriesHas(repositoryId))) {
173
+ markDirty();
174
+ return true;
175
+ }
176
+ queueControlPlaneOp(
177
+ async () => {
178
+ await upsertRepositoryByRemoteUrl(
179
+ trimmed,
180
+ mode === 'edit' ? (repositoryId ?? undefined) : undefined,
181
+ );
182
+ },
183
+ mode === 'edit' ? 'prompt-edit-repository' : 'prompt-add-repository',
184
+ );
185
+ markDirty();
186
+ return true;
187
+ }