@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,85 @@
1
+ import type { buildWorkspaceRailViewRows } from '../workspace-rail-model.ts';
2
+
3
+ export type LeftNavSelection =
4
+ | {
5
+ readonly kind: 'home';
6
+ }
7
+ | {
8
+ readonly kind: 'repository';
9
+ readonly repositoryId: string;
10
+ }
11
+ | {
12
+ readonly kind: 'project';
13
+ readonly directoryId: string;
14
+ }
15
+ | {
16
+ readonly kind: 'conversation';
17
+ readonly sessionId: string;
18
+ };
19
+
20
+ export function leftNavTargetKey(target: LeftNavSelection): string {
21
+ if (target.kind === 'home') {
22
+ return 'home';
23
+ }
24
+ if (target.kind === 'repository') {
25
+ return `repository:${target.repositoryId}`;
26
+ }
27
+ if (target.kind === 'project') {
28
+ return `directory:${target.directoryId}`;
29
+ }
30
+ return `conversation:${target.sessionId}`;
31
+ }
32
+
33
+ function leftNavTargetFromRow(
34
+ rows: ReturnType<typeof buildWorkspaceRailViewRows>,
35
+ rowIndex: number,
36
+ ): LeftNavSelection | null {
37
+ const row = rows[rowIndex];
38
+ if (row === undefined) {
39
+ return null;
40
+ }
41
+ if (row.railAction === 'home.open') {
42
+ return {
43
+ kind: 'home',
44
+ };
45
+ }
46
+ if (row.kind === 'repository-header' && row.repositoryId !== null) {
47
+ return {
48
+ kind: 'repository',
49
+ repositoryId: row.repositoryId,
50
+ };
51
+ }
52
+ if (row.kind === 'dir-header' && row.directoryKey !== null) {
53
+ return {
54
+ kind: 'project',
55
+ directoryId: row.directoryKey,
56
+ };
57
+ }
58
+ if (row.kind === 'conversation-title' && row.conversationSessionId !== null) {
59
+ return {
60
+ kind: 'conversation',
61
+ sessionId: row.conversationSessionId,
62
+ };
63
+ }
64
+ return null;
65
+ }
66
+
67
+ export function visibleLeftNavTargets(
68
+ rows: ReturnType<typeof buildWorkspaceRailViewRows>,
69
+ ): readonly LeftNavSelection[] {
70
+ const entries: LeftNavSelection[] = [];
71
+ const seen = new Set<string>();
72
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
73
+ const target = leftNavTargetFromRow(rows, rowIndex);
74
+ if (target === null) {
75
+ continue;
76
+ }
77
+ const key = leftNavTargetKey(target);
78
+ if (seen.has(key)) {
79
+ continue;
80
+ }
81
+ seen.add(key);
82
+ entries.push(target);
83
+ }
84
+ return entries;
85
+ }
@@ -0,0 +1,118 @@
1
+ interface HandleLeftRailActionClickOptions {
2
+ action: string | null;
3
+ selectedProjectId: string | null;
4
+ selectedRepositoryId: string | null;
5
+ activeConversationId: string | null;
6
+ repositoriesCollapsed: boolean;
7
+ clearConversationTitleEditClickState: () => void;
8
+ resolveDirectoryForAction: () => string | null;
9
+ openNewThreadPrompt: (directoryId: string) => void;
10
+ queueArchiveConversation: (conversationId: string) => void;
11
+ openAddDirectoryPrompt: () => void;
12
+ openRepositoryPromptForCreate: () => void;
13
+ repositoryExists: (repositoryId: string) => boolean;
14
+ openRepositoryPromptForEdit: (repositoryId: string) => void;
15
+ queueArchiveRepository: (repositoryId: string) => void;
16
+ toggleRepositoryGroup: (repositoryId: string) => void;
17
+ selectLeftNavRepository: (repositoryId: string) => void;
18
+ expandAllRepositoryGroups: () => void;
19
+ collapseAllRepositoryGroups: () => void;
20
+ enterHomePane: () => void;
21
+ queueCloseDirectory: (directoryId: string) => void;
22
+ toggleShortcutsCollapsed: () => void;
23
+ markDirty: () => void;
24
+ }
25
+
26
+ export function handleLeftRailActionClick(options: HandleLeftRailActionClickOptions): boolean {
27
+ if (options.action === 'conversation.new') {
28
+ options.clearConversationTitleEditClickState();
29
+ const targetDirectoryId = options.selectedProjectId ?? options.resolveDirectoryForAction();
30
+ if (targetDirectoryId !== null) {
31
+ options.openNewThreadPrompt(targetDirectoryId);
32
+ }
33
+ options.markDirty();
34
+ return true;
35
+ }
36
+ if (options.action === 'conversation.delete') {
37
+ options.clearConversationTitleEditClickState();
38
+ if (options.activeConversationId !== null) {
39
+ options.queueArchiveConversation(options.activeConversationId);
40
+ }
41
+ options.markDirty();
42
+ return true;
43
+ }
44
+ if (options.action === 'project.add') {
45
+ options.clearConversationTitleEditClickState();
46
+ options.openAddDirectoryPrompt();
47
+ options.markDirty();
48
+ return true;
49
+ }
50
+ if (options.action === 'repository.add') {
51
+ options.clearConversationTitleEditClickState();
52
+ options.openRepositoryPromptForCreate();
53
+ return true;
54
+ }
55
+ if (options.action === 'repository.edit') {
56
+ options.clearConversationTitleEditClickState();
57
+ if (
58
+ options.selectedRepositoryId !== null &&
59
+ options.repositoryExists(options.selectedRepositoryId)
60
+ ) {
61
+ options.openRepositoryPromptForEdit(options.selectedRepositoryId);
62
+ }
63
+ options.markDirty();
64
+ return true;
65
+ }
66
+ if (options.action === 'repository.archive') {
67
+ options.clearConversationTitleEditClickState();
68
+ if (
69
+ options.selectedRepositoryId !== null &&
70
+ options.repositoryExists(options.selectedRepositoryId)
71
+ ) {
72
+ options.queueArchiveRepository(options.selectedRepositoryId);
73
+ }
74
+ options.markDirty();
75
+ return true;
76
+ }
77
+ if (options.action === 'repository.toggle') {
78
+ options.clearConversationTitleEditClickState();
79
+ if (options.selectedRepositoryId !== null) {
80
+ options.toggleRepositoryGroup(options.selectedRepositoryId);
81
+ options.selectLeftNavRepository(options.selectedRepositoryId);
82
+ }
83
+ options.markDirty();
84
+ return true;
85
+ }
86
+ if (options.action === 'repositories.toggle') {
87
+ options.clearConversationTitleEditClickState();
88
+ if (options.repositoriesCollapsed) {
89
+ options.expandAllRepositoryGroups();
90
+ } else {
91
+ options.collapseAllRepositoryGroups();
92
+ }
93
+ options.markDirty();
94
+ return true;
95
+ }
96
+ if (options.action === 'home.open') {
97
+ options.clearConversationTitleEditClickState();
98
+ options.enterHomePane();
99
+ options.markDirty();
100
+ return true;
101
+ }
102
+ if (options.action === 'project.close') {
103
+ options.clearConversationTitleEditClickState();
104
+ const targetDirectoryId = options.selectedProjectId ?? options.resolveDirectoryForAction();
105
+ if (targetDirectoryId !== null) {
106
+ options.queueCloseDirectory(targetDirectoryId);
107
+ }
108
+ options.markDirty();
109
+ return true;
110
+ }
111
+ if (options.action === 'shortcuts.toggle') {
112
+ options.clearConversationTitleEditClickState();
113
+ options.toggleShortcutsCollapsed();
114
+ options.markDirty();
115
+ return true;
116
+ }
117
+ return false;
118
+ }
@@ -0,0 +1,82 @@
1
+ import { detectConversationDoubleClick } from '../double-click.ts';
2
+
3
+ interface ConversationTitleEditClickState {
4
+ readonly conversationId: string;
5
+ readonly atMs: number;
6
+ }
7
+
8
+ interface HandleLeftRailConversationClickOptions {
9
+ selectedConversationId: string | null;
10
+ selectedProjectId: string | null;
11
+ supportsConversationTitleEditClick: boolean;
12
+ previousClickState: ConversationTitleEditClickState | null;
13
+ nowMs: number;
14
+ conversationTitleEditDoubleClickWindowMs: number;
15
+ activeConversationId: string | null;
16
+ isConversationPaneActive: boolean;
17
+ setConversationClickState: (next: ConversationTitleEditClickState | null) => void;
18
+ ensureConversationPaneActive: (conversationId: string) => void;
19
+ beginConversationTitleEdit: (conversationId: string) => void;
20
+ queueActivateConversation: (conversationId: string) => void;
21
+ queueActivateConversationAndEdit: (conversationId: string) => void;
22
+ directoriesHas: (directoryId: string) => boolean;
23
+ enterProjectPane: (directoryId: string) => void;
24
+ markDirty: () => void;
25
+ }
26
+
27
+ export function handleLeftRailConversationClick(
28
+ options: HandleLeftRailConversationClickOptions,
29
+ ): boolean {
30
+ const conversationClick =
31
+ options.selectedConversationId !== null && options.supportsConversationTitleEditClick
32
+ ? detectConversationDoubleClick(
33
+ options.previousClickState,
34
+ options.selectedConversationId,
35
+ options.nowMs,
36
+ options.conversationTitleEditDoubleClickWindowMs,
37
+ )
38
+ : {
39
+ doubleClick: false,
40
+ nextState: null,
41
+ };
42
+ options.setConversationClickState(conversationClick.nextState);
43
+
44
+ if (
45
+ options.selectedConversationId !== null &&
46
+ options.selectedConversationId === options.activeConversationId
47
+ ) {
48
+ if (!options.isConversationPaneActive) {
49
+ options.ensureConversationPaneActive(options.selectedConversationId);
50
+ }
51
+ if (conversationClick.doubleClick) {
52
+ options.beginConversationTitleEdit(options.selectedConversationId);
53
+ }
54
+ options.markDirty();
55
+ return true;
56
+ }
57
+
58
+ if (options.selectedConversationId !== null) {
59
+ if (conversationClick.doubleClick) {
60
+ options.queueActivateConversationAndEdit(options.selectedConversationId);
61
+ } else {
62
+ options.queueActivateConversation(options.selectedConversationId);
63
+ }
64
+ options.markDirty();
65
+ return true;
66
+ }
67
+
68
+ if (
69
+ options.selectedConversationId === null &&
70
+ options.selectedProjectId !== null &&
71
+ options.directoriesHas(options.selectedProjectId)
72
+ ) {
73
+ options.setConversationClickState(null);
74
+ options.enterProjectPane(options.selectedProjectId);
75
+ options.markDirty();
76
+ return true;
77
+ }
78
+
79
+ options.setConversationClickState(null);
80
+ options.markDirty();
81
+ return true;
82
+ }
@@ -0,0 +1,74 @@
1
+ import {
2
+ actionAtWorkspaceRailCell,
3
+ conversationIdAtWorkspaceRailRow,
4
+ kindAtWorkspaceRailRow,
5
+ projectIdAtWorkspaceRailRow,
6
+ repositoryIdAtWorkspaceRailRow,
7
+ } from '../workspace-rail-model.ts';
8
+ import type { buildWorkspaceRailViewRows } from '../workspace-rail-model.ts';
9
+
10
+ export interface LeftRailPointerContext {
11
+ readonly selectedConversationId: string | null;
12
+ readonly selectedProjectId: string | null;
13
+ readonly selectedRepositoryId: string | null;
14
+ readonly selectedAction: string | null;
15
+ readonly supportsConversationTitleEditClick: boolean;
16
+ }
17
+
18
+ interface HandleLeftRailPointerClickOptions {
19
+ clickEligible: boolean;
20
+ rows: ReturnType<typeof buildWorkspaceRailViewRows>;
21
+ paneRows: number;
22
+ leftCols: number;
23
+ pointerRow: number;
24
+ pointerCol: number;
25
+ hasConversationTitleEdit: boolean;
26
+ conversationTitleEditConversationId: string | null;
27
+ stopConversationTitleEdit: () => void;
28
+ hasSelection: boolean;
29
+ clearSelection: () => void;
30
+ handleAction: (context: LeftRailPointerContext) => boolean;
31
+ handleConversation: (context: LeftRailPointerContext) => void;
32
+ }
33
+
34
+ export function handleLeftRailPointerClick(options: HandleLeftRailPointerClickOptions): boolean {
35
+ if (!options.clickEligible) {
36
+ return false;
37
+ }
38
+ const rowIndex = Math.max(0, Math.min(options.paneRows - 1, options.pointerRow - 1));
39
+ const colIndex = Math.max(0, Math.min(options.leftCols - 1, options.pointerCol - 1));
40
+ const selectedConversationId = conversationIdAtWorkspaceRailRow(options.rows, rowIndex);
41
+ const selectedProjectId = projectIdAtWorkspaceRailRow(options.rows, rowIndex);
42
+ const selectedRepositoryId = repositoryIdAtWorkspaceRailRow(options.rows, rowIndex);
43
+ const selectedAction = actionAtWorkspaceRailCell(
44
+ options.rows,
45
+ rowIndex,
46
+ colIndex,
47
+ options.leftCols,
48
+ );
49
+ const selectedRowKind = kindAtWorkspaceRailRow(options.rows, rowIndex);
50
+ const supportsConversationTitleEditClick =
51
+ selectedRowKind === 'conversation-title' || selectedRowKind === 'conversation-body';
52
+ const keepTitleEditActive =
53
+ options.hasConversationTitleEdit &&
54
+ selectedConversationId === options.conversationTitleEditConversationId &&
55
+ supportsConversationTitleEditClick;
56
+ if (!keepTitleEditActive && options.hasConversationTitleEdit) {
57
+ options.stopConversationTitleEdit();
58
+ }
59
+ if (options.hasSelection) {
60
+ options.clearSelection();
61
+ }
62
+ const context: LeftRailPointerContext = {
63
+ selectedConversationId,
64
+ selectedProjectId,
65
+ selectedRepositoryId,
66
+ selectedAction,
67
+ supportsConversationTitleEditClick,
68
+ };
69
+ if (options.handleAction(context)) {
70
+ return true;
71
+ }
72
+ options.handleConversation(context);
73
+ return true;
74
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ clampCommandMenuState,
3
+ COMMAND_MENU_MAX_RESULTS,
4
+ reduceCommandMenuInput,
5
+ resolveCommandMenuMatches,
6
+ type CommandMenuActionDescriptor,
7
+ type CommandMenuState,
8
+ } from './command-menu.ts';
9
+
10
+ interface HandleCommandMenuInputOptions {
11
+ readonly input: Buffer;
12
+ readonly menu: CommandMenuState | null;
13
+ readonly isQuitShortcut: (input: Buffer) => boolean;
14
+ readonly isToggleShortcut: (input: Buffer) => boolean;
15
+ readonly dismissOnOutsideClick: (
16
+ input: Buffer,
17
+ dismiss: () => void,
18
+ onInsidePointerPress?: (col: number, row: number) => boolean,
19
+ ) => boolean;
20
+ readonly buildCommandMenuModalOverlay: () => { top: number } | null;
21
+ readonly resolveActions: () => readonly CommandMenuActionDescriptor[];
22
+ readonly executeAction: (actionId: string) => void;
23
+ readonly setMenu: (next: CommandMenuState | null) => void;
24
+ readonly markDirty: () => void;
25
+ }
26
+
27
+ export function handleCommandMenuInput(options: HandleCommandMenuInputOptions): boolean {
28
+ const {
29
+ input,
30
+ menu,
31
+ isQuitShortcut,
32
+ isToggleShortcut,
33
+ dismissOnOutsideClick,
34
+ buildCommandMenuModalOverlay,
35
+ resolveActions,
36
+ executeAction,
37
+ setMenu,
38
+ markDirty,
39
+ } = options;
40
+ if (menu === null) {
41
+ return false;
42
+ }
43
+ if (input.length === 1 && input[0] === 0x03) {
44
+ return false;
45
+ }
46
+ if (isQuitShortcut(input)) {
47
+ setMenu(null);
48
+ markDirty();
49
+ return true;
50
+ }
51
+ if (isToggleShortcut(input)) {
52
+ setMenu(null);
53
+ markDirty();
54
+ return true;
55
+ }
56
+ const maybeMouseSequence = input.includes(0x3c);
57
+ if (
58
+ maybeMouseSequence &&
59
+ dismissOnOutsideClick(
60
+ input,
61
+ () => {
62
+ setMenu(null);
63
+ markDirty();
64
+ },
65
+ (_col, _row) => {
66
+ return buildCommandMenuModalOverlay() !== null;
67
+ },
68
+ )
69
+ ) {
70
+ return true;
71
+ }
72
+
73
+ const currentMatches = resolveCommandMenuMatches(
74
+ resolveActions(),
75
+ menu.query,
76
+ COMMAND_MENU_MAX_RESULTS,
77
+ );
78
+ const reduction = reduceCommandMenuInput(menu, input, currentMatches.length);
79
+ if (reduction.submit) {
80
+ const clamped = clampCommandMenuState(menu, currentMatches.length);
81
+ const selected = currentMatches[clamped.selectedIndex];
82
+ setMenu(null);
83
+ if (selected !== undefined) {
84
+ executeAction(selected.action.id);
85
+ }
86
+ markDirty();
87
+ return true;
88
+ }
89
+
90
+ const nextMatches = resolveCommandMenuMatches(
91
+ resolveActions(),
92
+ reduction.nextState.query,
93
+ COMMAND_MENU_MAX_RESULTS,
94
+ );
95
+ const nextState = clampCommandMenuState(reduction.nextState, nextMatches.length);
96
+ if (nextState.query !== menu.query || nextState.selectedIndex !== menu.selectedIndex) {
97
+ setMenu(nextState);
98
+ markDirty();
99
+ }
100
+ return true;
101
+ }
@@ -0,0 +1,217 @@
1
+ import {
2
+ type createNewThreadPromptState,
3
+ reduceNewThreadPromptInput,
4
+ type normalizeThreadAgentType,
5
+ } from '../new-thread-prompt.ts';
6
+ import { reduceLinePromptInput } from './modal-input-reducers.ts';
7
+
8
+ type NewThreadPromptState = ReturnType<typeof createNewThreadPromptState>;
9
+ type ThreadAgentType = ReturnType<typeof normalizeThreadAgentType>;
10
+
11
+ interface ConversationTitleEditState {
12
+ conversationId: string;
13
+ value: string;
14
+ lastSavedValue: string;
15
+ error: string | null;
16
+ persistInFlight: boolean;
17
+ debounceTimer: NodeJS.Timeout | null;
18
+ }
19
+
20
+ interface HandleConversationTitleEditInputOptions {
21
+ input: Buffer;
22
+ edit: ConversationTitleEditState | null;
23
+ isQuitShortcut: (input: Buffer) => boolean;
24
+ isArchiveShortcut: (input: Buffer) => boolean;
25
+ dismissOnOutsideClick: (
26
+ input: Buffer,
27
+ dismiss: () => void,
28
+ onInsidePointerPress?: (col: number, row: number) => boolean,
29
+ ) => boolean;
30
+ buildConversationTitleModalOverlay: () => { top: number } | null;
31
+ stopConversationTitleEdit: (persistPending: boolean) => void;
32
+ queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
33
+ archiveConversation: (sessionId: string) => Promise<void>;
34
+ markDirty: () => void;
35
+ conversations: ReadonlyMap<string, { title: string }>;
36
+ scheduleConversationTitlePersist: () => void;
37
+ }
38
+
39
+ interface HandleNewThreadPromptInputOptions {
40
+ input: Buffer;
41
+ prompt: NewThreadPromptState | null;
42
+ isQuitShortcut: (input: Buffer) => boolean;
43
+ dismissOnOutsideClick: (
44
+ input: Buffer,
45
+ dismiss: () => void,
46
+ onInsidePointerPress?: (col: number, row: number) => boolean,
47
+ ) => boolean;
48
+ buildNewThreadModalOverlay: () => { top: number } | null;
49
+ resolveNewThreadPromptAgentByRow: (overlayTop: number, row: number) => ThreadAgentType | null;
50
+ queueControlPlaneOp: (task: () => Promise<void>, label: string) => void;
51
+ createAndActivateConversationInDirectory: (
52
+ directoryId: string,
53
+ agentType: ThreadAgentType,
54
+ ) => Promise<void>;
55
+ markDirty: () => void;
56
+ setPrompt: (prompt: NewThreadPromptState | null) => void;
57
+ }
58
+
59
+ export function handleConversationTitleEditInput(
60
+ options: HandleConversationTitleEditInputOptions,
61
+ ): boolean {
62
+ const {
63
+ input,
64
+ edit,
65
+ isQuitShortcut,
66
+ isArchiveShortcut,
67
+ dismissOnOutsideClick,
68
+ buildConversationTitleModalOverlay,
69
+ stopConversationTitleEdit,
70
+ queueControlPlaneOp,
71
+ archiveConversation,
72
+ markDirty,
73
+ conversations,
74
+ scheduleConversationTitlePersist,
75
+ } = options;
76
+ if (edit === null) {
77
+ return false;
78
+ }
79
+ if (input.length === 1 && input[0] === 0x03) {
80
+ return false;
81
+ }
82
+ if (isQuitShortcut(input)) {
83
+ stopConversationTitleEdit(true);
84
+ return true;
85
+ }
86
+ if (isArchiveShortcut(input)) {
87
+ const targetConversationId = edit.conversationId;
88
+ stopConversationTitleEdit(true);
89
+ queueControlPlaneOp(async () => {
90
+ await archiveConversation(targetConversationId);
91
+ }, 'modal-archive-conversation');
92
+ markDirty();
93
+ return true;
94
+ }
95
+ if (
96
+ dismissOnOutsideClick(
97
+ input,
98
+ () => {
99
+ stopConversationTitleEdit(true);
100
+ },
101
+ (_col, row) => {
102
+ const overlay = buildConversationTitleModalOverlay();
103
+ if (overlay === null) {
104
+ return false;
105
+ }
106
+ const archiveButtonRow = overlay.top + 5;
107
+ if (row - 1 !== archiveButtonRow) {
108
+ return false;
109
+ }
110
+ const targetConversationId = edit.conversationId;
111
+ stopConversationTitleEdit(true);
112
+ queueControlPlaneOp(async () => {
113
+ await archiveConversation(targetConversationId);
114
+ }, 'modal-archive-conversation-click');
115
+ markDirty();
116
+ return true;
117
+ },
118
+ )
119
+ ) {
120
+ return true;
121
+ }
122
+
123
+ const reduced = reduceLinePromptInput(edit.value, input);
124
+ const nextValue = reduced.value;
125
+ const done = reduced.submit;
126
+
127
+ if (nextValue !== edit.value) {
128
+ edit.value = nextValue;
129
+ edit.error = null;
130
+ const conversation = conversations.get(edit.conversationId);
131
+ if (conversation !== undefined) {
132
+ conversation.title = nextValue;
133
+ }
134
+ scheduleConversationTitlePersist();
135
+ markDirty();
136
+ }
137
+
138
+ if (done) {
139
+ stopConversationTitleEdit(true);
140
+ }
141
+ return true;
142
+ }
143
+
144
+ export function handleNewThreadPromptInput(options: HandleNewThreadPromptInputOptions): boolean {
145
+ const {
146
+ input,
147
+ prompt,
148
+ isQuitShortcut,
149
+ dismissOnOutsideClick,
150
+ buildNewThreadModalOverlay,
151
+ resolveNewThreadPromptAgentByRow,
152
+ queueControlPlaneOp,
153
+ createAndActivateConversationInDirectory,
154
+ markDirty,
155
+ setPrompt,
156
+ } = options;
157
+ if (prompt === null) {
158
+ return false;
159
+ }
160
+ if (input.length === 1 && input[0] === 0x03) {
161
+ return false;
162
+ }
163
+ if (isQuitShortcut(input)) {
164
+ setPrompt(null);
165
+ markDirty();
166
+ return true;
167
+ }
168
+ const maybeMouseSequence = input.includes(0x3c);
169
+ if (
170
+ maybeMouseSequence &&
171
+ dismissOnOutsideClick(
172
+ input,
173
+ () => {
174
+ setPrompt(null);
175
+ markDirty();
176
+ },
177
+ (_col, row) => {
178
+ const overlay = buildNewThreadModalOverlay();
179
+ if (overlay === null) {
180
+ return false;
181
+ }
182
+ const selectedAgentType = resolveNewThreadPromptAgentByRow(overlay.top, row);
183
+ if (selectedAgentType === null) {
184
+ return false;
185
+ }
186
+ const targetDirectoryId = prompt.directoryId;
187
+ setPrompt(null);
188
+ queueControlPlaneOp(async () => {
189
+ await createAndActivateConversationInDirectory(targetDirectoryId, selectedAgentType);
190
+ }, `modal-new-thread-click:${selectedAgentType}`);
191
+ markDirty();
192
+ return true;
193
+ },
194
+ )
195
+ ) {
196
+ return true;
197
+ }
198
+
199
+ const reduction = reduceNewThreadPromptInput(prompt, input);
200
+ const changed = reduction.nextState.selectedAgentType !== prompt.selectedAgentType;
201
+
202
+ if (changed) {
203
+ setPrompt(reduction.nextState);
204
+ markDirty();
205
+ }
206
+ if (reduction.submit) {
207
+ const targetDirectoryId = prompt.directoryId;
208
+ const selectedAgentType = reduction.nextState.selectedAgentType;
209
+ setPrompt(null);
210
+ queueControlPlaneOp(async () => {
211
+ await createAndActivateConversationInDirectory(targetDirectoryId, selectedAgentType);
212
+ }, `modal-new-thread:${selectedAgentType}`);
213
+ markDirty();
214
+ return true;
215
+ }
216
+ return true;
217
+ }