@jmoyers/harness 0.1.10 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/README.md +31 -35
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. package/src/ui/surface.ts +0 -224
@@ -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
+ }