@jmoyers/harness 0.1.11 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/README.md +31 -39
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/packages/harness-ui/src/modal-manager.ts +222 -0
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -1,25 +1,47 @@
1
1
  import type { WorkspaceModel } from '../domain/workspace.ts';
2
2
  import {
3
- createTaskComposerBuffer as createTaskComposerBufferFrame,
4
- normalizeTaskComposerBuffer as normalizeTaskComposerBufferFrame,
5
- taskFieldsFromComposerText as taskFieldsFromComposerTextFrame,
3
+ createTaskComposerBuffer,
4
+ insertTaskComposerText,
5
+ normalizeTaskComposerBuffer,
6
+ taskComposerBackspace,
7
+ taskComposerDeleteForward,
8
+ taskComposerDeleteToLineEnd,
9
+ taskComposerDeleteToLineStart,
10
+ taskComposerDeleteWordLeft,
11
+ taskComposerMoveLeft,
12
+ taskComposerMoveLineEnd,
13
+ taskComposerMoveLineStart,
14
+ taskComposerMoveRight,
15
+ taskComposerMoveVertical,
16
+ taskComposerMoveWordLeft,
17
+ taskComposerMoveWordRight,
18
+ taskFieldsFromComposerText,
6
19
  type TaskComposerBuffer,
7
20
  } from '../mux/task-composer.ts';
8
- import { handleTaskPaneShortcutInput as handleTaskPaneShortcutInputFrame } from '../mux/live-mux/task-pane-shortcuts.ts';
9
- import type { ResolvedTaskScreenKeybindings } from '../mux/task-screen-keybindings.ts';
21
+ import {
22
+ detectTaskScreenKeybindingAction,
23
+ type ResolvedTaskScreenKeybindings,
24
+ } from '../mux/task-screen-keybindings.ts';
25
+
26
+ type TaskPaneShortcutAction =
27
+ | 'task.ready'
28
+ | 'task.draft'
29
+ | 'task.complete'
30
+ | 'task.reorder-up'
31
+ | 'task.reorder-down';
32
+ type TaskComposerSubmitMode = 'ready' | 'queue';
10
33
 
11
- type TaskPaneShortcutAction = Parameters<
12
- Parameters<typeof handleTaskPaneShortcutInputFrame>[0]['runTaskPaneAction']
13
- >[0];
34
+ const BRACKETED_PASTE_START = Buffer.from('\u001b[200~', 'utf8');
35
+ const BRACKETED_PASTE_END = Buffer.from('\u001b[201~', 'utf8');
14
36
 
15
37
  interface TaskRecordShape {
16
38
  readonly taskId: string;
17
39
  readonly repositoryId: string | null;
18
40
  readonly title: string;
19
- readonly description: string;
41
+ readonly body: string;
20
42
  }
21
43
 
22
- interface RuntimeTaskPaneShortcutsOptions<TTaskRecord extends TaskRecordShape> {
44
+ export interface RuntimeTaskPaneShortcutsOptions<TTaskRecord extends TaskRecordShape> {
23
45
  readonly workspace: WorkspaceModel;
24
46
  readonly taskScreenKeybindings: ResolvedTaskScreenKeybindings;
25
47
  readonly repositoriesHas: (repositoryId: string) => boolean;
@@ -36,154 +58,332 @@ interface RuntimeTaskPaneShortcutsOptions<TTaskRecord extends TaskRecordShape> {
36
58
  readonly createTask: (payload: {
37
59
  repositoryId: string;
38
60
  title: string;
39
- description: string;
61
+ body: string;
40
62
  }) => Promise<TTaskRecord>;
63
+ readonly taskReady: (taskId: string) => Promise<TTaskRecord>;
41
64
  readonly applyTaskRecord: (task: TTaskRecord) => void;
42
65
  readonly syncTaskPaneSelection: () => void;
43
66
  readonly markDirty: () => void;
44
- readonly createTaskComposerBuffer?: typeof createTaskComposerBufferFrame;
45
- readonly normalizeTaskComposerBuffer?: typeof normalizeTaskComposerBufferFrame;
46
- readonly taskFieldsFromComposerText?: typeof taskFieldsFromComposerTextFrame;
47
- readonly handleTaskPaneShortcutInput?: typeof handleTaskPaneShortcutInputFrame;
48
67
  }
49
68
 
50
- export class RuntimeTaskPaneShortcuts<TTaskRecord extends TaskRecordShape> {
51
- private readonly createTaskComposerBuffer: typeof createTaskComposerBufferFrame;
52
- private readonly normalizeTaskComposerBuffer: typeof normalizeTaskComposerBufferFrame;
53
- private readonly taskFieldsFromComposerText: typeof taskFieldsFromComposerTextFrame;
54
- private readonly handleTaskPaneShortcutInput: typeof handleTaskPaneShortcutInputFrame;
55
-
56
- constructor(private readonly options: RuntimeTaskPaneShortcutsOptions<TTaskRecord>) {
57
- this.createTaskComposerBuffer =
58
- options.createTaskComposerBuffer ?? createTaskComposerBufferFrame;
59
- this.normalizeTaskComposerBuffer =
60
- options.normalizeTaskComposerBuffer ?? normalizeTaskComposerBufferFrame;
61
- this.taskFieldsFromComposerText =
62
- options.taskFieldsFromComposerText ?? taskFieldsFromComposerTextFrame;
63
- this.handleTaskPaneShortcutInput =
64
- options.handleTaskPaneShortcutInput ?? handleTaskPaneShortcutInputFrame;
65
- }
66
-
67
- homeEditorBuffer(): TaskComposerBuffer {
68
- const taskEditorTarget = this.options.workspace.taskEditorTarget;
69
+ export interface RuntimeTaskPaneShortcuts {
70
+ homeEditorBuffer(): TaskComposerBuffer;
71
+ updateHomeEditorBuffer(next: TaskComposerBuffer): void;
72
+ selectRepositoryByDirection(direction: 1 | -1): void;
73
+ submitDraftTaskFromComposer(mode: TaskComposerSubmitMode): void;
74
+ moveTaskEditorFocusUp(): void;
75
+ moveTaskEditorFocusDown(): void;
76
+ handleInput(input: Buffer): boolean;
77
+ }
78
+
79
+ export function createRuntimeTaskPaneShortcuts<TTaskRecord extends TaskRecordShape>(
80
+ options: RuntimeTaskPaneShortcutsOptions<TTaskRecord>,
81
+ ): RuntimeTaskPaneShortcuts {
82
+ const homeEditorBuffer = (): TaskComposerBuffer => {
83
+ const taskEditorTarget = options.workspace.taskEditorTarget;
69
84
  if (taskEditorTarget.kind === 'task') {
70
- return (
71
- this.options.taskComposerForTask(taskEditorTarget.taskId) ??
72
- this.createTaskComposerBuffer('')
73
- );
85
+ return options.taskComposerForTask(taskEditorTarget.taskId) ?? createTaskComposerBuffer('');
74
86
  }
75
- return this.options.workspace.taskDraftComposer;
76
- }
87
+ return options.workspace.taskDraftComposer;
88
+ };
77
89
 
78
- updateHomeEditorBuffer(next: TaskComposerBuffer): void {
79
- const taskEditorTarget = this.options.workspace.taskEditorTarget;
90
+ const updateHomeEditorBuffer = (next: TaskComposerBuffer): void => {
91
+ const taskEditorTarget = options.workspace.taskEditorTarget;
80
92
  if (taskEditorTarget.kind === 'task') {
81
- this.options.setTaskComposerForTask(taskEditorTarget.taskId, next);
82
- this.options.scheduleTaskComposerPersist(taskEditorTarget.taskId);
93
+ options.setTaskComposerForTask(taskEditorTarget.taskId, next);
94
+ options.scheduleTaskComposerPersist(taskEditorTarget.taskId);
83
95
  } else {
84
- this.options.workspace.taskDraftComposer = this.normalizeTaskComposerBuffer(next);
96
+ options.workspace.taskDraftComposer = normalizeTaskComposerBuffer(next);
85
97
  }
86
- this.options.markDirty();
87
- }
98
+ options.markDirty();
99
+ };
88
100
 
89
- selectRepositoryByDirection(direction: 1 | -1): void {
90
- const orderedIds = this.options.activeRepositoryIds();
101
+ const selectRepositoryByDirection = (direction: 1 | -1): void => {
102
+ const orderedIds = options.activeRepositoryIds();
91
103
  if (orderedIds.length === 0) {
92
104
  return;
93
105
  }
94
106
  const currentIndex = Math.max(
95
107
  0,
96
- orderedIds.indexOf(this.options.workspace.taskPaneSelectedRepositoryId ?? ''),
108
+ orderedIds.indexOf(options.workspace.taskPaneSelectedRepositoryId ?? ''),
97
109
  );
98
110
  const nextIndex = Math.max(0, Math.min(orderedIds.length - 1, currentIndex + direction));
99
111
  const nextRepositoryId = orderedIds[nextIndex];
100
112
  if (nextRepositoryId !== undefined) {
101
- this.options.selectRepositoryById(nextRepositoryId);
113
+ options.selectRepositoryById(nextRepositoryId);
102
114
  }
103
- }
115
+ };
104
116
 
105
- submitDraftTaskFromComposer(): void {
106
- const repositoryId = this.options.workspace.taskPaneSelectedRepositoryId;
107
- if (repositoryId === null || !this.options.repositoriesHas(repositoryId)) {
108
- this.options.workspace.taskPaneNotice = 'select a repository first';
109
- this.options.markDirty();
117
+ const taskNavigationIds = (): readonly string[] => {
118
+ const ids: string[] = [];
119
+ const seen = new Set<string>();
120
+ for (const taskId of options.workspace.latestTaskPaneView.taskIds) {
121
+ if (taskId === null || seen.has(taskId)) {
122
+ continue;
123
+ }
124
+ ids.push(taskId);
125
+ seen.add(taskId);
126
+ }
127
+ if (ids.length > 0) {
128
+ return ids;
129
+ }
130
+ for (const task of options.selectedRepositoryTaskRecords()) {
131
+ if (seen.has(task.taskId)) {
132
+ continue;
133
+ }
134
+ ids.push(task.taskId);
135
+ seen.add(task.taskId);
136
+ }
137
+ return ids;
138
+ };
139
+
140
+ const submitDraftTaskFromComposer = (mode: TaskComposerSubmitMode): void => {
141
+ const repositoryId = options.workspace.taskPaneSelectedRepositoryId;
142
+ if (repositoryId === null || !options.repositoriesHas(repositoryId)) {
143
+ options.workspace.taskPaneNotice = 'select a repository first';
144
+ options.markDirty();
110
145
  return;
111
146
  }
112
- const fields = this.taskFieldsFromComposerText(this.options.workspace.taskDraftComposer.text);
113
- if (fields.title.length === 0) {
114
- this.options.workspace.taskPaneNotice = 'first line is required';
115
- this.options.markDirty();
147
+ const fields = taskFieldsFromComposerText(options.workspace.taskDraftComposer.text);
148
+ if (fields.body.trim().length === 0) {
149
+ options.workspace.taskPaneNotice = 'task body is required';
150
+ options.markDirty();
116
151
  return;
117
152
  }
118
- this.options.queueControlPlaneOp(async () => {
119
- const task = await this.options.createTask({
120
- repositoryId,
121
- title: fields.title,
122
- description: fields.description,
123
- });
124
- this.options.applyTaskRecord(task);
125
- this.options.workspace.taskDraftComposer = this.createTaskComposerBuffer('');
126
- this.options.workspace.taskPaneNotice = null;
127
- this.options.syncTaskPaneSelection();
128
- this.options.markDirty();
129
- }, 'task-composer-create');
130
- }
131
-
132
- moveTaskEditorFocusUp(): void {
133
- const workspace = this.options.workspace;
134
- const scopedTasks = this.options.selectedRepositoryTaskRecords();
153
+ options.queueControlPlaneOp(
154
+ async () => {
155
+ const created = await options.createTask({
156
+ repositoryId,
157
+ title: fields.title ?? '',
158
+ body: fields.body,
159
+ });
160
+ const task = mode === 'ready' ? await options.taskReady(created.taskId) : created;
161
+ options.applyTaskRecord(task);
162
+ options.workspace.taskDraftComposer = createTaskComposerBuffer('');
163
+ options.workspace.taskPaneNotice = null;
164
+ options.syncTaskPaneSelection();
165
+ options.markDirty();
166
+ },
167
+ mode === 'ready' ? 'task-composer-submit-ready' : 'task-composer-queue',
168
+ );
169
+ };
170
+
171
+ const moveTaskEditorFocusUp = (): void => {
172
+ const workspace = options.workspace;
173
+ const navigationTaskIds = taskNavigationIds();
135
174
  if (workspace.taskEditorTarget.kind === 'draft') {
136
- const fallback = scopedTasks[scopedTasks.length - 1];
137
- if (fallback !== undefined) {
138
- this.options.focusTaskComposer(fallback.taskId);
175
+ const fallbackTaskId = navigationTaskIds[navigationTaskIds.length - 1];
176
+ if (fallbackTaskId !== undefined) {
177
+ options.focusTaskComposer(fallbackTaskId);
139
178
  }
140
179
  return;
141
180
  }
142
181
 
143
182
  const focusedTaskId = workspace.taskEditorTarget.taskId;
144
- const index = scopedTasks.findIndex((task) => task.taskId === focusedTaskId);
183
+ const index = navigationTaskIds.indexOf(focusedTaskId);
145
184
  if (index <= 0) {
146
185
  return;
147
186
  }
148
- const target = scopedTasks[index - 1];
149
- if (target !== undefined) {
150
- this.options.focusTaskComposer(target.taskId);
151
- }
152
- }
153
-
154
- handleInput(input: Buffer): boolean {
155
- const workspace = this.options.workspace;
156
- return this.handleTaskPaneShortcutInput({
157
- input,
158
- mainPaneMode: workspace.mainPaneMode,
159
- taskScreenKeybindings: this.options.taskScreenKeybindings,
160
- taskEditorTarget: workspace.taskEditorTarget,
161
- homeEditorBuffer: () => this.homeEditorBuffer(),
162
- updateHomeEditorBuffer: (next) => {
163
- this.updateHomeEditorBuffer(next);
164
- },
165
- moveTaskEditorFocusUp: () => {
166
- this.moveTaskEditorFocusUp();
167
- },
168
- focusDraftComposer: () => {
169
- this.options.focusDraftComposer();
170
- },
171
- submitDraftTaskFromComposer: () => {
172
- this.submitDraftTaskFromComposer();
173
- },
174
- runTaskPaneAction: (action) => {
175
- this.options.runTaskPaneAction(action);
176
- },
177
- selectRepositoryByDirection: (direction) => {
178
- this.selectRepositoryByDirection(direction);
179
- },
180
- getTaskRepositoryDropdownOpen: () => workspace.taskRepositoryDropdownOpen,
181
- setTaskRepositoryDropdownOpen: (open) => {
182
- workspace.taskRepositoryDropdownOpen = open;
183
- },
184
- markDirty: () => {
185
- this.options.markDirty();
186
- },
187
- });
188
- }
187
+ const targetTaskId = navigationTaskIds[index - 1];
188
+ if (targetTaskId !== undefined) {
189
+ options.focusTaskComposer(targetTaskId);
190
+ }
191
+ };
192
+
193
+ const moveTaskEditorFocusDown = (): void => {
194
+ const workspace = options.workspace;
195
+ if (workspace.taskEditorTarget.kind !== 'task') {
196
+ return;
197
+ }
198
+ const navigationTaskIds = taskNavigationIds();
199
+ const focusedTaskId = workspace.taskEditorTarget.taskId;
200
+ const index = navigationTaskIds.indexOf(focusedTaskId);
201
+ if (index < 0) {
202
+ options.focusDraftComposer();
203
+ return;
204
+ }
205
+ const targetTaskId = navigationTaskIds[index + 1];
206
+ if (targetTaskId !== undefined) {
207
+ options.focusTaskComposer(targetTaskId);
208
+ return;
209
+ }
210
+ options.focusDraftComposer();
211
+ };
212
+
213
+ const matchesSequence = (input: Buffer, startIndex: number, sequence: Buffer): boolean => {
214
+ if (startIndex < 0 || startIndex + sequence.length > input.length) {
215
+ return false;
216
+ }
217
+ for (let index = 0; index < sequence.length; index += 1) {
218
+ if (input[startIndex + index] !== sequence[index]) {
219
+ return false;
220
+ }
221
+ }
222
+ return true;
223
+ };
224
+
225
+ const extractInsertText = (input: Buffer): string | null => {
226
+ const chunks: Buffer[] = [];
227
+ let inBracketedPaste = false;
228
+ for (let index = 0; index < input.length; index += 1) {
229
+ if (!inBracketedPaste && matchesSequence(input, index, BRACKETED_PASTE_START)) {
230
+ inBracketedPaste = true;
231
+ index += BRACKETED_PASTE_START.length - 1;
232
+ continue;
233
+ }
234
+ if (inBracketedPaste && matchesSequence(input, index, BRACKETED_PASTE_END)) {
235
+ inBracketedPaste = false;
236
+ index += BRACKETED_PASTE_END.length - 1;
237
+ continue;
238
+ }
239
+ const byte = input[index]!;
240
+ if (inBracketedPaste) {
241
+ chunks.push(Buffer.from([byte]));
242
+ continue;
243
+ }
244
+ if (byte === 0x1b) {
245
+ return null;
246
+ }
247
+ if (byte >= 32 && byte <= 126) {
248
+ chunks.push(Buffer.from([byte]));
249
+ }
250
+ }
251
+ if (chunks.length === 0) {
252
+ return '';
253
+ }
254
+ return Buffer.concat(chunks).toString('utf8').replace(/\r\n?/gu, '\n');
255
+ };
256
+
257
+ const handleShortcutAction = (action: string): boolean => {
258
+ const workspace = options.workspace;
259
+ switch (action) {
260
+ case 'mux.home.repo.dropdown.toggle':
261
+ workspace.taskRepositoryDropdownOpen = !workspace.taskRepositoryDropdownOpen;
262
+ options.markDirty();
263
+ return true;
264
+ case 'mux.home.repo.next':
265
+ workspace.taskRepositoryDropdownOpen = true;
266
+ selectRepositoryByDirection(1);
267
+ return true;
268
+ case 'mux.home.repo.previous':
269
+ workspace.taskRepositoryDropdownOpen = true;
270
+ selectRepositoryByDirection(-1);
271
+ return true;
272
+ case 'mux.home.task.status.ready':
273
+ options.runTaskPaneAction('task.ready');
274
+ return true;
275
+ case 'mux.home.task.status.draft':
276
+ options.runTaskPaneAction('task.draft');
277
+ return true;
278
+ case 'mux.home.task.status.complete':
279
+ options.runTaskPaneAction('task.complete');
280
+ return true;
281
+ case 'mux.home.task.reorder.up':
282
+ options.runTaskPaneAction('task.reorder-up');
283
+ return true;
284
+ case 'mux.home.task.reorder.down':
285
+ options.runTaskPaneAction('task.reorder-down');
286
+ return true;
287
+ case 'mux.home.task.newline':
288
+ updateHomeEditorBuffer(insertTaskComposerText(homeEditorBuffer(), '\n'));
289
+ return true;
290
+ case 'mux.home.task.queue':
291
+ if (workspace.taskEditorTarget.kind === 'draft') {
292
+ submitDraftTaskFromComposer('queue');
293
+ } else {
294
+ options.focusDraftComposer();
295
+ }
296
+ return true;
297
+ case 'mux.home.task.submit':
298
+ if (workspace.taskEditorTarget.kind === 'draft') {
299
+ submitDraftTaskFromComposer('ready');
300
+ } else {
301
+ options.focusDraftComposer();
302
+ }
303
+ return true;
304
+ case 'mux.home.editor.cursor.left':
305
+ updateHomeEditorBuffer(taskComposerMoveLeft(homeEditorBuffer()));
306
+ return true;
307
+ case 'mux.home.editor.cursor.right':
308
+ updateHomeEditorBuffer(taskComposerMoveRight(homeEditorBuffer()));
309
+ return true;
310
+ case 'mux.home.editor.cursor.up': {
311
+ const vertical = taskComposerMoveVertical(homeEditorBuffer(), -1);
312
+ if (vertical.hitBoundary) {
313
+ moveTaskEditorFocusUp();
314
+ } else {
315
+ updateHomeEditorBuffer(vertical.next);
316
+ }
317
+ return true;
318
+ }
319
+ case 'mux.home.editor.cursor.down':
320
+ if (workspace.taskEditorTarget.kind === 'task') {
321
+ const vertical = taskComposerMoveVertical(homeEditorBuffer(), 1);
322
+ if (vertical.hitBoundary) {
323
+ moveTaskEditorFocusDown();
324
+ } else {
325
+ updateHomeEditorBuffer(vertical.next);
326
+ }
327
+ } else {
328
+ updateHomeEditorBuffer(taskComposerMoveVertical(homeEditorBuffer(), 1).next);
329
+ }
330
+ return true;
331
+ case 'mux.home.editor.line.start':
332
+ updateHomeEditorBuffer(taskComposerMoveLineStart(homeEditorBuffer()));
333
+ return true;
334
+ case 'mux.home.editor.line.end':
335
+ updateHomeEditorBuffer(taskComposerMoveLineEnd(homeEditorBuffer()));
336
+ return true;
337
+ case 'mux.home.editor.word.left':
338
+ updateHomeEditorBuffer(taskComposerMoveWordLeft(homeEditorBuffer()));
339
+ return true;
340
+ case 'mux.home.editor.word.right':
341
+ updateHomeEditorBuffer(taskComposerMoveWordRight(homeEditorBuffer()));
342
+ return true;
343
+ case 'mux.home.editor.delete.backward':
344
+ updateHomeEditorBuffer(taskComposerBackspace(homeEditorBuffer()));
345
+ return true;
346
+ case 'mux.home.editor.delete.forward':
347
+ updateHomeEditorBuffer(taskComposerDeleteForward(homeEditorBuffer()));
348
+ return true;
349
+ case 'mux.home.editor.delete.word.backward':
350
+ updateHomeEditorBuffer(taskComposerDeleteWordLeft(homeEditorBuffer()));
351
+ return true;
352
+ case 'mux.home.editor.delete.line.start':
353
+ updateHomeEditorBuffer(taskComposerDeleteToLineStart(homeEditorBuffer()));
354
+ return true;
355
+ case 'mux.home.editor.delete.line.end':
356
+ updateHomeEditorBuffer(taskComposerDeleteToLineEnd(homeEditorBuffer()));
357
+ return true;
358
+ default:
359
+ return false;
360
+ }
361
+ };
362
+
363
+ const handleInput = (input: Buffer): boolean => {
364
+ const workspace = options.workspace;
365
+ if (workspace.mainPaneMode !== 'home' || workspace.leftNavSelection.kind !== 'tasks') {
366
+ return false;
367
+ }
368
+ const action = detectTaskScreenKeybindingAction(input, options.taskScreenKeybindings);
369
+ if (action !== null && handleShortcutAction(action)) {
370
+ return true;
371
+ }
372
+ const inserted = extractInsertText(input);
373
+ if (inserted === null || inserted.length === 0) {
374
+ return false;
375
+ }
376
+ updateHomeEditorBuffer(insertTaskComposerText(homeEditorBuffer(), inserted));
377
+ return true;
378
+ };
379
+
380
+ return {
381
+ homeEditorBuffer,
382
+ updateHomeEditorBuffer,
383
+ selectRepositoryByDirection,
384
+ submitDraftTaskFromComposer,
385
+ moveTaskEditorFocusUp,
386
+ moveTaskEditorFocusDown,
387
+ handleInput,
388
+ };
189
389
  }
@@ -0,0 +1,25 @@
1
+ import type { RuntimeWorkspaceObservedQueuedReaction } from './runtime-workspace-observed-transition-policy.ts';
2
+
3
+ export interface RuntimeWorkspaceObservedEffectQueueOptions {
4
+ // Must enqueue work; callers should not execute reactions inline from the subscriber path.
5
+ readonly enqueueQueuedReaction: (task: () => Promise<void>, label: string) => void;
6
+ readonly unsubscribeConversationEvents: (sessionId: string) => Promise<void>;
7
+ readonly activateConversation: (sessionId: string) => Promise<void>;
8
+ }
9
+
10
+ export function enqueueRuntimeWorkspaceObservedReactions(input: {
11
+ readonly reactions: readonly RuntimeWorkspaceObservedQueuedReaction[];
12
+ readonly options: RuntimeWorkspaceObservedEffectQueueOptions;
13
+ }): void {
14
+ for (const reaction of input.reactions) {
15
+ if (reaction.kind === 'unsubscribe-conversation') {
16
+ input.options.enqueueQueuedReaction(async () => {
17
+ await input.options.unsubscribeConversationEvents(reaction.sessionId);
18
+ }, reaction.label);
19
+ continue;
20
+ }
21
+ input.options.enqueueQueuedReaction(async () => {
22
+ await input.options.activateConversation(reaction.sessionId);
23
+ }, reaction.label);
24
+ }
25
+ }